redeem proffered tickets, db schema 1.1.0
[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: 1,
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 async almanacGetAll(dbCtx) {
229 const _scope = _fileScope('almanacGetAll');
230 this.logger.debug(_scope, 'called');
231
232 try {
233 return await dbCtx.manyOrNone(this.statement.almanacGetAll);
234 } catch (e) {
235 this.logger.error(_scope, 'failed', { error: e });
236 throw e;
237 }
238 }
239
240
241 async almanacUpsert(dbCtx, event, date) {
242 const _scope = _fileScope('almanacUpsert');
243 this.logger.debug(_scope, 'called', { event, date });
244
245 try {
246 const result = await dbCtx.result(this.statement.almanacUpsert, { event, date: date ?? new Date() });
247 if (result.rowCount != 1) {
248 throw new DBErrors.UnexpectedResult('did not upsert almanac event');
249 }
250 } catch (e) {
251 this.logger.error(_scope, 'failed', { error: e, event, date });
252 throw e;
253 }
254 }
255
256
257 async authenticationGet(dbCtx, identifier) {
258 const _scope = _fileScope('authenticationGet');
259 this.logger.debug(_scope, 'called', { identifier });
260
261 try {
262 return await dbCtx.oneOrNone(this.statement.authenticationGet, { identifier });
263 } catch (e) {
264 this.logger.error(_scope, 'failed', { error: e, identifier });
265 throw e;
266 }
267 }
268
269
270 async authenticationSuccess(dbCtx, identifier) {
271 const _scope = _fileScope('authenticationSuccess');
272 this.logger.debug(_scope, 'called', { identifier });
273
274 try {
275 const result = await dbCtx.result(this.statement.authenticationSuccess, { identifier });
276 if (result.rowCount != 1) {
277 throw new DBErrors.UnexpectedResult('did not update authentication success event');
278 }
279 } catch (e) {
280 this.logger.error(_scope, 'failed', { error: e, identifier });
281 throw e;
282 }
283 }
284
285
286 async authenticationUpsert(dbCtx, identifier, credential) {
287 const _scope = _fileScope('authenticationUpsert');
288 const scrubbedCredential = '*'.repeat((credential || '').length);
289 this.logger.debug(_scope, 'called', { identifier, scrubbedCredential });
290
291 try {
292 const result = await dbCtx.result(this.statement.authenticationUpsert, { identifier, credential });
293 if (result.rowCount != 1) {
294 throw new DBErrors.UnexpectedResult('did not upsert authentication');
295 }
296 } catch (e) {
297 this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential });
298 throw e;
299 }
300 }
301
302
303 async profileIdentifierInsert(dbCtx, profile, identifier) {
304 const _scope = _fileScope('profileIdentifierInsert');
305 this.logger.debug(_scope, 'called', { profile, identifier });
306
307 try {
308 const result = await dbCtx.result(this.statement.profileIdentifierInsert, { profile, identifier });
309 if (result.rowCount != 1) {
310 throw new DBErrors.UnexpectedResult('did not insert identifier');
311 }
312 } catch (e) {
313 this.logger.error(_scope, 'failed', { error: e, profile, identifier });
314 throw e;
315 }
316 }
317
318
319 async profileIsValid(dbCtx, profile) {
320 const _scope = _fileScope('profileIsValid');
321 this.logger.debug(_scope, 'called', { profile });
322
323 try {
324 const profileResponse = await dbCtx.oneOrNone(this.statement.profileGet, { profile });
325 return !!profileResponse;
326 } catch (e) {
327 this.logger.error(_scope, 'failed', { error: e, profile });
328 throw e;
329 }
330 }
331
332
333 async profileScopeInsert(dbCtx, profile, scope) {
334 const _scope = _fileScope('profileScopeInsert');
335 this.logger.debug(_scope, 'called', { profile, scope });
336
337 try {
338 const result = await dbCtx.result(this.statement.profileScopeInsert, { profile, scope });
339 // Duplicate inserts get ignored
340 if (result.rowCount != 1 && result.rowCount != 0) {
341 throw new DBErrors.UnexpectedResult('did not insert profile scope');
342 }
343 } catch (e) {
344 this.logger.error(_scope, 'failed', { error: e, profile, scope });
345 throw e;
346 }
347 }
348
349
350 async profileScopesSetAll(dbCtx, profile, scopes) {
351 const _scope = _fileScope('profileScopesSetAll');
352 this.logger.debug(_scope, 'called', { profile, scopes });
353
354 try {
355 await this.transaction(dbCtx, async (txCtx) => {
356 await txCtx.result(this.statement.profileScopesClear, { profile });
357 if (scopes.length) {
358 await txCtx.result(this.statement.profileScopesSetAll, { profile, scopes });
359 }
360 }); // transaction
361 } catch (e) {
362 this.logger.error(_scope, 'failed', { error: e, profile, scopes });
363 throw e;
364 }
365 }
366
367
368 async profilesScopesByIdentifier(dbCtx, identifier) {
369 const _scope = _fileScope('profilesScopesByIdentifier');
370 this.logger.debug(_scope, 'called', { identifier });
371
372 try {
373 const profileScopesRows = await dbCtx.manyOrNone(this.statement.profilesScopesByIdentifier, { identifier });
374 return Database._profilesScopesBuilder(profileScopesRows);
375 } catch (e) {
376 this.logger.error(_scope, 'failed', { error: e, identifier });
377 throw e;
378 }
379 }
380
381
382 async redeemCode(dbCtx, { codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshLifespanSeconds, resource, profileData }) {
383 const _scope = _fileScope('redeemCode');
384 this.logger.debug(_scope, 'called', { codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshLifespanSeconds, resource, profileData });
385
386 let result, ret = false;
387 try {
388 await this.transaction(dbCtx, async (txCtx) => {
389 result = await txCtx.result(this.statement.redeemCode, { codeId, created, isToken, clientId, profile, identifier, lifespanSeconds, refreshLifespanSeconds, resource, profileData });
390 if (result.rowCount != 1) {
391 this.logger.error(_scope, 'failed', { result });
392 throw new DBErrors.UnexpectedResult('did not redeem code');
393 }
394 // Abort and return false if redemption resulted in revocation.
395 if (result.rows[0].isRevoked) {
396 return;
397 }
398 this.logger.debug(_scope, 'code redeemed', { redeemed: result.rows[0] });
399
400 // Ensure there are entries for all scopes.
401 if (scopes.length !== 0) {
402 await txCtx.result(this.statement.scopesInsert, { scopes });
403 }
404
405 // Record accepted scopes for this token.
406 result = await txCtx.result(this.statement.tokenScopesSet, { codeId, scopes });
407 if (result.rowCount != scopes.length) {
408 this.logger.error(_scope, 'token scope count mismatch', { codeId, scopes, result });
409 throw new DBErrors.UnexpectedResult('did not set all scopes on token');
410 }
411 ret = true;
412 }); // txCtx
413 } catch (e) {
414 this.logger.error(_scope, 'failed', { error: e, codeId, created, isToken, clientId, profile, identifier, scopes, lifespanSeconds, refreshLifespanSeconds, profileData });
415 throw e;
416 }
417
418 return ret;
419 }
420
421
422 async refreshCode(dbCtx, codeId, refreshed, removeScopes) {
423 const _scope = _fileScope('refreshCode');
424 this.logger.debug(_scope, 'called', { codeId, refreshed, removeScopes });
425
426 try {
427 return await this.transaction(dbCtx, async (txCtx) => {
428 const refreshedToken = await txCtx.oneOrNone(this.statement.refreshCode, { codeId, refreshed });
429 if (refreshedToken) {
430 if (removeScopes.length) {
431 const removeResult = await txCtx.result(this.statement.tokenScopesRemove, { codeId, removeScopes });
432 if (removeResult.rowCount != removeScopes.length) {
433 this.logger.error(_scope, 'failed to remove token scopes', { actual: removeResult.rowCount, expected: removeScopes.length });
434 throw new DBErrors.UnexpectedResult('did not remove scopes from token');
435 }
436 } else {
437 delete refreshedToken.scopes; // Not updated, remove from response.
438 }
439 } else {
440 this.logger.debug(_scope, 'did not refresh token', {});
441 }
442 return refreshedToken;
443 });
444 } catch (e) {
445 this.logger.error(_scope, 'failed', { error: e, codeId });
446 throw e;
447 }
448 }
449
450
451 async resourceGet(dbCtx, resourceId) {
452 const _scope = _fileScope('resourceGet');
453 this.logger.debug(_scope, 'called', { resourceId });
454
455 try {
456 return await dbCtx.oneOrNone(this.statement.resourceGet, { resourceId });
457 } catch (e) {
458 this.logger.error(_scope, 'failed', { error: e, resourceId });
459 throw e;
460 }
461 }
462
463
464 async resourceUpsert(dbCtx, resourceId, secret, description) {
465 const _scope = _fileScope('resourceUpsert');
466 const logSecret = secret?.length && common.logTruncate('*'.repeat(secret.length), 3) || undefined;
467 this.logger.debug(_scope, 'called', { resourceId, secret: logSecret, description });
468
469 try {
470 const result = await dbCtx.result(this.statement.resourceUpsert, { resourceId, secret, description });
471 if (result.rowCount != 1) {
472 throw new DBErrors.UnexpectedResult('did not upsert resource');
473 }
474 return result.rows[0];
475 } catch (e) {
476 this.logger.error(_scope, 'failed', { error: e, resourceId, secret: logSecret, description });
477 throw e;
478 }
479 }
480
481
482 async scopeCleanup(dbCtx, atLeastMsSinceLast) {
483 const _scope = _fileScope('scopeCleanup');
484 this.logger.debug(_scope, 'called', { atLeastMsSinceLast });
485
486 const almanacEvent = Enum.AlmanacEntry.ScopeCleanup;
487 try {
488 return await this.transaction(dbCtx, async (txCtx) => {
489
490 // Check that enough time has passed since last cleanup
491 const now = new Date();
492 const cleanupNotAfter = new Date(now.getTime() - atLeastMsSinceLast);
493 const { date: lastCleanupDate } = await txCtx.oneOrNone(this.statement.almanacGet, { event: almanacEvent }) || { date: new Date(0) };
494 if (lastCleanupDate >= cleanupNotAfter) {
495 this.logger.debug(_scope, 'skipping token cleanup, too soon', { lastCleanupDate, cleanupNotAfter, atLeastMsSinceLast });
496 return;
497 }
498
499 // Do the cleanup
500 const { rowCount: scopesRemoved } = await txCtx.result(this.statement.scopeCleanup);
501
502 // Update the last cleanup time
503 const result = await txCtx.result(this.statement.almanacUpsert, { event: almanacEvent, date: now });
504 if (result.rowCount != 1) {
505 throw new DBErrors.UnexpectedResult('did not update almanac');
506 }
507
508 this.logger.debug(_scope, 'completed', { scopesRemoved, atLeastMsSinceLast });
509 return scopesRemoved;
510 }); // tx
511
512 } catch (e) {
513 this.logger.error(_scope, 'failed', { error: e, atLeastMsSinceLast });
514 throw e;
515 }
516 }
517
518
519 async scopeDelete(dbCtx, scope) {
520 const _scope = _fileScope('scopeDelete');
521 this.logger.debug(_scope, 'called', { scope });
522
523 try {
524 return await this.transaction(dbCtx, async (txCtx) => {
525 const { inUse } = await txCtx.one(this.statement.scopeInUse, { scope });
526 if (inUse) {
527 this.logger.debug(_scope, 'not deleted, in use', { scope });
528 return false;
529 }
530 const result = await txCtx.result(this.statement.scopeDelete, { scope });
531 if (result.rowCount == 0) {
532 this.logger.debug(_scope, 'no such scope', { scope });
533 } else {
534 this.logger.debug(_scope, 'deleted', { scope });
535 }
536 return true;
537 });
538 } catch (e) {
539 this.logger.error(_scope, 'failed', { error: e, scope });
540 throw e;
541 }
542 }
543
544
545 async scopeUpsert(dbCtx, scope, application, description, manuallyAdded = false) {
546 const _scope = _fileScope('scopeUpsert');
547 this.logger.debug(_scope, 'called', { scope, description });
548
549 try {
550 const result = await dbCtx.result(this.statement.scopeUpsert, { scope, application, description, manuallyAdded });
551 if (result.rowCount != 1) {
552 throw new DBErrors.UnexpectedResult('did not upsert scope');
553 }
554 } catch (e) {
555 this.logger.error(_scope, 'failed', { error: e, scope, application, description });
556 throw e;
557 }
558 }
559
560
561 async tokenCleanup(dbCtx, codeLifespanSeconds, atLeastMsSinceLast) {
562 const _scope = _fileScope('tokenCleanup');
563 this.logger.debug(_scope, 'called', { codeLifespanSeconds, atLeastMsSinceLast });
564
565 const almanacEvent = Enum.AlmanacEntry.TokenCleanup;
566 try {
567 return await this.transaction(dbCtx, async (txCtx) => {
568
569 // Check that enough time has passed since last cleanup
570 const now = new Date();
571 const cleanupNotAfter = new Date(now.getTime() - atLeastMsSinceLast);
572 const { date: lastCleanupDate } = await txCtx.oneOrNone(this.statement.almanacGet, { event: almanacEvent }) || { date: new Date(0) };
573 if (lastCleanupDate >= cleanupNotAfter) {
574 this.logger.debug(_scope, 'skipping token cleanup, too soon', { lastCleanupDate, cleanupNotAfter, codeLifespanSeconds, atLeastMsSinceLast });
575 return;
576 }
577
578 // Do the cleanup
579 const { rowCount: tokensRemoved } = await txCtx.result(this.statement.tokenCleanup, { codeLifespanSeconds });
580
581 // Update the last cleanup time
582 const result = await txCtx.result(this.statement.almanacUpsert, { event: almanacEvent, date: now });
583 if (result.rowCount != 1) {
584 throw new DBErrors.UnexpectedResult('did not update almanac');
585 }
586
587 this.logger.debug(_scope, 'completed', { tokensRemoved, codeLifespanSeconds, atLeastMsSinceLast });
588 return tokensRemoved;
589 }); // tx
590
591 } catch (e) {
592 this.logger.error(_scope, 'failed', { error: e, atLeastMsSinceLast });
593 throw e;
594 }
595 }
596
597
598 async tokenGetByCodeId(dbCtx, codeId) {
599 const _scope = _fileScope('tokenGetByCodeId');
600 this.logger.debug(_scope, 'called', { codeId });
601
602 try {
603 return await dbCtx.oneOrNone(this.statement.tokenGetByCodeId, { codeId });
604 } catch (e) {
605 this.logger.error(_scope, 'failed', { error: e, codeId });
606 throw e;
607 }
608 }
609
610
611 async tokenRevokeByCodeId(dbCtx, codeId) {
612 const _scope = _fileScope('tokenRevokeByCodeId');
613 this.logger.debug(_scope, 'called', { codeId });
614
615 try {
616 const result = await dbCtx.result(this.statement.tokenRevokeByCodeId, { codeId });
617 if (result.rowCount != 1) {
618 throw new DBErrors.UnexpectedResult('did not revoke token');
619 }
620 } catch (e) {
621 this.logger.error(_scope, 'failed', { error: e, codeId });
622 throw e;
623 }
624 }
625
626
627 async tokenRefreshRevokeByCodeId(dbCtx, codeId) {
628 const _scope = _fileScope('tokenRefreshRevokeByCodeId');
629 this.logger.debug(_scope, 'called', { codeId });
630
631 try {
632 const result = await dbCtx.result(this.statement.tokenRefreshRevokeByCodeId, { codeId });
633 if (result.rowCount != 1) {
634 throw new DBErrors.UnexpectedResult('did not revoke token');
635 }
636 } catch (e) {
637 this.logger.error(_scope, 'failed', { error: e, codeId });
638 throw e;
639 }
640 }
641
642
643 async tokensGetByIdentifier(dbCtx, identifier) {
644 const _scope = _fileScope('tokensGetByIdentifier');
645 this.logger.debug(_scope, 'called', { identifier });
646
647 try {
648 return await dbCtx.manyOrNone(this.statement.tokensGetByIdentifier, { identifier });
649 } catch (e) {
650 this.logger.error(_scope, 'failed', { error: e, identifier });
651 throw e;
652 }
653 }
654
655
656 async ticketRedeemed(dbCtx, redeemedData) {
657 const _scope = _fileScope('ticketRedeemed');
658 this.logger.debug(_scope, 'called', { ...redeemedData });
659
660 try {
661 const result = await dbCtx.result(this.statement.ticketRedeemed, redeemedData);
662 if (result.rowCount != 1) {
663 throw new DBErrors.UnexpectedResult('did not store redeemed ticket');
664 }
665 } catch (e) {
666 this.logger.error(_scope, 'failed', { error: e, ...redeemedData });
667 throw e;
668 }
669 }
670
671
672 async ticketTokenPublished(dbCtx, redeemedData) {
673 const _scope = _fileScope('ticketRedeemed');
674 this.logger.debug(_scope, 'called', { ...redeemedData });
675
676 const almanacEvent = Enum.AlmanacEntry.TicketPublished;
677 try {
678 const result = await dbCtx.result(this.statement.ticketTokenPublished, redeemedData);
679 if (result.rowCount != 1) {
680 throw new DBErrors.UnexpectedResult('did not store redeemed ticket');
681 }
682 const almanacResult = await dbCtx.result(this.statement.almanacUpsert, { event: almanacEvent, date: new Date() });
683 if (almanacResult.rowCount != 1) {
684 throw new DBErrors.UnexpectedResult('did not update almanac');
685 }
686 } catch (e) {
687 this.logger.error(_scope, 'failed', { error: e, ...redeemedData });
688 throw e;
689 }
690 }
691
692 async ticketTokenGetUnpublished(dbCtx) {
693 const _scope = _fileScope('ticketTokenGetUnpublished');
694 this.logger.debug(_scope, 'called');
695
696 try {
697 return await dbCtx.manyOrNone(this.statement.ticketTokenGetUnpublished);
698 } catch (e) {
699 this.logger.error(_scope, 'failed', { error: e });
700 throw e;
701 }
702 }
703
704 }
705
706 module.exports = DatabasePostgres;