3 const common
= require('../../common');
4 const Enum
= require('../../enum');
5 const Database
= require('../abstract');
6 const DBErrors
= require('../errors');
7 const { unappliedSchemaVersions
} = require('../schema-version-helper');
8 const SQLite
= require('better-sqlite3');
9 const fs
= require('fs');
10 const path
= require('path');
11 const uuid
= require('uuid');
12 const { performance
} = require('perf_hooks');
14 const _fileScope
= common
.fileScope(__filename
);
16 const schemaVersionsSupported
= {
29 // max of signed int64 (2^63 - 1), should be enough
30 // const EPOCH_FOREVER = BigInt('9223372036854775807');
32 class DatabaseSQLite
extends Database
{
33 constructor(logger
, options
) {
34 super(logger
, options
);
36 const connectionString
= options
.db
.connectionString
|| 'sqlite://:memory:';
37 const csDelim
= '://';
38 const dbFilename
= connectionString
.slice(connectionString
.indexOf(csDelim
) + csDelim
.length
);
40 const queryLogLevel
= options
.db
.queryLogLevel
;
42 const sqliteOptions
= {
43 ...(queryLogLevel
&& {
44 // eslint-disable-next-line security/detect-object-injection
45 verbose: (query
) => this.logger
[queryLogLevel
](_fileScope('SQLite:verbose'), '', { query
}),
48 this.db
= new SQLite(dbFilename
, sqliteOptions
);
49 this.schemaVersionsSupported
= schemaVersionsSupported
;
50 this.changesSinceLastOptimize
= BigInt(0);
51 this.optimizeAfterChanges
= options
.db
.sqliteOptimizeAfterChanges
|| 0; // Default to no periodic optimization.
52 this.db
.pragma('foreign_keys = on'); // Enforce consistency.
53 this.db
.pragma('journal_mode = WAL'); // Be faster, expect local filesystem.
54 this.db
.defaultSafeIntegers(true); // This probably isn't necessary, but by using these BigInts we keep weird floats out of the query logs.
57 this._initStatements();
62 * Boolean to 0/1 representation for SQLite params.
63 * @param {Boolean} bool
66 static _booleanToNumeric(bool
) {
67 // eslint-disable-next-line security/detect-object-injection
76 * SQLite cannot prepare its statements without a schema, ensure such exists.
79 const _scope
= _fileScope('_initTables');
81 // Migrations rely upon this table, ensure it exists.
82 const metaVersionTable
= '_meta_schema_version';
83 const tableExists
= this.db
.prepare('SELECT name FROM sqlite_master WHERE type=:type AND name=:name').pluck(true).bind({ type: 'table', name: metaVersionTable
});
84 let metaExists
= tableExists
.get();
85 if (metaExists
=== undefined) {
86 const fPath
= path
.join(__dirname
, 'sql', 'schema', 'init.sql');
87 // eslint-disable-next-line security/detect-non-literal-fs-filename
88 const fSql
= fs
.readFileSync(fPath
, { encoding: 'utf8' });
90 metaExists
= tableExists
.get();
91 /* istanbul ignore if */
92 if (metaExists
=== undefined) {
93 throw new DBErrors
.UnexpectedResult(`did not create ${metaVersionTable} table`);
95 this.logger
.info(_scope
, 'created schema version table', { metaVersionTable
});
99 const currentSchema
= this._currentSchema();
100 const migrationsWanted
= unappliedSchemaVersions(__dirname
, currentSchema
, this.schemaVersionsSupported
);
101 this.logger
.debug(_scope
, 'schema migrations wanted', { migrationsWanted
});
102 migrationsWanted
.forEach((v
) => {
103 const fPath
= path
.join(__dirname
, 'sql', 'schema', v
, 'apply.sql');
104 // eslint-disable-next-line security/detect-non-literal-fs-filename
105 const fSql
= fs
.readFileSync(fPath
, { encoding: 'utf8' });
106 this.logger
.info(_scope
, 'applying migration', { version: v
});
113 const _scope
= _fileScope('_initStatements');
114 const sqlDir
= path
.join(__dirname
, 'sql');
117 // Decorate the statement calls we use with timing and logging.
118 const wrapFetch
= (logName
, statementName
, fn
) => {
119 const _wrapScope
= _fileScope(logName
);
120 return (...args
) => {
121 const startTimestampMs
= performance
.now();
122 const rows
= fn(...args
);
123 DatabaseSQLite
._deOphidiate(rows
);
124 const elapsedTimeMs
= performance
.now() - startTimestampMs
;
125 this.logger
.debug(_wrapScope
, 'complete', { statementName
, elapsedTimeMs
});
129 const wrapRun
= (logName
, statementName
, fn
) => {
130 const _wrapScope
= _fileScope(logName
);
131 return (...args
) => {
132 const startTimestampMs
= performance
.now();
133 const result
= fn(...args
);
134 const elapsedTimeMs
= performance
.now() - startTimestampMs
;
135 this._updateChanges(result
);
136 this.logger
.debug(_wrapScope
, 'complete', { ...result
, statementName
, elapsedTimeMs
});
137 result
.duration
= elapsedTimeMs
;
142 // eslint-disable-next-line security/detect-non-literal-fs-filename
143 for (const f
of fs
.readdirSync(sqlDir
)) {
144 const fPath
= path
.join(sqlDir
, f
);
145 const { name: fName
, ext: fExt
} = path
.parse(f
);
146 // eslint-disable-next-line security/detect-non-literal-fs-filename
147 const stat
= fs
.statSync(fPath
);
149 || fExt
.toLowerCase() !== '.sql') {
152 // eslint-disable-next-line security/detect-non-literal-fs-filename
153 const fSql
= fs
.readFileSync(fPath
, { encoding: 'utf8' });
154 const statementName
= common
.camelfy(fName
.toLowerCase(), '-');
157 statement
= this.db
.prepare(fSql
);
158 } catch (e
) /* istanbul ignore next */ {
159 this.logger
.error(_scope
, 'failed to prepare statement', { error: e
, file: f
});
162 // eslint-disable-next-line security/detect-object-injection
163 this.statement
[statementName
] = statement
;
164 const { get: origGet
, all: origAll
, run: origRun
} = statement
;
165 statement
.get = wrapFetch('SQLite:get', statementName
, origGet
.bind(statement
));
166 statement
.all
= wrapFetch('SQLite:all', statementName
, origAll
.bind(statement
));
167 statement
.run
= wrapRun('SQLite:run', statementName
, origRun
.bind(statement
));
169 this.statement
._optimize
= this.db
.prepare('SELECT * FROM pragma_optimize(0xffff)');
171 this.logger
.debug(_scope
, 'statements initialized', { statements: Object
.keys(this.statement
).length
});
175 static _deOphidiate(rows
) {
176 const rowsIsArray
= Array
.isArray(rows
);
180 const exemplaryRow
= rows
[0];
181 for (const prop
in exemplaryRow
) {
182 const camel
= common
.camelfy(prop
);
183 if (!(camel
in exemplaryRow
)) {
184 for (const d
of rows
) {
185 d
[camel
] = d
[prop
]; // eslint-disable-line security/detect-object-injection
186 delete d
[prop
]; // eslint-disable-line security/detect-object-injection
190 return rowsIsArray
? rows : rows
[0];
195 return this.db
.prepare('SELECT major, minor, patch FROM _meta_schema_version ORDER BY major DESC, minor DESC, patch DESC LIMIT 1').get();
200 const _scope
= _fileScope('healthCheck');
201 this.logger
.debug(_scope
, 'called', {});
203 throw new DBErrors
.UnexpectedResult('database is not open');
205 return { open: this.db
.open
};
215 const _scope
= _fileScope('_optimize');
217 const optimize
= this.statement
._optimize
.all();
218 this.logger
.debug(_scope
, 'optimize', { optimize
, changes: this.changesSinceLastOptimize
});
219 this.db
.pragma('optimize');
220 this.changesSinceLastOptimize
= BigInt(0);
224 _updateChanges(dbResult
) {
225 if (this.optimizeAfterChanges
) {
226 this.changesSinceLastOptimize
+= BigInt(dbResult
.changes
);
227 if (this.changesSinceLastOptimize
>= this.optimizeAfterChanges
) {
234 _purgeTables(really
) {
243 ].forEach((table
) => {
244 const result
= this.db
.prepare(`DELETE FROM ${table}`).run();
245 this.logger
.debug(_fileScope('_purgeTables'), 'success', { table
, result
});
256 transaction(dbCtx
, fn
) {
257 dbCtx
= dbCtx
|| this.db
;
258 return dbCtx
.transaction(fn
)();
262 static _almanacToNative(entry
) {
265 date: new Date(Number(entry
.epoch
) * 1000),
270 almanacGetAll(dbCtx
) { // eslint-disable-line no-unused-vars
271 const _scope
= _fileScope('almanacGetAll');
272 this.logger
.debug(_scope
, 'called');
275 const entries
= this.statement
.almanacGetAll
.all();
276 return entries
.map((entry
) => DatabaseSQLite
._almanacToNative(entry
));
278 this.logger
.error(_scope
, 'failed', { error: e
});
284 almanacUpsert(dbCtx
, event
, date
) {
285 const _scope
= _fileScope('almanacUpsert');
286 this.logger
.debug(_scope
, 'called', { event
, date
});
289 const epoch
= common
.dateToEpoch(date
);
290 const result
= this.statement
.almanacUpsert
.run({ event
, epoch
});
291 if (result
.changes
!= 1) {
292 throw new DBErrors
.UnexpectedResult('did not upsert almanac event');
295 this.logger
.error(_scope
, 'failed', { error: e
, event
, date
});
301 static _authenticationToNative(authentication
) {
302 if (authentication
) {
303 authentication
.created
= new Date(Number(authentication
.created
) * 1000);
304 authentication
.lastAuthentication
= new Date(Number(authentication
.lastAuthentication
) * 1000);
306 return authentication
;
310 authenticationGet(dbCtx
, identifier
) {
311 const _scope
= _fileScope('authenticationGet');
312 this.logger
.debug(_scope
, 'called', { identifier
});
315 const authentication
= this.statement
.authenticationGet
.get({ identifier
});
316 return DatabaseSQLite
._authenticationToNative(authentication
);
318 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
324 authenticationSuccess(dbCtx
, identifier
) {
325 const _scope
= _fileScope('authenticationSuccess');
326 this.logger
.debug(_scope
, 'called', { identifier
});
329 const result
= this.statement
.authenticationSuccess
.run({ identifier
});
330 if (result
.changes
!= 1) {
331 throw new DBErrors
.UnexpectedResult('did not update authentication success');
334 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
340 authenticationUpsert(dbCtx
, identifier
, credential
) {
341 const _scope
= _fileScope('authenticationUpsert');
342 const scrubbedCredential
= '*'.repeat((credential
|| '').length
);
343 this.logger
.debug(_scope
, 'called', { identifier
, scrubbedCredential
});
347 result
= this.statement
.authenticationUpsert
.run({ identifier
, credential
});
348 if (result
.changes
!= 1) {
349 throw new DBErrors
.UnexpectedResult('did not upsert authentication');
352 this.logger
.error(_scope
, 'failed', { error: e
, identifier
, scrubbedCredential
});
358 profileIdentifierInsert(dbCtx
, profile
, identifier
) {
359 const _scope
= _fileScope('profileIdentifierInsert');
360 this.logger
.debug(_scope
, 'called', { profile
, identifier
});
363 const result
= this.statement
.profileIdentifierInsert
.run({ profile
, identifier
});
364 if (result
.changes
!= 1) {
365 throw new DBErrors
.UnexpectedResult('did not insert profile identifier relationship');
368 this.logger
.error(_scope
, 'failed', { error: e
, profile
, identifier
});
374 profileIsValid(dbCtx
, profile
) {
375 const _scope
= _fileScope('profileIsValid');
376 this.logger
.debug(_scope
, 'called', { profile
});
379 const profileResponse
= this.statement
.profileGet
.get({ profile
});
380 return !!profileResponse
;
382 this.logger
.error(_scope
, 'failed', { error: e
, profile
});
388 profileScopeInsert(dbCtx
, profile
, scope
) {
389 const _scope
= _fileScope('profileScopeInsert');
390 this.logger
.debug(_scope
, 'called', { profile
, scope
});
393 const result
= this.statement
.profileScopeInsert
.run({ profile
, scope
});
394 // Duplicate inserts get ignored
395 if (result
.changes
!= 1 && result
.changes
!= 0) {
396 throw new DBErrors
.UnexpectedResult('did not insert profile scope');
399 this.logger
.error(_scope
, 'failed', { error: e
, profile
, scope
});
405 profileScopesSetAll(dbCtx
, profile
, scopes
) {
406 const _scope
= _fileScope('profileScopesSetAll');
407 this.logger
.debug(_scope
, 'called', { profile
, scopes
});
410 this.transaction(dbCtx
, () => {
411 this.statement
.profileScopesClear
.run({ profile
});
413 scopes
.forEach((scope
) => {
414 this.statement
.profileScopeInsert
.run({ profile
, scope
});
419 this.logger
.error(_scope
, 'failed', { error: e
, profile
, scopes
});
425 profilesScopesByIdentifier(dbCtx
, identifier
) {
426 const _scope
= _fileScope('profilesScopesByIdentifier');
427 this.logger
.debug(_scope
, 'called', { identifier
});
430 const profileScopesRows
= this.statement
.profilesScopesByIdentifier
.all({ identifier
});
431 return Database
._profilesScopesBuilder(profileScopesRows
);
433 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
439 redeemCode(dbCtx
, { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
}) {
440 const _scope
= _fileScope('redeemCode');
441 this.logger
.debug(_scope
, 'called', { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
});
443 let result
, ret
= false;
446 profileData
= JSON
.stringify(profileData
);
448 this.transaction(dbCtx
, () => {
449 result
= this.statement
.redeemCode
.get({ codeId
, created: common
.dateToEpoch(created
), isToken: DatabaseSQLite
._booleanToNumeric(isToken
), clientId
, profile
, identifier
, lifespanSeconds
, refreshLifespanSeconds
, profileData
});
451 this.logger
.error(_scope
, 'failed', { result
});
452 throw new DBErrors
.UnexpectedResult('did not redeem code');
454 // Abort and return false if redemption resulted in revocation.
455 if (result
.isRevoked
) {
459 // Ensure there are entries for all scopes, and associate with token.
460 scopes
.forEach((scope
) => {
461 this.statement
.scopeInsert
.run({ scope
});
462 this.statement
.tokenScopeSet
.run({ codeId
, scope
});
467 this.logger
.error(_scope
, 'failed', { error: e
, codeId
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
});
474 static _refreshCodeResponseToNative(refreshResponse
) {
475 if (refreshResponse
) {
476 ['expires', 'refreshExpires'].forEach((epochField
) => {
477 if (refreshResponse
[epochField
]) { // eslint-disable-line security/detect-object-injection
478 refreshResponse
[epochField
] = new Date(Number(refreshResponse
[epochField
]) * 1000); // eslint-disable-line security/detect-object-injection
482 return refreshResponse
;
486 refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
) {
487 const _scope
= _fileScope('refreshCode');
488 this.logger
.debug(_scope
, 'called', { codeId
, refreshed
, removeScopes
});
491 return this.transaction(dbCtx
, () => {
492 const refreshResponse
= this.statement
.refreshCode
.get({ codeId
, refreshed: common
.dateToEpoch(refreshed
) });
493 if (refreshResponse
) {
494 removeScopes
.forEach((scope
) => {
495 const result
= this.statement
.tokenScopeRemove
.run({ codeId
, scope
});
496 if (result
?.changes
!= 1) {
497 this.logger
.error(_scope
, 'failed to remove token scope', { codeId
, scope
});
498 throw new DBErrors
.UnexpectedResult('did not remove scope from token');
501 if (removeScopes
.length
) {
502 refreshResponse
.scopes
= (this.statement
.tokenScopesGetByCodeId
.all({ codeId
}) || [])
503 .map((row
) => row
.scope
);
506 this.logger
.debug(_scope
, 'did not refresh token', {});
508 return DatabaseSQLite
._refreshCodeResponseToNative(refreshResponse
);
511 this.logger
.error(_scope
, 'failed', { error: e
, codeId
, refreshed
});
517 static _resourceToNative(resource
) {
519 resource
.created
= new Date(Number(resource
.created
) * 1000);
525 resourceGet(dbCtx
, resourceId
) {
526 const _scope
= _fileScope('resourceGet');
527 this.logger
.debug(_scope
, 'called', { resourceId
});
530 const resource
= this.statement
.resourceGet
.get({ resourceId
});
531 return DatabaseSQLite
._resourceToNative(resource
);
533 this.logger
.error(_scope
, 'failed', { error: e
, resourceId
});
539 resourceUpsert(dbCtx
, resourceId
, secret
, description
) {
540 const _scope
= _fileScope('resourceUpsert');
541 this.logger
.debug(_scope
, 'called', { resourceId
});
545 resourceId
= uuid
.v4();
547 const result
= this.statement
.resourceUpsert
.run({ resourceId
, secret
, description
});
548 if (result
.changes
!= 1) {
549 throw new DBErrors
.UnexpectedResult('did not upsert resource');
551 const resource
= this.statement
.resourceGet
.get({ resourceId
});
552 return DatabaseSQLite
._resourceToNative(resource
);
554 this.logger
.error(_scope
, 'failed', { error: e
, resourceId
, secret
, description
});
560 scopeCleanup(dbCtx
, atLeastMsSinceLast
) {
561 const _scope
= _fileScope('scopeCleanup');
562 this.logger
.debug(_scope
, 'called', { atLeastMsSinceLast
});
564 const almanacEvent
= Enum
.AlmanacEntry
.ScopeCleanup
;
566 return this.db
.transaction(() => {
568 // Check that enough time has passed since last cleanup
569 const nowEpoch
= BigInt(common
.dateToEpoch());
570 const { epoch: lastCleanupEpoch
} = this.statement
.almanacGet
.get({ event: almanacEvent
}) || { epoch: 0n
};
571 const elapsedMs
= (nowEpoch
- lastCleanupEpoch
) * 1000n
;
572 if (elapsedMs
< atLeastMsSinceLast
) {
573 this.logger
.debug(_scope
, 'skipping token cleanup, too soon', { lastCleanupEpoch
, elapsedMs
, atLeastMsSinceLast
});
578 const { changes: scopesRemoved
} = this.statement
.scopeCleanup
.run();
580 // Update the last cleanup time
581 const result
= this.statement
.almanacUpsert
.run({ event: almanacEvent
, epoch: nowEpoch
});
582 if (result
.changes
!= 1) {
583 throw new DBErrors
.UnexpectedResult('did not update almanac');
586 this.logger
.debug(_scope
, 'finished', { scopesRemoved
, atLeastMsSinceLast
});
587 return scopesRemoved
;
590 this.logger
.error(_scope
, 'failed', { error: e
, atLeastMsSinceLast
});
596 scopeDelete(dbCtx
, scope
) {
597 const _scope
= _fileScope('scopeDelete');
598 this.logger
.debug(_scope
, 'called', { scope
});
601 return this.transaction(dbCtx
, () => {
602 const { inUse
} = this.statement
.scopeInUse
.get({ scope
});
604 this.logger
.debug(_scope
, 'not deleted, in use', { scope
});
607 const result
= this.statement
.scopeDelete
.run({ scope
});
608 if (result
.changes
== 0) {
609 this.logger
.debug(_scope
, 'no such scope', { scope
});
611 this.logger
.debug(_scope
, 'deleted', { scope
});
616 this.logger
.error(_scope
, 'failed', { error: e
, scope
});
622 scopeUpsert(dbCtx
, scope
, application
, description
, manuallyAdded
) {
623 const _scope
= _fileScope('scopeUpsert');
624 this.logger
.debug(_scope
, 'called', { scope
, application
, description
, manuallyAdded
});
627 const result
= this.statement
.scopeUpsert
.run({ scope
, application
, description
, manuallyAdded: DatabaseSQLite
._booleanToNumeric(manuallyAdded
) });
628 if (result
.changes
!= 1) {
629 throw new DBErrors
.UnexpectedResult('did not upsert scope');
632 this.logger
.error(_scope
, 'failed', { error: e
, scope
, application
, description
, manuallyAdded
});
638 tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
) {
639 const _scope
= _fileScope('tokenCleanup');
640 this.logger
.debug(_scope
, 'called', { codeLifespanSeconds
, atLeastMsSinceLast
});
642 const almanacEvent
= Enum
.AlmanacEntry
.TokenCleanup
;
644 return this.db
.transaction(() => {
646 // Check that enough time has passed since last cleanup
647 const nowEpoch
= BigInt(common
.dateToEpoch());
648 const { epoch: lastCleanupEpoch
} = this.statement
.almanacGet
.get({ event: almanacEvent
}) || { epoch: 0n
};
649 const elapsedMs
= (nowEpoch
- lastCleanupEpoch
) * 1000n
;
650 if (elapsedMs
< atLeastMsSinceLast
) {
651 this.logger
.debug(_scope
, 'skipping token cleanup, too soon', { lastCleanupEpoch
, elapsedMs
, atLeastMsSinceLast
});
656 const { changes: tokensRemoved
} = this.statement
.tokenCleanup
.run({ codeLifespanSeconds
});
658 // Update the last cleanup time
659 const result
= this.statement
.almanacUpsert
.run({ event: almanacEvent
, epoch: nowEpoch
});
660 if (result
.changes
!= 1) {
661 throw new DBErrors
.UnexpectedResult('did not update almanac');
664 this.logger
.debug(_scope
, 'finished', { tokensRemoved
, codeLifespanSeconds
, atLeastMsSinceLast
});
665 return tokensRemoved
;
668 this.logger
.error(_scope
, 'failed', { error: e
, codeLifespanSeconds
, atLeastMsSinceLast
});
674 static _tokenToNative(token
) {
676 token
.created
= new Date(Number(token
.created
) * 1000);
677 if (token
.expires
|| token
.expires
== 0) {
678 token
.expires
= new Date(Number(token
.expires
) * 1000);
680 if (token
.refreshExpires
|| token
.refreshExpires
== 0) {
681 token
.refreshExpires
= new Date(Number(token
.refreshExpires
) * 1000);
683 if (token
.refreshed
|| token
.refreshed
== 0) {
684 token
.refreshed
= new Date(Number(token
.refreshed
) * 1000);
686 token
.isRevoked
= !!token
.isRevoked
;
687 token
.isToken
= !!token
.isToken
;
688 if (token
.profileData
) {
689 token
.profileData
= JSON
.parse(token
.profileData
);
696 tokenGetByCodeId(dbCtx
, codeId
) {
697 const _scope
= _fileScope('tokenGetByCodeId');
698 this.logger
.debug(_scope
, 'called', { codeId
});
701 return this.transaction(dbCtx
, () => {
702 const token
= this.statement
.tokenGetByCodeId
.get({ codeId
});
703 token
.scopes
= (this.statement
.tokenScopesGetByCodeId
.all({ codeId
}) || [])
704 .map((row
) => row
.scope
);
705 return DatabaseSQLite
._tokenToNative(token
);
708 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
714 tokenRefreshRevokeByCodeId(dbCtx
, codeId
) {
715 const _scope
= _fileScope('tokenRefreshRevokeByCodeId');
716 this.logger
.debug(_scope
, 'called', { codeId
});
719 const result
= this.statement
.tokenRefreshRevokeByCodeId
.run({ codeId
});
720 if (result
.changes
!= 1) {
721 throw new DBErrors
.UnexpectedResult('did not revoke refresh');
724 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
730 tokenRevokeByCodeId(dbCtx
, codeId
) {
731 const _scope
= _fileScope('tokenRevokeByCodeId');
732 this.logger
.debug(_scope
, 'called', { codeId
});
735 const result
= this.statement
.tokenRevokeByCodeId
.run({ codeId
});
736 if (result
.changes
!= 1) {
737 throw new DBErrors
.UnexpectedResult('did not revoke token');
740 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
746 tokensGetByIdentifier(dbCtx
, identifier
) {
747 const _scope
= _fileScope('tokensGetByIdentifier');
748 this.logger
.debug(_scope
, 'called', { identifier
});
751 const tokens
= this.statement
.tokensGetByIdentifier
.all({ identifier
});
752 return tokens
.map(DatabaseSQLite
._tokenToNative
);
754 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
760 ticketRedeemed(dbCtx
, redeemedData
) {
761 const _scope
= _fileScope('ticketRedeemed');
762 this.logger
.debug(_scope
, 'called', { ...redeemedData
});
765 const result
= this.statement
.ticketRedeemed
.run(redeemedData
);
766 if (result
.changes
!= 1) {
767 throw new DBErrors
.UnexpectedResult('did not store redeemed ticket');
770 this.logger
.error(_scope
, 'failed', { error: e
});
776 ticketTokenPublished(dbCtx
, redeemedData
) {
777 const _scope
= _fileScope('ticketRedeemed');
778 this.logger
.debug(_scope
, 'called', { ...redeemedData
});
780 const almanacEvent
= Enum
.AlmanacEntry
.TicketPublished
;
782 const result
= this.statement
.ticketTokenPublished
.run(redeemedData
);
783 if (result
.changes
!= 1) {
784 throw new DBErrors
.UnexpectedResult('did not store redeemed ticket');
786 const epoch
= common
.dateToEpoch();
787 const almanacResult
= this.statement
.almanacUpsert
.run({ event: almanacEvent
, epoch
});
788 if (almanacResult
.changes
!= 1) {
789 throw new DBErrors
.UnexpectedResult('did not update almanac');
793 this.logger
.error(_scope
, 'failed', { error: e
});
798 static _redeemedTicketToNative(redeemedTicket
) {
799 redeemedTicket
.created
= new Date(Number(redeemedTicket
.created
) * 1000);
800 if (redeemedTicket
.published
) {
801 redeemedTicket
.published
= new Date(Number(redeemedTicket
.published
) * 1000);
803 return redeemedTicket
;
806 ticketTokenGetUnpublished() {
807 const _scope
= _fileScope('ticketTokenGetUnpublished');
808 this.logger
.debug(_scope
, 'called');
811 const unpublished
= this.statement
.ticketTokenGetUnpublished
.all();
812 return unpublished
.map((x
) => DatabaseSQLite
._redeemedTicketToNative(x
));
814 this.logger
.error(_scope
, 'failed', { error: e
});
821 module
.exports
= DatabaseSQLite
;