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 static _almanacErrorThrow() {
271 throw new DBErrors
.UnexpectedResult('did not update almanac');
275 almanacGetAll(dbCtx
) { // eslint-disable-line no-unused-vars
276 const _scope
= _fileScope('almanacGetAll');
277 this.logger
.debug(_scope
, 'called');
280 const entries
= this.statement
.almanacGetAll
.all();
281 return entries
.map((entry
) => DatabaseSQLite
._almanacToNative(entry
));
283 this.logger
.error(_scope
, 'failed', { error: e
});
289 almanacUpsert(dbCtx
, event
, date
) {
290 const _scope
= _fileScope('almanacUpsert');
291 this.logger
.debug(_scope
, 'called', { event
, date
});
294 const epoch
= common
.dateToEpoch(date
);
295 const result
= this.statement
.almanacUpsert
.run({ event
, epoch
});
296 if (result
.changes
!= 1) {
297 this.constructor._almanacErrorThrow();
300 this.logger
.error(_scope
, 'failed', { error: e
, event
, date
});
306 static _authenticationToNative(authentication
) {
307 if (authentication
) {
308 authentication
.created
= new Date(Number(authentication
.created
) * 1000);
309 authentication
.lastAuthentication
= new Date(Number(authentication
.lastAuthentication
) * 1000);
311 return authentication
;
315 authenticationGet(dbCtx
, identifier
) {
316 const _scope
= _fileScope('authenticationGet');
317 this.logger
.debug(_scope
, 'called', { identifier
});
320 const authentication
= this.statement
.authenticationGet
.get({ identifier
});
321 return DatabaseSQLite
._authenticationToNative(authentication
);
323 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
329 authenticationSuccess(dbCtx
, identifier
) {
330 const _scope
= _fileScope('authenticationSuccess');
331 this.logger
.debug(_scope
, 'called', { identifier
});
334 const result
= this.statement
.authenticationSuccess
.run({ identifier
});
335 if (result
.changes
!= 1) {
336 throw new DBErrors
.UnexpectedResult('did not update authentication success');
339 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
345 authenticationUpsert(dbCtx
, identifier
, credential
) {
346 const _scope
= _fileScope('authenticationUpsert');
347 const scrubbedCredential
= '*'.repeat((credential
|| '').length
);
348 this.logger
.debug(_scope
, 'called', { identifier
, scrubbedCredential
});
352 result
= this.statement
.authenticationUpsert
.run({ identifier
, credential
});
353 if (result
.changes
!= 1) {
354 throw new DBErrors
.UnexpectedResult('did not upsert authentication');
357 this.logger
.error(_scope
, 'failed', { error: e
, identifier
, scrubbedCredential
});
363 profileIdentifierInsert(dbCtx
, profile
, identifier
) {
364 const _scope
= _fileScope('profileIdentifierInsert');
365 this.logger
.debug(_scope
, 'called', { profile
, identifier
});
368 const result
= this.statement
.profileIdentifierInsert
.run({ profile
, identifier
});
369 if (result
.changes
!= 1) {
370 throw new DBErrors
.UnexpectedResult('did not insert profile identifier relationship');
373 this.logger
.error(_scope
, 'failed', { error: e
, profile
, identifier
});
379 profileIsValid(dbCtx
, profile
) {
380 const _scope
= _fileScope('profileIsValid');
381 this.logger
.debug(_scope
, 'called', { profile
});
384 const profileResponse
= this.statement
.profileGet
.get({ profile
});
385 return !!profileResponse
;
387 this.logger
.error(_scope
, 'failed', { error: e
, profile
});
393 profileScopeInsert(dbCtx
, profile
, scope
) {
394 const _scope
= _fileScope('profileScopeInsert');
395 this.logger
.debug(_scope
, 'called', { profile
, scope
});
398 const result
= this.statement
.profileScopeInsert
.run({ profile
, scope
});
399 // Duplicate inserts get ignored
400 if (result
.changes
!= 1 && result
.changes
!= 0) {
401 throw new DBErrors
.UnexpectedResult('did not insert profile scope');
404 this.logger
.error(_scope
, 'failed', { error: e
, profile
, scope
});
410 profileScopesSetAll(dbCtx
, profile
, scopes
) {
411 const _scope
= _fileScope('profileScopesSetAll');
412 this.logger
.debug(_scope
, 'called', { profile
, scopes
});
415 this.transaction(dbCtx
, () => {
416 this.statement
.profileScopesClear
.run({ profile
});
418 scopes
.forEach((scope
) => {
419 this.statement
.profileScopeInsert
.run({ profile
, scope
});
424 this.logger
.error(_scope
, 'failed', { error: e
, profile
, scopes
});
430 profilesScopesByIdentifier(dbCtx
, identifier
) {
431 const _scope
= _fileScope('profilesScopesByIdentifier');
432 this.logger
.debug(_scope
, 'called', { identifier
});
435 const profileScopesRows
= this.statement
.profilesScopesByIdentifier
.all({ identifier
});
436 return Database
._profilesScopesBuilder(profileScopesRows
);
438 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
444 redeemCode(dbCtx
, { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
}) {
445 const _scope
= _fileScope('redeemCode');
446 this.logger
.debug(_scope
, 'called', { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
});
448 let result
, ret
= false;
451 profileData
= JSON
.stringify(profileData
);
453 this.transaction(dbCtx
, () => {
454 result
= this.statement
.redeemCode
.get({ codeId
, created: common
.dateToEpoch(created
), isToken: DatabaseSQLite
._booleanToNumeric(isToken
), clientId
, profile
, identifier
, lifespanSeconds
, refreshLifespanSeconds
, profileData
});
456 this.logger
.error(_scope
, 'failed', { result
});
457 throw new DBErrors
.UnexpectedResult('did not redeem code');
459 // Abort and return false if redemption resulted in revocation.
460 if (result
.isRevoked
) {
464 // Ensure there are entries for all scopes, and associate with token.
465 scopes
.forEach((scope
) => {
466 this.statement
.scopeInsert
.run({ scope
});
467 this.statement
.tokenScopeSet
.run({ codeId
, scope
});
472 this.logger
.error(_scope
, 'failed', { error: e
, codeId
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
});
479 static _refreshCodeResponseToNative(refreshResponse
) {
480 if (refreshResponse
) {
481 ['expires', 'refreshExpires'].forEach((epochField
) => {
482 if (refreshResponse
[epochField
]) { // eslint-disable-line security/detect-object-injection
483 refreshResponse
[epochField
] = new Date(Number(refreshResponse
[epochField
]) * 1000); // eslint-disable-line security/detect-object-injection
487 return refreshResponse
;
491 refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
) {
492 const _scope
= _fileScope('refreshCode');
493 this.logger
.debug(_scope
, 'called', { codeId
, refreshed
, removeScopes
});
496 return this.transaction(dbCtx
, () => {
497 const refreshResponse
= this.statement
.refreshCode
.get({ codeId
, refreshed: common
.dateToEpoch(refreshed
) });
498 if (refreshResponse
) {
499 removeScopes
.forEach((scope
) => {
500 const result
= this.statement
.tokenScopeRemove
.run({ codeId
, scope
});
501 if (result
?.changes
!= 1) {
502 this.logger
.error(_scope
, 'failed to remove token scope', { codeId
, scope
});
503 throw new DBErrors
.UnexpectedResult('did not remove scope from token');
506 if (removeScopes
.length
) {
507 refreshResponse
.scopes
= (this.statement
.tokenScopesGetByCodeId
.all({ codeId
}) || [])
508 .map((row
) => row
.scope
);
511 this.logger
.debug(_scope
, 'did not refresh token', {});
513 return DatabaseSQLite
._refreshCodeResponseToNative(refreshResponse
);
516 this.logger
.error(_scope
, 'failed', { error: e
, codeId
, refreshed
});
522 static _resourceToNative(resource
) {
524 resource
.created
= new Date(Number(resource
.created
) * 1000);
530 resourceGet(dbCtx
, resourceId
) {
531 const _scope
= _fileScope('resourceGet');
532 this.logger
.debug(_scope
, 'called', { resourceId
});
535 const resource
= this.statement
.resourceGet
.get({ resourceId
});
536 return DatabaseSQLite
._resourceToNative(resource
);
538 this.logger
.error(_scope
, 'failed', { error: e
, resourceId
});
544 resourceUpsert(dbCtx
, resourceId
, secret
, description
) {
545 const _scope
= _fileScope('resourceUpsert');
546 this.logger
.debug(_scope
, 'called', { resourceId
});
550 resourceId
= uuid
.v4();
552 const result
= this.statement
.resourceUpsert
.run({ resourceId
, secret
, description
});
553 if (result
.changes
!= 1) {
554 throw new DBErrors
.UnexpectedResult('did not upsert resource');
556 const resource
= this.statement
.resourceGet
.get({ resourceId
});
557 return DatabaseSQLite
._resourceToNative(resource
);
559 this.logger
.error(_scope
, 'failed', { error: e
, resourceId
, secret
, description
});
565 scopeCleanup(dbCtx
, atLeastMsSinceLast
) {
566 const _scope
= _fileScope('scopeCleanup');
567 this.logger
.debug(_scope
, 'called', { atLeastMsSinceLast
});
569 const almanacEvent
= Enum
.AlmanacEntry
.ScopeCleanup
;
571 return this.db
.transaction(() => {
573 // Check that enough time has passed since last cleanup
574 const nowEpoch
= BigInt(common
.dateToEpoch());
575 const { epoch: lastCleanupEpoch
} = this.statement
.almanacGet
.get({ event: almanacEvent
}) || { epoch: 0n
};
576 const elapsedMs
= (nowEpoch
- lastCleanupEpoch
) * 1000n
;
577 if (elapsedMs
< atLeastMsSinceLast
) {
578 this.logger
.debug(_scope
, 'skipping token cleanup, too soon', { lastCleanupEpoch
, elapsedMs
, atLeastMsSinceLast
});
583 const { changes: scopesRemoved
} = this.statement
.scopeCleanup
.run();
585 // Update the last cleanup time
586 const result
= this.statement
.almanacUpsert
.run({ event: almanacEvent
, epoch: nowEpoch
});
587 if (result
.changes
!= 1) {
588 this.constructor._almanacErrorThrow();
591 this.logger
.debug(_scope
, 'finished', { scopesRemoved
, atLeastMsSinceLast
});
592 return scopesRemoved
;
595 this.logger
.error(_scope
, 'failed', { error: e
, atLeastMsSinceLast
});
601 scopeDelete(dbCtx
, scope
) {
602 const _scope
= _fileScope('scopeDelete');
603 this.logger
.debug(_scope
, 'called', { scope
});
606 return this.transaction(dbCtx
, () => {
607 const { inUse
} = this.statement
.scopeInUse
.get({ scope
});
609 this.logger
.debug(_scope
, 'not deleted, in use', { scope
});
612 const result
= this.statement
.scopeDelete
.run({ scope
});
613 if (result
.changes
== 0) {
614 this.logger
.debug(_scope
, 'no such scope', { scope
});
616 this.logger
.debug(_scope
, 'deleted', { scope
});
621 this.logger
.error(_scope
, 'failed', { error: e
, scope
});
627 scopeUpsert(dbCtx
, scope
, application
, description
, manuallyAdded
) {
628 const _scope
= _fileScope('scopeUpsert');
629 this.logger
.debug(_scope
, 'called', { scope
, application
, description
, manuallyAdded
});
632 const result
= this.statement
.scopeUpsert
.run({ scope
, application
, description
, manuallyAdded: DatabaseSQLite
._booleanToNumeric(manuallyAdded
) });
633 if (result
.changes
!= 1) {
634 throw new DBErrors
.UnexpectedResult('did not upsert scope');
637 this.logger
.error(_scope
, 'failed', { error: e
, scope
, application
, description
, manuallyAdded
});
643 tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
) {
644 const _scope
= _fileScope('tokenCleanup');
645 this.logger
.debug(_scope
, 'called', { codeLifespanSeconds
, atLeastMsSinceLast
});
647 const almanacEvent
= Enum
.AlmanacEntry
.TokenCleanup
;
649 return this.db
.transaction(() => {
651 // Check that enough time has passed since last cleanup
652 const nowEpoch
= BigInt(common
.dateToEpoch());
653 const { epoch: lastCleanupEpoch
} = this.statement
.almanacGet
.get({ event: almanacEvent
}) || { epoch: 0n
};
654 const elapsedMs
= (nowEpoch
- lastCleanupEpoch
) * 1000n
;
655 if (elapsedMs
< atLeastMsSinceLast
) {
656 this.logger
.debug(_scope
, 'skipping token cleanup, too soon', { lastCleanupEpoch
, elapsedMs
, atLeastMsSinceLast
});
661 const { changes: tokensRemoved
} = this.statement
.tokenCleanup
.run({ codeLifespanSeconds
});
663 // Update the last cleanup time
664 const result
= this.statement
.almanacUpsert
.run({ event: almanacEvent
, epoch: nowEpoch
});
665 if (result
.changes
!= 1) {
666 this.constructor._almanacErrorThrow();
669 this.logger
.debug(_scope
, 'finished', { tokensRemoved
, codeLifespanSeconds
, atLeastMsSinceLast
});
670 return tokensRemoved
;
673 this.logger
.error(_scope
, 'failed', { error: e
, codeLifespanSeconds
, atLeastMsSinceLast
});
679 static _tokenToNative(token
) {
681 token
.created
= new Date(Number(token
.created
) * 1000);
682 if (token
.expires
|| token
.expires
== 0) {
683 token
.expires
= new Date(Number(token
.expires
) * 1000);
685 if (token
.refreshExpires
|| token
.refreshExpires
== 0) {
686 token
.refreshExpires
= new Date(Number(token
.refreshExpires
) * 1000);
688 if (token
.refreshed
|| token
.refreshed
== 0) {
689 token
.refreshed
= new Date(Number(token
.refreshed
) * 1000);
691 token
.isRevoked
= !!token
.isRevoked
;
692 token
.isToken
= !!token
.isToken
;
693 if (token
.profileData
) {
694 token
.profileData
= JSON
.parse(token
.profileData
);
701 tokenGetByCodeId(dbCtx
, codeId
) {
702 const _scope
= _fileScope('tokenGetByCodeId');
703 this.logger
.debug(_scope
, 'called', { codeId
});
706 return this.transaction(dbCtx
, () => {
707 const token
= this.statement
.tokenGetByCodeId
.get({ codeId
});
708 token
.scopes
= (this.statement
.tokenScopesGetByCodeId
.all({ codeId
}) || [])
709 .map((row
) => row
.scope
);
710 return DatabaseSQLite
._tokenToNative(token
);
713 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
719 tokenRefreshRevokeByCodeId(dbCtx
, codeId
) {
720 const _scope
= _fileScope('tokenRefreshRevokeByCodeId');
721 this.logger
.debug(_scope
, 'called', { codeId
});
724 const result
= this.statement
.tokenRefreshRevokeByCodeId
.run({ codeId
});
725 if (result
.changes
!= 1) {
726 throw new DBErrors
.UnexpectedResult('did not revoke refresh');
729 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
735 tokenRevokeByCodeId(dbCtx
, codeId
) {
736 const _scope
= _fileScope('tokenRevokeByCodeId');
737 this.logger
.debug(_scope
, 'called', { codeId
});
740 const result
= this.statement
.tokenRevokeByCodeId
.run({ codeId
});
741 if (result
.changes
!= 1) {
742 throw new DBErrors
.UnexpectedResult('did not revoke token');
745 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
751 tokensGetByIdentifier(dbCtx
, identifier
) {
752 const _scope
= _fileScope('tokensGetByIdentifier');
753 this.logger
.debug(_scope
, 'called', { identifier
});
756 const tokens
= this.statement
.tokensGetByIdentifier
.all({ identifier
});
757 return tokens
.map(DatabaseSQLite
._tokenToNative
);
759 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
765 ticketRedeemed(dbCtx
, redeemedData
) {
766 const _scope
= _fileScope('ticketRedeemed');
767 this.logger
.debug(_scope
, 'called', { ...redeemedData
});
770 const result
= this.statement
.ticketRedeemed
.run(redeemedData
);
771 if (result
.changes
!= 1) {
772 throw new DBErrors
.UnexpectedResult('did not store redeemed ticket');
775 this.logger
.error(_scope
, 'failed', { error: e
});
781 ticketTokenPublished(dbCtx
, redeemedData
) {
782 const _scope
= _fileScope('ticketRedeemed');
783 this.logger
.debug(_scope
, 'called', { ...redeemedData
});
785 const almanacEvent
= Enum
.AlmanacEntry
.TicketPublished
;
787 const result
= this.statement
.ticketTokenPublished
.run(redeemedData
);
788 if (result
.changes
!= 1) {
789 throw new DBErrors
.UnexpectedResult('did not store redeemed ticket');
791 const epoch
= common
.dateToEpoch();
792 const almanacResult
= this.statement
.almanacUpsert
.run({ event: almanacEvent
, epoch
});
793 if (almanacResult
.changes
!= 1) {
794 this.constructor._almanacErrorThrow();
798 this.logger
.error(_scope
, 'failed', { error: e
});
803 static _redeemedTicketToNative(redeemedTicket
) {
804 redeemedTicket
.created
= new Date(Number(redeemedTicket
.created
) * 1000);
805 if (redeemedTicket
.published
) {
806 redeemedTicket
.published
= new Date(Number(redeemedTicket
.published
) * 1000);
808 return redeemedTicket
;
811 ticketTokenGetUnpublished() {
812 const _scope
= _fileScope('ticketTokenGetUnpublished');
813 this.logger
.debug(_scope
, 'called');
816 const unpublished
= this.statement
.ticketTokenGetUnpublished
.all();
817 return unpublished
.map((x
) => DatabaseSQLite
._redeemedTicketToNative(x
));
819 this.logger
.error(_scope
, 'failed', { error: e
});
826 module
.exports
= DatabaseSQLite
;