1 /* eslint-disable security/detect-object-injection */
4 const pgpInitOptions
= {
8 const path
= require('path');
9 const pgp
= require('pg-promise')(pgpInitOptions
);
10 const { unappliedSchemaVersions
} = require('../schema-version-helper');
11 const Database
= require('../abstract');
12 const DBErrors
= require('../errors');
13 const common
= require('../../common');
14 const Enum
= require('../../enum');
16 const _fileScope
= common
.fileScope(__filename
);
18 const PGTypeIdINT8
= 20; // Type Id 20 == INT8 (BIGINT)
19 const PGTypeIdINT8Array
= 1016; //Type Id 1016 == INT8[] (BIGINT[])
20 pgp
.pg
.types
.setTypeParser(PGTypeIdINT8
, BigInt
); // Type Id 20 = INT8 (BIGINT)
21 const parseBigIntArray
= pgp
.pg
.types
.getTypeParser(PGTypeIdINT8Array
); // Type Id 1016 = INT8[] (BIGINT[])
22 pgp
.pg
.types
.setTypeParser(PGTypeIdINT8Array
, (a
) => parseBigIntArray(a
).map(BigInt
));
24 const schemaVersionsSupported
= {
37 class DatabasePostgres
extends Database
{
38 constructor(logger
, options
, _pgp
= pgp
) {
39 super(logger
, options
);
41 this.db
= _pgp(options
.db
.connectionString
);
42 this.schemaVersionsSupported
= schemaVersionsSupported
;
44 // Suppress QF warnings when running tests
45 this.noWarnings
= options
.db
.noWarnings
;
48 const queryLogLevel
= options
.db
.queryLogLevel
;
50 const queryScope
= _fileScope('pgp:query');
51 pgpInitOptions
.query
= (event
) => {
52 this.logger
[queryLogLevel
](queryScope
, '', { ...common
.pick(event
|| {}, ['query', 'params']) });
57 const errorScope
= _fileScope('pgp:error');
58 pgpInitOptions
.error
= (err
, event
) => {
59 this.logger
.error(errorScope
, '', { err
, event
});
62 // Deophidiate column names in-place, log results
63 pgpInitOptions
.receive
= ({ data
, result
, ctx: event
}) => {
64 const exemplaryRow
= data
[0];
65 for (const prop
in exemplaryRow
) {
66 const camel
= common
.camelfy(prop
);
67 if (!(camel
in exemplaryRow
)) {
68 for (const d
of data
) {
76 const resultLog
= common
.pick(result
|| {}, ['command', 'rowCount', 'duration']);
77 this.logger
[queryLogLevel
](_fileScope('pgp:result'), '', { query: event
.query
, ...resultLog
});
81 // Expose these for test coverage
82 this.pgpInitOptions
= pgpInitOptions
;
85 this._initStatements(_pgp
);
89 _queryFileHelper(_pgp
) {
91 const _scope
= _fileScope('_queryFile');
92 /* istanbul ignore next */
95 ...(this.noWarnings
&& { noWarnings: this.noWarnings
}),
97 const qf
= new _pgp
.QueryFile(file
, qfParams
);
99 this.logger
.error(_scope
, 'failed to create SQL statement', { error: qf
.error
, file
});
107 async
initialize(applyMigrations
= true) {
108 const _scope
= _fileScope('initialize');
109 this.logger
.debug(_scope
, 'called', { applyMigrations
});
110 if (applyMigrations
) {
111 await
this._initTables();
113 await
super.initialize();
115 await
this.listener
.start();
120 async
_initTables(_pgp
) {
121 const _scope
= _fileScope('_initTables');
122 this.logger
.debug(_scope
, 'called', {});
124 const _queryFile
= this._queryFileHelper(_pgp
|| this._pgp
);
126 // Migrations rely upon this table, ensure it exists.
127 const metaVersionTable
= '_meta_schema_version';
129 const tableExists
= async (name
) => this.db
.oneOrNone('SELECT table_name FROM information_schema.tables WHERE table_name=$(name)', { name
});
130 let metaExists
= await
tableExists(metaVersionTable
);
132 const fPath
= path
.join(__dirname
, 'sql', 'schema', 'init.sql');
133 const initSql
= _queryFile(fPath
);
134 const results
= await
this.db
.multiResult(initSql
);
135 this.logger
.debug(_scope
, 'executed init sql', { results
});
136 metaExists
= await
tableExists(metaVersionTable
);
137 /* istanbul ignore if */
139 throw new DBErrors
.UnexpectedResult(`did not create ${metaVersionTable} table`);
141 this.logger
.info(_scope
, 'created schema version table', { metaVersionTable
});
145 const currentSchema
= await
this._currentSchema();
146 const migrationsWanted
= unappliedSchemaVersions(__dirname
, currentSchema
, this.schemaVersionsSupported
);
147 this.logger
.debug(_scope
, 'schema migrations wanted', { migrationsWanted
});
148 for (const v
of migrationsWanted
) {
149 const fPath
= path
.join(__dirname
, 'sql', 'schema', v
, 'apply.sql');
150 const migrationSql
= _queryFile(fPath
);
151 const results
= await
this.db
.multiResult(migrationSql
);
152 this.logger
.debug(_scope
, 'executed migration sql', { version: v
, results
});
153 this.logger
.info(_scope
, 'applied migration', { version: v
});
158 _initStatements(_pgp
) {
159 const _scope
= _fileScope('_initStatements');
160 const _queryFile
= this._queryFileHelper(_pgp
);
161 this.statement
= _pgp
.utils
.enumSql(path
.join(__dirname
, 'sql'), {}, _queryFile
);
162 this.logger
.debug(_scope
, 'statements initialized', { statements: Object
.keys(this.statement
).length
});
166 async
healthCheck() {
167 const _scope
= _fileScope('healthCheck');
168 this.logger
.debug(_scope
, 'called', {});
169 const c
= await
this.db
.connect();
171 return { serverVersion: c
.client
.serverVersion
};
175 async
_currentSchema() {
176 return this.db
.one('SELECT major, minor, patch FROM _meta_schema_version ORDER BY major DESC, minor DESC, patch DESC LIMIT 1');
180 async
_closeConnection() {
181 const _scope
= _fileScope('_closeConnection');
184 await
this.listener
.stop();
186 await
this._pgp
.end();
188 this.logger
.error(_scope
, 'failed', { error: e
});
194 /* istanbul ignore next */
195 async
_purgeTables(really
= false) {
196 const _scope
= _fileScope('_purgeTables');
199 await
this.db
.tx(async (t
) => {
207 ].map(async (table
) => t
.query('TRUNCATE TABLE $(table:name) CASCADE', { table
})));
211 this.logger
.error(_scope
, 'failed', { error: e
});
218 return this.db
.task(async (t
) => fn(t
));
222 // eslint-disable-next-line class-methods-use-this
223 async
transaction(dbCtx
, fn
) {
224 return dbCtx
.txIf(async (t
) => fn(t
));
228 static _almanacErrorThrow() {
229 throw new DBErrors
.UnexpectedResult('did not update almanac');
233 async
almanacGetAll(dbCtx
) {
234 const _scope
= _fileScope('almanacGetAll');
235 this.logger
.debug(_scope
, 'called');
238 return await dbCtx
.manyOrNone(this.statement
.almanacGetAll
);
240 this.logger
.error(_scope
, 'failed', { error: e
});
246 async
almanacUpsert(dbCtx
, event
, date
) {
247 const _scope
= _fileScope('almanacUpsert');
248 this.logger
.debug(_scope
, 'called', { event
, date
});
251 const result
= await dbCtx
.result(this.statement
.almanacUpsert
, { event
, date: date
?? new Date() });
252 if (result
.rowCount
!= 1) {
253 this.constructor._almanacErrorThrow();
256 this.logger
.error(_scope
, 'failed', { error: e
, event
, date
});
262 async
authenticationGet(dbCtx
, identifier
) {
263 const _scope
= _fileScope('authenticationGet');
264 this.logger
.debug(_scope
, 'called', { identifier
});
267 return await dbCtx
.oneOrNone(this.statement
.authenticationGet
, { identifier
});
269 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
275 async
authenticationSuccess(dbCtx
, identifier
) {
276 const _scope
= _fileScope('authenticationSuccess');
277 this.logger
.debug(_scope
, 'called', { identifier
});
280 const result
= await dbCtx
.result(this.statement
.authenticationSuccess
, { identifier
});
281 if (result
.rowCount
!= 1) {
282 throw new DBErrors
.UnexpectedResult('did not update authentication success event');
285 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
291 async
authenticationUpsert(dbCtx
, identifier
, credential
, otpKey
) {
292 const _scope
= _fileScope('authenticationUpsert');
293 const scrubbedCredential
= '*'.repeat((credential
|| '').length
);
294 const scrubbedOTPKey
= '*'.repeat((otpKey
|| '').length
);
295 this.logger
.debug(_scope
, 'called', { identifier
, scrubbedCredential
, scrubbedOTPKey
});
298 const result
= await dbCtx
.result(this.statement
.authenticationUpsert
, { identifier
, credential
, otpKey
});
299 if (result
.rowCount
!= 1) {
300 throw new DBErrors
.UnexpectedResult('did not upsert authentication');
303 this.logger
.error(_scope
, 'failed', { error: e
, identifier
, scrubbedCredential
, scrubbedOTPKey
});
309 async
authenticationUpdateOTPKey(dbCtx
, identifier
, otpKey
= null) {
310 const _scope
= _fileScope('authenticationUpdateOTPKey');
311 const scrubbedOTPKey
= '*'.repeat((otpKey
|| '').length
);
312 this.logger
.debug(_scope
, 'called', { identifier
, scrubbedOTPKey
});
315 const result
= await dbCtx
.result(this.statement
.authenticationUpdateOtpKey
, { identifier
, otpKey
});
316 if (result
.rowCount
!= 1) {
317 throw new DBErrors
.UnexpectedResult('did not update otpKey');
320 this.logger
.error(_scope
, 'failed', { error: e
, identifier
, scrubbedOTPKey
});
326 async
authenticationUpdateCredential(dbCtx
, identifier
, credential
) {
327 const _scope
= _fileScope('authenticationUpdateCredential');
328 const scrubbedCredential
= '*'.repeat((credential
|| '').length
);
329 this.logger
.debug(_scope
, 'called', { identifier
, scrubbedCredential
});
332 const result
= await dbCtx
.result(this.statement
.authenticationUpdateCredential
, { identifier
, credential
});
333 if (result
.rowCount
!= 1) {
334 throw new DBErrors
.UnexpectedResult('did not update credential');
337 this.logger
.error(_scope
, 'failed', { error: e
, identifier
, scrubbedCredential
});
344 async
profileIdentifierInsert(dbCtx
, profile
, identifier
) {
345 const _scope
= _fileScope('profileIdentifierInsert');
346 this.logger
.debug(_scope
, 'called', { profile
, identifier
});
349 const result
= await dbCtx
.result(this.statement
.profileIdentifierInsert
, { profile
, identifier
});
350 if (result
.rowCount
!= 1) {
351 throw new DBErrors
.UnexpectedResult('did not insert identifier');
354 this.logger
.error(_scope
, 'failed', { error: e
, profile
, identifier
});
360 async
profileIsValid(dbCtx
, profile
) {
361 const _scope
= _fileScope('profileIsValid');
362 this.logger
.debug(_scope
, 'called', { profile
});
365 const profileResponse
= await dbCtx
.oneOrNone(this.statement
.profileGet
, { profile
});
366 return !!profileResponse
;
368 this.logger
.error(_scope
, 'failed', { error: e
, profile
});
374 async
profileScopeInsert(dbCtx
, profile
, scope
) {
375 const _scope
= _fileScope('profileScopeInsert');
376 this.logger
.debug(_scope
, 'called', { profile
, scope
});
379 const result
= await dbCtx
.result(this.statement
.profileScopeInsert
, { profile
, scope
});
380 // Duplicate inserts get ignored
381 if (result
.rowCount
!= 1 && result
.rowCount
!= 0) {
382 throw new DBErrors
.UnexpectedResult('did not insert profile scope');
385 this.logger
.error(_scope
, 'failed', { error: e
, profile
, scope
});
391 async
profileScopesSetAll(dbCtx
, profile
, scopes
) {
392 const _scope
= _fileScope('profileScopesSetAll');
393 this.logger
.debug(_scope
, 'called', { profile
, scopes
});
396 await
this.transaction(dbCtx
, async (txCtx
) => {
397 await txCtx
.result(this.statement
.profileScopesClear
, { profile
});
399 await txCtx
.result(this.statement
.profileScopesSetAll
, { profile
, scopes
});
403 this.logger
.error(_scope
, 'failed', { error: e
, profile
, scopes
});
409 async
profilesScopesByIdentifier(dbCtx
, identifier
) {
410 const _scope
= _fileScope('profilesScopesByIdentifier');
411 this.logger
.debug(_scope
, 'called', { identifier
});
414 const profileScopesRows
= await dbCtx
.manyOrNone(this.statement
.profilesScopesByIdentifier
, { identifier
});
415 return Database
._profilesScopesBuilder(profileScopesRows
);
417 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
423 async
redeemCode(dbCtx
, { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, resource
, profileData
}) {
424 const _scope
= _fileScope('redeemCode');
425 this.logger
.debug(_scope
, 'called', { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, resource
, profileData
});
427 let result
, ret
= false;
429 await
this.transaction(dbCtx
, async (txCtx
) => {
430 result
= await txCtx
.result(this.statement
.redeemCode
, { codeId
, created
, isToken
, clientId
, profile
, identifier
, lifespanSeconds
, refreshLifespanSeconds
, resource
, profileData
});
431 if (result
.rowCount
!= 1) {
432 this.logger
.error(_scope
, 'failed', { result
});
433 throw new DBErrors
.UnexpectedResult('did not redeem code');
435 // Abort and return false if redemption resulted in revocation.
436 if (result
.rows
[0].isRevoked
) {
439 this.logger
.debug(_scope
, 'code redeemed', { redeemed: result
.rows
[0] });
441 // Ensure there are entries for all scopes.
442 if (scopes
.length
!== 0) {
443 await txCtx
.result(this.statement
.scopesInsert
, { scopes
});
446 // Record accepted scopes for this token.
447 result
= await txCtx
.result(this.statement
.tokenScopesSet
, { codeId
, scopes
});
448 if (result
.rowCount
!= scopes
.length
) {
449 this.logger
.error(_scope
, 'token scope count mismatch', { codeId
, scopes
, result
});
450 throw new DBErrors
.UnexpectedResult('did not set all scopes on token');
455 this.logger
.error(_scope
, 'failed', { error: e
, codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
});
463 async
refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
) {
464 const _scope
= _fileScope('refreshCode');
465 this.logger
.debug(_scope
, 'called', { codeId
, refreshed
, removeScopes
});
468 return await
this.transaction(dbCtx
, async (txCtx
) => {
469 const refreshedToken
= await txCtx
.oneOrNone(this.statement
.refreshCode
, { codeId
, refreshed
});
470 if (refreshedToken
) {
471 if (removeScopes
.length
) {
472 const removeResult
= await txCtx
.result(this.statement
.tokenScopesRemove
, { codeId
, removeScopes
});
473 if (removeResult
.rowCount
!= removeScopes
.length
) {
474 this.logger
.error(_scope
, 'failed to remove token scopes', { actual: removeResult
.rowCount
, expected: removeScopes
.length
});
475 throw new DBErrors
.UnexpectedResult('did not remove scopes from token');
478 delete refreshedToken
.scopes
; // Not updated, remove from response.
481 this.logger
.debug(_scope
, 'did not refresh token', {});
483 return refreshedToken
;
486 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
492 async
resourceGet(dbCtx
, resourceId
) {
493 const _scope
= _fileScope('resourceGet');
494 this.logger
.debug(_scope
, 'called', { resourceId
});
497 return await dbCtx
.oneOrNone(this.statement
.resourceGet
, { resourceId
});
499 this.logger
.error(_scope
, 'failed', { error: e
, resourceId
});
505 async
resourceUpsert(dbCtx
, resourceId
, secret
, description
) {
506 const _scope
= _fileScope('resourceUpsert');
507 const logSecret
= secret
?.length
&& common
.logTruncate('*'.repeat(secret
.length
), 3) || undefined;
508 this.logger
.debug(_scope
, 'called', { resourceId
, secret: logSecret
, description
});
511 const result
= await dbCtx
.result(this.statement
.resourceUpsert
, { resourceId
, secret
, description
});
512 if (result
.rowCount
!= 1) {
513 throw new DBErrors
.UnexpectedResult('did not upsert resource');
515 return result
.rows
[0];
517 this.logger
.error(_scope
, 'failed', { error: e
, resourceId
, secret: logSecret
, description
});
523 async
scopeCleanup(dbCtx
, atLeastMsSinceLast
) {
524 const _scope
= _fileScope('scopeCleanup');
525 this.logger
.debug(_scope
, 'called', { atLeastMsSinceLast
});
527 const almanacEvent
= Enum
.AlmanacEntry
.ScopeCleanup
;
529 return await
this.transaction(dbCtx
, async (txCtx
) => {
531 // Check that enough time has passed since last cleanup
532 const now
= new Date();
533 const cleanupNotAfter
= new Date(now
.getTime() - atLeastMsSinceLast
);
534 const { date: lastCleanupDate
} = await txCtx
.oneOrNone(this.statement
.almanacGet
, { event: almanacEvent
}) || { date: new Date(0) };
535 if (lastCleanupDate
>= cleanupNotAfter
) {
536 this.logger
.debug(_scope
, 'skipping token cleanup, too soon', { lastCleanupDate
, cleanupNotAfter
, atLeastMsSinceLast
});
541 const { rowCount: scopesRemoved
} = await txCtx
.result(this.statement
.scopeCleanup
);
543 // Update the last cleanup time
544 const result
= await txCtx
.result(this.statement
.almanacUpsert
, { event: almanacEvent
, date: now
});
545 if (result
.rowCount
!= 1) {
546 this.constructor._almanacErrorThrow();
549 this.logger
.debug(_scope
, 'completed', { scopesRemoved
, atLeastMsSinceLast
});
550 return scopesRemoved
;
554 this.logger
.error(_scope
, 'failed', { error: e
, atLeastMsSinceLast
});
560 async
scopeDelete(dbCtx
, scope
) {
561 const _scope
= _fileScope('scopeDelete');
562 this.logger
.debug(_scope
, 'called', { scope
});
565 return await
this.transaction(dbCtx
, async (txCtx
) => {
566 const { inUse
} = await txCtx
.one(this.statement
.scopeInUse
, { scope
});
568 this.logger
.debug(_scope
, 'not deleted, in use', { scope
});
571 const result
= await txCtx
.result(this.statement
.scopeDelete
, { scope
});
572 if (result
.rowCount
== 0) {
573 this.logger
.debug(_scope
, 'no such scope', { scope
});
575 this.logger
.debug(_scope
, 'deleted', { scope
});
580 this.logger
.error(_scope
, 'failed', { error: e
, scope
});
586 async
scopeUpsert(dbCtx
, scope
, application
, description
, manuallyAdded
= false) {
587 const _scope
= _fileScope('scopeUpsert');
588 this.logger
.debug(_scope
, 'called', { scope
, description
});
591 const result
= await dbCtx
.result(this.statement
.scopeUpsert
, { scope
, application
, description
, manuallyAdded
});
592 if (result
.rowCount
!= 1) {
593 throw new DBErrors
.UnexpectedResult('did not upsert scope');
596 this.logger
.error(_scope
, 'failed', { error: e
, scope
, application
, description
});
602 async
tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
) {
603 const _scope
= _fileScope('tokenCleanup');
604 this.logger
.debug(_scope
, 'called', { codeLifespanSeconds
, atLeastMsSinceLast
});
606 const almanacEvent
= Enum
.AlmanacEntry
.TokenCleanup
;
608 return await
this.transaction(dbCtx
, async (txCtx
) => {
610 // Check that enough time has passed since last cleanup
611 const now
= new Date();
612 const cleanupNotAfter
= new Date(now
.getTime() - atLeastMsSinceLast
);
613 const { date: lastCleanupDate
} = await txCtx
.oneOrNone(this.statement
.almanacGet
, { event: almanacEvent
}) || { date: new Date(0) };
614 if (lastCleanupDate
>= cleanupNotAfter
) {
615 this.logger
.debug(_scope
, 'skipping token cleanup, too soon', { lastCleanupDate
, cleanupNotAfter
, codeLifespanSeconds
, atLeastMsSinceLast
});
620 const { rowCount: tokensRemoved
} = await txCtx
.result(this.statement
.tokenCleanup
, { codeLifespanSeconds
});
622 // Update the last cleanup time
623 const result
= await txCtx
.result(this.statement
.almanacUpsert
, { event: almanacEvent
, date: now
});
624 if (result
.rowCount
!= 1) {
625 this.constructor._almanacErrorThrow();
628 this.logger
.debug(_scope
, 'completed', { tokensRemoved
, codeLifespanSeconds
, atLeastMsSinceLast
});
629 return tokensRemoved
;
633 this.logger
.error(_scope
, 'failed', { error: e
, atLeastMsSinceLast
});
639 async
tokenGetByCodeId(dbCtx
, codeId
) {
640 const _scope
= _fileScope('tokenGetByCodeId');
641 this.logger
.debug(_scope
, 'called', { codeId
});
644 return await dbCtx
.oneOrNone(this.statement
.tokenGetByCodeId
, { codeId
});
646 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
652 async
tokenRevokeByCodeId(dbCtx
, codeId
) {
653 const _scope
= _fileScope('tokenRevokeByCodeId');
654 this.logger
.debug(_scope
, 'called', { codeId
});
657 const result
= await dbCtx
.result(this.statement
.tokenRevokeByCodeId
, { codeId
});
658 if (result
.rowCount
!= 1) {
659 throw new DBErrors
.UnexpectedResult('did not revoke token');
662 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
668 async
tokenRefreshRevokeByCodeId(dbCtx
, codeId
) {
669 const _scope
= _fileScope('tokenRefreshRevokeByCodeId');
670 this.logger
.debug(_scope
, 'called', { codeId
});
673 const result
= await dbCtx
.result(this.statement
.tokenRefreshRevokeByCodeId
, { codeId
});
674 if (result
.rowCount
!= 1) {
675 throw new DBErrors
.UnexpectedResult('did not revoke token');
678 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
684 async
tokensGetByIdentifier(dbCtx
, identifier
) {
685 const _scope
= _fileScope('tokensGetByIdentifier');
686 this.logger
.debug(_scope
, 'called', { identifier
});
689 return await dbCtx
.manyOrNone(this.statement
.tokensGetByIdentifier
, { identifier
});
691 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
697 async
ticketRedeemed(dbCtx
, redeemedData
) {
698 const _scope
= _fileScope('ticketRedeemed');
699 this.logger
.debug(_scope
, 'called', { ...redeemedData
});
702 const result
= await dbCtx
.result(this.statement
.ticketRedeemed
, redeemedData
);
703 if (result
.rowCount
!= 1) {
704 throw new DBErrors
.UnexpectedResult('did not store redeemed ticket');
707 this.logger
.error(_scope
, 'failed', { error: e
, ...redeemedData
});
713 async
ticketTokenPublished(dbCtx
, redeemedData
) {
714 const _scope
= _fileScope('ticketRedeemed');
715 this.logger
.debug(_scope
, 'called', { ...redeemedData
});
717 const almanacEvent
= Enum
.AlmanacEntry
.TicketPublished
;
719 const result
= await dbCtx
.result(this.statement
.ticketTokenPublished
, redeemedData
);
720 if (result
.rowCount
!= 1) {
721 throw new DBErrors
.UnexpectedResult('did not store redeemed ticket');
723 const almanacResult
= await dbCtx
.result(this.statement
.almanacUpsert
, { event: almanacEvent
, date: new Date() });
724 if (almanacResult
.rowCount
!= 1) {
725 this.constructor._almanacErrorThrow();
728 this.logger
.error(_scope
, 'failed', { error: e
, ...redeemedData
});
733 async
ticketTokenGetUnpublished(dbCtx
) {
734 const _scope
= _fileScope('ticketTokenGetUnpublished');
735 this.logger
.debug(_scope
, 'called');
738 return await dbCtx
.manyOrNone(this.statement
.ticketTokenGetUnpublished
);
740 this.logger
.error(_scope
, 'failed', { error: e
});
747 module
.exports
= DatabasePostgres
;