update depedencies, changes to support updated authentication-module
[squeep-indie-auther] / src / db / postgres / index.js
1 /* eslint-disable security/detect-object-injection */
2 'use strict';
3
4 const pgpInitOptions = {
5 capSQL: true,
6 };
7
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');
15
16 const _fileScope = common.fileScope(__filename);
17
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));
23
24 const schemaVersionsSupported = {
25 min: {
26 major: 1,
27 minor: 0,
28 patch: 0,
29 },
30 max: {
31 major: 1,
32 minor: 2,
33 patch: 0,
34 },
35 };
36
37 class DatabasePostgres extends Database {
38 constructor(logger, options, _pgp = pgp) {
39 super(logger, options);
40
41 this.db = _pgp(options.db.connectionString);
42 this.schemaVersionsSupported = schemaVersionsSupported;
43
44 // Suppress QF warnings when running tests
45 this.noWarnings = options.db.noWarnings;
46
47 // Log queries
48 const queryLogLevel = options.db.queryLogLevel;
49 if (queryLogLevel) {
50 const queryScope = _fileScope('pgp:query');
51 pgpInitOptions.query = (event) => {
52 this.logger[queryLogLevel](queryScope, '', { ...common.pick(event || {}, ['query', 'params']) });
53 };
54 }
55
56 // Log errors
57 const errorScope = _fileScope('pgp:error');
58 pgpInitOptions.error = (err, event) => {
59 this.logger.error(errorScope, '', { err, event });
60 };
61
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) {
69 d[camel] = d[prop];
70 delete d[prop];
71 }
72 }
73 }
74 if (queryLogLevel) {
75 // Omitting .rows
76 const resultLog = common.pick(result || {}, ['command', 'rowCount', 'duration']);
77 this.logger[queryLogLevel](_fileScope('pgp:result'), '', { query: event.query, ...resultLog });
78 }
79 };
80
81 // Expose these for test coverage
82 this.pgpInitOptions = pgpInitOptions;
83 this._pgp = _pgp;
84
85 this._initStatements(_pgp);
86 }
87
88
89 _queryFileHelper(_pgp) {
90 return (file) => {
91 const _scope = _fileScope('_queryFile');
92 /* istanbul ignore next */
93 const qfParams = {
94 minify: true,
95 ...(this.noWarnings && { noWarnings: this.noWarnings }),
96 };
97 const qf = new _pgp.QueryFile(file, qfParams);
98 if (qf.error) {
99 this.logger.error(_scope, 'failed to create SQL statement', { error: qf.error, file });
100 throw qf.error;
101 }
102 return qf;
103 };
104 }
105
106
107 async initialize(applyMigrations = true) {
108 const _scope = _fileScope('initialize');
109 this.logger.debug(_scope, 'called', { applyMigrations });
110 if (applyMigrations) {
111 await this._initTables();
112 }
113 await super.initialize();
114 if (this.listener) {
115 await this.listener.start();
116 }
117 }
118
119
120 async _initTables(_pgp) {
121 const _scope = _fileScope('_initTables');
122 this.logger.debug(_scope, 'called', {});
123
124 const _queryFile = this._queryFileHelper(_pgp || this._pgp);
125
126 // Migrations rely upon this table, ensure it exists.
127 const metaVersionTable = '_meta_schema_version';
128
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);
131 if (!metaExists) {
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 */
138 if (!metaExists) {
139 throw new DBErrors.UnexpectedResult(`did not create ${metaVersionTable} table`);
140 }
141 this.logger.info(_scope, 'created schema version table', { metaVersionTable });
142 }
143
144 // Apply migrations
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 });
154 }
155 }
156
157
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 });
163 }
164
165
166 async healthCheck() {
167 const _scope = _fileScope('healthCheck');
168 this.logger.debug(_scope, 'called', {});
169 const c = await this.db.connect();
170 c.done();
171 return { serverVersion: c.client.serverVersion };
172 }
173
174
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');
177 }
178
179
180 async _closeConnection() {
181 const _scope = _fileScope('_closeConnection');
182 try {
183 if (this.listener) {
184 await this.listener.stop();
185 }
186 await this._pgp.end();
187 } catch (e) {
188 this.logger.error(_scope, 'failed', { error: e });
189 throw e;
190 }
191 }
192
193
194 /* istanbul ignore next */
195 async _purgeTables(really = false) {
196 const _scope = _fileScope('_purgeTables');
197 try {
198 if (really) {
199 await this.db.tx(async (t) => {
200 await t.batch([
201 'almanac',
202 'authentication',
203 'profile',
204 'redeemed_ticket',
205 'resource',
206 'token',
207 ].map(async (table) => t.query('TRUNCATE TABLE $(table:name) CASCADE', { table })));
208 });
209 }
210 } catch (e) {
211 this.logger.error(_scope, 'failed', { error: e });
212 throw e;
213 }
214 }
215
216
217 async context(fn) {
218 return this.db.task(async (t) => fn(t));
219 }
220
221
222 // eslint-disable-next-line class-methods-use-this
223 async transaction(dbCtx, fn) {
224 return dbCtx.txIf(async (t) => fn(t));
225 }
226
227
228 static _almanacErrorThrow() {
229 throw new DBErrors.UnexpectedResult('did not update almanac');
230 }
231
232
233 async almanacGetAll(dbCtx) {
234 const _scope = _fileScope('almanacGetAll');
235 this.logger.debug(_scope, 'called');
236
237 try {
238 return await dbCtx.manyOrNone(this.statement.almanacGetAll);
239 } catch (e) {
240 this.logger.error(_scope, 'failed', { error: e });
241 throw e;
242 }
243 }
244
245
246 async almanacUpsert(dbCtx, event, date) {
247 const _scope = _fileScope('almanacUpsert');
248 this.logger.debug(_scope, 'called', { event, date });
249
250 try {
251 const result = await dbCtx.result(this.statement.almanacUpsert, { event, date: date ?? new Date() });
252 if (result.rowCount != 1) {
253 this.constructor._almanacErrorThrow();
254 }
255 } catch (e) {
256 this.logger.error(_scope, 'failed', { error: e, event, date });
257 throw e;
258 }
259 }
260
261
262 async authenticationGet(dbCtx, identifier) {
263 const _scope = _fileScope('authenticationGet');
264 this.logger.debug(_scope, 'called', { identifier });
265
266 try {
267 return await dbCtx.oneOrNone(this.statement.authenticationGet, { identifier });
268 } catch (e) {
269 this.logger.error(_scope, 'failed', { error: e, identifier });
270 throw e;
271 }
272 }
273
274
275 async authenticationSuccess(dbCtx, identifier) {
276 const _scope = _fileScope('authenticationSuccess');
277 this.logger.debug(_scope, 'called', { identifier });
278
279 try {
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');
283 }
284 } catch (e) {
285 this.logger.error(_scope, 'failed', { error: e, identifier });
286 throw e;
287 }
288 }
289
290
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 });
296
297 try {
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');
301 }
302 } catch (e) {
303 this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential, scrubbedOTPKey });
304 throw e;
305 }
306 }
307
308
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 });
313
314 try {
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');
318 }
319 } catch (e) {
320 this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedOTPKey });
321 throw e;
322 }
323 }
324
325
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 });
330
331 try {
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');
335 }
336 } catch (e) {
337 this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential });
338 throw e;
339 }
340
341 }
342
343
344 async profileIdentifierInsert(dbCtx, profile, identifier) {
345 const _scope = _fileScope('profileIdentifierInsert');
346 this.logger.debug(_scope, 'called', { profile, identifier });
347
348 try {
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');
352 }
353 } catch (e) {
354 this.logger.error(_scope, 'failed', { error: e, profile, identifier });
355 throw e;
356 }
357 }
358
359
360 async profileIsValid(dbCtx, profile) {
361 const _scope = _fileScope('profileIsValid');
362 this.logger.debug(_scope, 'called', { profile });
363
364 try {
365 const profileResponse = await dbCtx.oneOrNone(this.statement.profileGet, { profile });
366 return !!profileResponse;
367 } catch (e) {
368 this.logger.error(_scope, 'failed', { error: e, profile });
369 throw e;
370 }
371 }
372
373
374 async profileScopeInsert(dbCtx, profile, scope) {
375 const _scope = _fileScope('profileScopeInsert');
376 this.logger.debug(_scope, 'called', { profile, scope });
377
378 try {
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');
383 }
384 } catch (e) {
385 this.logger.error(_scope, 'failed', { error: e, profile, scope });
386 throw e;
387 }
388 }
389
390
391 async profileScopesSetAll(dbCtx, profile, scopes) {
392 const _scope = _fileScope('profileScopesSetAll');
393 this.logger.debug(_scope, 'called', { profile, scopes });
394
395 try {
396 await this.transaction(dbCtx, async (txCtx) => {
397 await txCtx.result(this.statement.profileScopesClear, { profile });
398 if (scopes.length) {
399 await txCtx.result(this.statement.profileScopesSetAll, { profile, scopes });
400 }
401 }); // transaction
402 } catch (e) {
403 this.logger.error(_scope, 'failed', { error: e, profile, scopes });
404 throw e;
405 }
406 }
407
408
409 async profilesScopesByIdentifier(dbCtx, identifier) {
410 const _scope = _fileScope('profilesScopesByIdentifier');
411 this.logger.debug(_scope, 'called', { identifier });
412
413 try {
414 const profileScopesRows = await dbCtx.manyOrNone(this.statement.profilesScopesByIdentifier, { identifier });
415 return Database._profilesScopesBuilder(profileScopesRows);
416 } catch (e) {
417 this.logger.error(_scope, 'failed', { error: e, identifier });
418 throw e;
419 }
420 }
421
422
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 });
426
427 let result, ret = false;
428 try {
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');
434 }
435 // Abort and return false if redemption resulted in revocation.
436 if (result.rows[0].isRevoked) {
437 return;
438 }
439 this.logger.debug(_scope, 'code redeemed', { redeemed: result.rows[0] });
440
441 // Ensure there are entries for all scopes.
442 if (scopes.length !== 0) {
443 await txCtx.result(this.statement.scopesInsert, { scopes });
444 }
445
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');
451 }
452 ret = true;
453 }); // txCtx
454 } catch (e) {
455 this.logger.error(_scope, 'failed', { error: e, codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshLifespanSeconds, profileData });
456 throw e;
457 }
458
459 return ret;
460 }
461
462
463 async refreshCode(dbCtx, codeId, refreshed, removeScopes) {
464 const _scope = _fileScope('refreshCode');
465 this.logger.debug(_scope, 'called', { codeId, refreshed, removeScopes });
466
467 try {
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');
476 }
477 } else {
478 delete refreshedToken.scopes; // Not updated, remove from response.
479 }
480 } else {
481 this.logger.debug(_scope, 'did not refresh token', {});
482 }
483 return refreshedToken;
484 });
485 } catch (e) {
486 this.logger.error(_scope, 'failed', { error: e, codeId });
487 throw e;
488 }
489 }
490
491
492 async resourceGet(dbCtx, resourceId) {
493 const _scope = _fileScope('resourceGet');
494 this.logger.debug(_scope, 'called', { resourceId });
495
496 try {
497 return await dbCtx.oneOrNone(this.statement.resourceGet, { resourceId });
498 } catch (e) {
499 this.logger.error(_scope, 'failed', { error: e, resourceId });
500 throw e;
501 }
502 }
503
504
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 });
509
510 try {
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');
514 }
515 return result.rows[0];
516 } catch (e) {
517 this.logger.error(_scope, 'failed', { error: e, resourceId, secret: logSecret, description });
518 throw e;
519 }
520 }
521
522
523 async scopeCleanup(dbCtx, atLeastMsSinceLast) {
524 const _scope = _fileScope('scopeCleanup');
525 this.logger.debug(_scope, 'called', { atLeastMsSinceLast });
526
527 const almanacEvent = Enum.AlmanacEntry.ScopeCleanup;
528 try {
529 return await this.transaction(dbCtx, async (txCtx) => {
530
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 });
537 return;
538 }
539
540 // Do the cleanup
541 const { rowCount: scopesRemoved } = await txCtx.result(this.statement.scopeCleanup);
542
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();
547 }
548
549 this.logger.debug(_scope, 'completed', { scopesRemoved, atLeastMsSinceLast });
550 return scopesRemoved;
551 }); // tx
552
553 } catch (e) {
554 this.logger.error(_scope, 'failed', { error: e, atLeastMsSinceLast });
555 throw e;
556 }
557 }
558
559
560 async scopeDelete(dbCtx, scope) {
561 const _scope = _fileScope('scopeDelete');
562 this.logger.debug(_scope, 'called', { scope });
563
564 try {
565 return await this.transaction(dbCtx, async (txCtx) => {
566 const { inUse } = await txCtx.one(this.statement.scopeInUse, { scope });
567 if (inUse) {
568 this.logger.debug(_scope, 'not deleted, in use', { scope });
569 return false;
570 }
571 const result = await txCtx.result(this.statement.scopeDelete, { scope });
572 if (result.rowCount == 0) {
573 this.logger.debug(_scope, 'no such scope', { scope });
574 } else {
575 this.logger.debug(_scope, 'deleted', { scope });
576 }
577 return true;
578 });
579 } catch (e) {
580 this.logger.error(_scope, 'failed', { error: e, scope });
581 throw e;
582 }
583 }
584
585
586 async scopeUpsert(dbCtx, scope, application, description, manuallyAdded = false) {
587 const _scope = _fileScope('scopeUpsert');
588 this.logger.debug(_scope, 'called', { scope, description });
589
590 try {
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');
594 }
595 } catch (e) {
596 this.logger.error(_scope, 'failed', { error: e, scope, application, description });
597 throw e;
598 }
599 }
600
601
602 async tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast) {
603 const _scope = _fileScope('tokenCleanup');
604 this.logger.debug(_scope, 'called', { codeLifespanSeconds, atLeastMsSinceLast });
605
606 const almanacEvent = Enum.AlmanacEntry.TokenCleanup;
607 try {
608 return await this.transaction(dbCtx, async (txCtx) => {
609
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 });
616 return;
617 }
618
619 // Do the cleanup
620 const { rowCount: tokensRemoved } = await txCtx.result(this.statement.tokenCleanup, { codeLifespanSeconds });
621
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();
626 }
627
628 this.logger.debug(_scope, 'completed', { tokensRemoved, codeLifespanSeconds, atLeastMsSinceLast });
629 return tokensRemoved;
630 }); // tx
631
632 } catch (e) {
633 this.logger.error(_scope, 'failed', { error: e, atLeastMsSinceLast });
634 throw e;
635 }
636 }
637
638
639 async tokenGetByCodeId(dbCtx, codeId) {
640 const _scope = _fileScope('tokenGetByCodeId');
641 this.logger.debug(_scope, 'called', { codeId });
642
643 try {
644 return await dbCtx.oneOrNone(this.statement.tokenGetByCodeId, { codeId });
645 } catch (e) {
646 this.logger.error(_scope, 'failed', { error: e, codeId });
647 throw e;
648 }
649 }
650
651
652 async tokenRevokeByCodeId(dbCtx, codeId) {
653 const _scope = _fileScope('tokenRevokeByCodeId');
654 this.logger.debug(_scope, 'called', { codeId });
655
656 try {
657 const result = await dbCtx.result(this.statement.tokenRevokeByCodeId, { codeId });
658 if (result.rowCount != 1) {
659 throw new DBErrors.UnexpectedResult('did not revoke token');
660 }
661 } catch (e) {
662 this.logger.error(_scope, 'failed', { error: e, codeId });
663 throw e;
664 }
665 }
666
667
668 async tokenRefreshRevokeByCodeId(dbCtx, codeId) {
669 const _scope = _fileScope('tokenRefreshRevokeByCodeId');
670 this.logger.debug(_scope, 'called', { codeId });
671
672 try {
673 const result = await dbCtx.result(this.statement.tokenRefreshRevokeByCodeId, { codeId });
674 if (result.rowCount != 1) {
675 throw new DBErrors.UnexpectedResult('did not revoke token');
676 }
677 } catch (e) {
678 this.logger.error(_scope, 'failed', { error: e, codeId });
679 throw e;
680 }
681 }
682
683
684 async tokensGetByIdentifier(dbCtx, identifier) {
685 const _scope = _fileScope('tokensGetByIdentifier');
686 this.logger.debug(_scope, 'called', { identifier });
687
688 try {
689 return await dbCtx.manyOrNone(this.statement.tokensGetByIdentifier, { identifier });
690 } catch (e) {
691 this.logger.error(_scope, 'failed', { error: e, identifier });
692 throw e;
693 }
694 }
695
696
697 async ticketRedeemed(dbCtx, redeemedData) {
698 const _scope = _fileScope('ticketRedeemed');
699 this.logger.debug(_scope, 'called', { ...redeemedData });
700
701 try {
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');
705 }
706 } catch (e) {
707 this.logger.error(_scope, 'failed', { error: e, ...redeemedData });
708 throw e;
709 }
710 }
711
712
713 async ticketTokenPublished(dbCtx, redeemedData) {
714 const _scope = _fileScope('ticketRedeemed');
715 this.logger.debug(_scope, 'called', { ...redeemedData });
716
717 const almanacEvent = Enum.AlmanacEntry.TicketPublished;
718 try {
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');
722 }
723 const almanacResult = await dbCtx.result(this.statement.almanacUpsert, { event: almanacEvent, date: new Date() });
724 if (almanacResult.rowCount != 1) {
725 this.constructor._almanacErrorThrow();
726 }
727 } catch (e) {
728 this.logger.error(_scope, 'failed', { error: e, ...redeemedData });
729 throw e;
730 }
731 }
732
733 async ticketTokenGetUnpublished(dbCtx) {
734 const _scope = _fileScope('ticketTokenGetUnpublished');
735 this.logger.debug(_scope, 'called');
736
737 try {
738 return await dbCtx.manyOrNone(this.statement.ticketTokenGetUnpublished);
739 } catch (e) {
740 this.logger.error(_scope, 'failed', { error: e });
741 throw e;
742 }
743 }
744
745 }
746
747 module.exports = DatabasePostgres;