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
, otpKey
) {
346 const _scope
= _fileScope('authenticationUpsert');
347 const scrubbedCredential
= '*'.repeat((credential
|| '').length
);
348 const scrubbedOTPKey
= '*'.repeat((otpKey
|| '').length
);
349 this.logger
.debug(_scope
, 'called', { identifier
, scrubbedCredential
, scrubbedOTPKey
});
352 const result
= this.statement
.authenticationUpsert
.run({ identifier
, credential
, otpKey
});
353 if (result
.changes
!= 1) {
354 throw new DBErrors
.UnexpectedResult('did not upsert authentication');
357 this.logger
.error(_scope
, 'failed', { error: e
, identifier
, scrubbedCredential
, scrubbedOTPKey
});
363 authenticationUpdateCredential(dbCtx
, identifier
, credential
) {
364 const _scope
= _fileScope('authenticationUpdateCredential');
365 const scrubbedCredential
= '*'.repeat((credential
|| '').length
);
366 this.logger
.debug(_scope
, 'called', { identifier
, scrubbedCredential
});
369 const result
= this.statement
.authenticationUpdateCredential
.run({ identifier
, credential
});
370 if (result
.changes
!= 1) {
371 throw new DBErrors
.UnexpectedResult('did not update credential');
374 this.logger
.error(_scope
, 'failed', { error: e
, identifier
, scrubbedCredential
});
380 authenticationUpdateOTPKey(dbCtx
, identifier
, otpKey
) {
381 const _scope
= _fileScope('authenticationUpdateOTPKey');
382 const scrubbedOTPKey
= '*'.repeat((otpKey
|| '').length
);
383 this.logger
.debug(_scope
, 'called', { identifier
, scrubbedOTPKey
});
386 const result
= this.statement
.authenticationUpdateOtpKey
.run({ identifier
, otpKey
});
387 if (result
.changes
!= 1) {
388 throw new DBErrors
.UnexpectedResult('did not update otpKey');
391 this.logger
.error(_scope
, 'failed', { error: e
, identifier
, scrubbedOTPKey
});
397 profileIdentifierInsert(dbCtx
, profile
, identifier
) {
398 const _scope
= _fileScope('profileIdentifierInsert');
399 this.logger
.debug(_scope
, 'called', { profile
, identifier
});
402 const result
= this.statement
.profileIdentifierInsert
.run({ profile
, identifier
});
403 if (result
.changes
!= 1) {
404 throw new DBErrors
.UnexpectedResult('did not insert profile identifier relationship');
407 this.logger
.error(_scope
, 'failed', { error: e
, profile
, identifier
});
413 profileIsValid(dbCtx
, profile
) {
414 const _scope
= _fileScope('profileIsValid');
415 this.logger
.debug(_scope
, 'called', { profile
});
418 const profileResponse
= this.statement
.profileGet
.get({ profile
});
419 return !!profileResponse
;
421 this.logger
.error(_scope
, 'failed', { error: e
, profile
});
427 profileScopeInsert(dbCtx
, profile
, scope
) {
428 const _scope
= _fileScope('profileScopeInsert');
429 this.logger
.debug(_scope
, 'called', { profile
, scope
});
432 const result
= this.statement
.profileScopeInsert
.run({ profile
, scope
});
433 // Duplicate inserts get ignored
434 if (result
.changes
!= 1 && result
.changes
!= 0) {
435 throw new DBErrors
.UnexpectedResult('did not insert profile scope');
438 this.logger
.error(_scope
, 'failed', { error: e
, profile
, scope
});
444 profileScopesSetAll(dbCtx
, profile
, scopes
) {
445 const _scope
= _fileScope('profileScopesSetAll');
446 this.logger
.debug(_scope
, 'called', { profile
, scopes
});
449 this.transaction(dbCtx
, () => {
450 this.statement
.profileScopesClear
.run({ profile
});
452 scopes
.forEach((scope
) => {
453 this.statement
.profileScopeInsert
.run({ profile
, scope
});
458 this.logger
.error(_scope
, 'failed', { error: e
, profile
, scopes
});
464 profilesScopesByIdentifier(dbCtx
, identifier
) {
465 const _scope
= _fileScope('profilesScopesByIdentifier');
466 this.logger
.debug(_scope
, 'called', { identifier
});
469 const profileScopesRows
= this.statement
.profilesScopesByIdentifier
.all({ identifier
});
470 return Database
._profilesScopesBuilder(profileScopesRows
);
472 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
478 redeemCode(dbCtx
, { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
}) {
479 const _scope
= _fileScope('redeemCode');
480 this.logger
.debug(_scope
, 'called', { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
});
482 let result
, ret
= false;
485 profileData
= JSON
.stringify(profileData
);
487 this.transaction(dbCtx
, () => {
488 result
= this.statement
.redeemCode
.get({ codeId
, created: common
.dateToEpoch(created
), isToken: DatabaseSQLite
._booleanToNumeric(isToken
), clientId
, profile
, identifier
, lifespanSeconds
, refreshLifespanSeconds
, profileData
});
490 this.logger
.error(_scope
, 'failed', { result
});
491 throw new DBErrors
.UnexpectedResult('did not redeem code');
493 // Abort and return false if redemption resulted in revocation.
494 if (result
.isRevoked
) {
498 // Ensure there are entries for all scopes, and associate with token.
499 scopes
.forEach((scope
) => {
500 this.statement
.scopeInsert
.run({ scope
});
501 this.statement
.tokenScopeSet
.run({ codeId
, scope
});
506 this.logger
.error(_scope
, 'failed', { error: e
, codeId
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
});
513 static _refreshCodeResponseToNative(refreshResponse
) {
514 if (refreshResponse
) {
515 ['expires', 'refreshExpires'].forEach((epochField
) => {
516 if (refreshResponse
[epochField
]) { // eslint-disable-line security/detect-object-injection
517 refreshResponse
[epochField
] = new Date(Number(refreshResponse
[epochField
]) * 1000); // eslint-disable-line security/detect-object-injection
521 return refreshResponse
;
525 refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
) {
526 const _scope
= _fileScope('refreshCode');
527 this.logger
.debug(_scope
, 'called', { codeId
, refreshed
, removeScopes
});
530 return this.transaction(dbCtx
, () => {
531 const refreshResponse
= this.statement
.refreshCode
.get({ codeId
, refreshed: common
.dateToEpoch(refreshed
) });
532 if (refreshResponse
) {
533 removeScopes
.forEach((scope
) => {
534 const result
= this.statement
.tokenScopeRemove
.run({ codeId
, scope
});
535 if (result
?.changes
!= 1) {
536 this.logger
.error(_scope
, 'failed to remove token scope', { codeId
, scope
});
537 throw new DBErrors
.UnexpectedResult('did not remove scope from token');
540 if (removeScopes
.length
) {
541 refreshResponse
.scopes
= (this.statement
.tokenScopesGetByCodeId
.all({ codeId
}) || [])
542 .map((row
) => row
.scope
);
545 this.logger
.debug(_scope
, 'did not refresh token', {});
547 return DatabaseSQLite
._refreshCodeResponseToNative(refreshResponse
);
550 this.logger
.error(_scope
, 'failed', { error: e
, codeId
, refreshed
});
556 static _resourceToNative(resource
) {
558 resource
.created
= new Date(Number(resource
.created
) * 1000);
564 resourceGet(dbCtx
, resourceId
) {
565 const _scope
= _fileScope('resourceGet');
566 this.logger
.debug(_scope
, 'called', { resourceId
});
569 const resource
= this.statement
.resourceGet
.get({ resourceId
});
570 return DatabaseSQLite
._resourceToNative(resource
);
572 this.logger
.error(_scope
, 'failed', { error: e
, resourceId
});
578 resourceUpsert(dbCtx
, resourceId
, secret
, description
) {
579 const _scope
= _fileScope('resourceUpsert');
580 this.logger
.debug(_scope
, 'called', { resourceId
});
584 resourceId
= uuid
.v4();
586 const result
= this.statement
.resourceUpsert
.run({ resourceId
, secret
, description
});
587 if (result
.changes
!= 1) {
588 throw new DBErrors
.UnexpectedResult('did not upsert resource');
590 const resource
= this.statement
.resourceGet
.get({ resourceId
});
591 return DatabaseSQLite
._resourceToNative(resource
);
593 this.logger
.error(_scope
, 'failed', { error: e
, resourceId
, secret
, description
});
599 scopeCleanup(dbCtx
, atLeastMsSinceLast
) {
600 const _scope
= _fileScope('scopeCleanup');
601 this.logger
.debug(_scope
, 'called', { atLeastMsSinceLast
});
603 const almanacEvent
= Enum
.AlmanacEntry
.ScopeCleanup
;
605 return this.db
.transaction(() => {
607 // Check that enough time has passed since last cleanup
608 const nowEpoch
= BigInt(common
.dateToEpoch());
609 const { epoch: lastCleanupEpoch
} = this.statement
.almanacGet
.get({ event: almanacEvent
}) || { epoch: 0n
};
610 const elapsedMs
= (nowEpoch
- lastCleanupEpoch
) * 1000n
;
611 if (elapsedMs
< atLeastMsSinceLast
) {
612 this.logger
.debug(_scope
, 'skipping token cleanup, too soon', { lastCleanupEpoch
, elapsedMs
, atLeastMsSinceLast
});
617 const { changes: scopesRemoved
} = this.statement
.scopeCleanup
.run();
619 // Update the last cleanup time
620 const result
= this.statement
.almanacUpsert
.run({ event: almanacEvent
, epoch: nowEpoch
});
621 if (result
.changes
!= 1) {
622 this.constructor._almanacErrorThrow();
625 this.logger
.debug(_scope
, 'finished', { scopesRemoved
, atLeastMsSinceLast
});
626 return scopesRemoved
;
629 this.logger
.error(_scope
, 'failed', { error: e
, atLeastMsSinceLast
});
635 scopeDelete(dbCtx
, scope
) {
636 const _scope
= _fileScope('scopeDelete');
637 this.logger
.debug(_scope
, 'called', { scope
});
640 return this.transaction(dbCtx
, () => {
641 const { inUse
} = this.statement
.scopeInUse
.get({ scope
});
643 this.logger
.debug(_scope
, 'not deleted, in use', { scope
});
646 const result
= this.statement
.scopeDelete
.run({ scope
});
647 if (result
.changes
== 0) {
648 this.logger
.debug(_scope
, 'no such scope', { scope
});
650 this.logger
.debug(_scope
, 'deleted', { scope
});
655 this.logger
.error(_scope
, 'failed', { error: e
, scope
});
661 scopeUpsert(dbCtx
, scope
, application
, description
, manuallyAdded
) {
662 const _scope
= _fileScope('scopeUpsert');
663 this.logger
.debug(_scope
, 'called', { scope
, application
, description
, manuallyAdded
});
666 const result
= this.statement
.scopeUpsert
.run({ scope
, application
, description
, manuallyAdded: DatabaseSQLite
._booleanToNumeric(manuallyAdded
) });
667 if (result
.changes
!= 1) {
668 throw new DBErrors
.UnexpectedResult('did not upsert scope');
671 this.logger
.error(_scope
, 'failed', { error: e
, scope
, application
, description
, manuallyAdded
});
677 tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
) {
678 const _scope
= _fileScope('tokenCleanup');
679 this.logger
.debug(_scope
, 'called', { codeLifespanSeconds
, atLeastMsSinceLast
});
681 const almanacEvent
= Enum
.AlmanacEntry
.TokenCleanup
;
683 return this.db
.transaction(() => {
685 // Check that enough time has passed since last cleanup
686 const nowEpoch
= BigInt(common
.dateToEpoch());
687 const { epoch: lastCleanupEpoch
} = this.statement
.almanacGet
.get({ event: almanacEvent
}) || { epoch: 0n
};
688 const elapsedMs
= (nowEpoch
- lastCleanupEpoch
) * 1000n
;
689 if (elapsedMs
< atLeastMsSinceLast
) {
690 this.logger
.debug(_scope
, 'skipping token cleanup, too soon', { lastCleanupEpoch
, elapsedMs
, atLeastMsSinceLast
});
695 const { changes: tokensRemoved
} = this.statement
.tokenCleanup
.run({ codeLifespanSeconds
});
697 // Update the last cleanup time
698 const result
= this.statement
.almanacUpsert
.run({ event: almanacEvent
, epoch: nowEpoch
});
699 if (result
.changes
!= 1) {
700 this.constructor._almanacErrorThrow();
703 this.logger
.debug(_scope
, 'finished', { tokensRemoved
, codeLifespanSeconds
, atLeastMsSinceLast
});
704 return tokensRemoved
;
707 this.logger
.error(_scope
, 'failed', { error: e
, codeLifespanSeconds
, atLeastMsSinceLast
});
713 static _tokenToNative(token
) {
715 token
.created
= new Date(Number(token
.created
) * 1000);
716 if (token
.expires
|| token
.expires
== 0) {
717 token
.expires
= new Date(Number(token
.expires
) * 1000);
719 if (token
.refreshExpires
|| token
.refreshExpires
== 0) {
720 token
.refreshExpires
= new Date(Number(token
.refreshExpires
) * 1000);
722 if (token
.refreshed
|| token
.refreshed
== 0) {
723 token
.refreshed
= new Date(Number(token
.refreshed
) * 1000);
725 token
.isRevoked
= !!token
.isRevoked
;
726 token
.isToken
= !!token
.isToken
;
727 if (token
.profileData
) {
728 token
.profileData
= JSON
.parse(token
.profileData
);
735 tokenGetByCodeId(dbCtx
, codeId
) {
736 const _scope
= _fileScope('tokenGetByCodeId');
737 this.logger
.debug(_scope
, 'called', { codeId
});
740 return this.transaction(dbCtx
, () => {
741 const token
= this.statement
.tokenGetByCodeId
.get({ codeId
});
742 token
.scopes
= (this.statement
.tokenScopesGetByCodeId
.all({ codeId
}) || [])
743 .map((row
) => row
.scope
);
744 return DatabaseSQLite
._tokenToNative(token
);
747 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
753 tokenRefreshRevokeByCodeId(dbCtx
, codeId
) {
754 const _scope
= _fileScope('tokenRefreshRevokeByCodeId');
755 this.logger
.debug(_scope
, 'called', { codeId
});
758 const result
= this.statement
.tokenRefreshRevokeByCodeId
.run({ codeId
});
759 if (result
.changes
!= 1) {
760 throw new DBErrors
.UnexpectedResult('did not revoke refresh');
763 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
769 tokenRevokeByCodeId(dbCtx
, codeId
) {
770 const _scope
= _fileScope('tokenRevokeByCodeId');
771 this.logger
.debug(_scope
, 'called', { codeId
});
774 const result
= this.statement
.tokenRevokeByCodeId
.run({ codeId
});
775 if (result
.changes
!= 1) {
776 throw new DBErrors
.UnexpectedResult('did not revoke token');
779 this.logger
.error(_scope
, 'failed', { error: e
, codeId
});
785 tokensGetByIdentifier(dbCtx
, identifier
) {
786 const _scope
= _fileScope('tokensGetByIdentifier');
787 this.logger
.debug(_scope
, 'called', { identifier
});
790 const tokens
= this.statement
.tokensGetByIdentifier
.all({ identifier
});
791 return tokens
.map(DatabaseSQLite
._tokenToNative
);
793 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
799 ticketRedeemed(dbCtx
, redeemedData
) {
800 const _scope
= _fileScope('ticketRedeemed');
801 this.logger
.debug(_scope
, 'called', { ...redeemedData
});
804 const result
= this.statement
.ticketRedeemed
.run(redeemedData
);
805 if (result
.changes
!= 1) {
806 throw new DBErrors
.UnexpectedResult('did not store redeemed ticket');
809 this.logger
.error(_scope
, 'failed', { error: e
});
815 ticketTokenPublished(dbCtx
, redeemedData
) {
816 const _scope
= _fileScope('ticketRedeemed');
817 this.logger
.debug(_scope
, 'called', { ...redeemedData
});
819 const almanacEvent
= Enum
.AlmanacEntry
.TicketPublished
;
821 const result
= this.statement
.ticketTokenPublished
.run(redeemedData
);
822 if (result
.changes
!= 1) {
823 throw new DBErrors
.UnexpectedResult('did not store redeemed ticket');
825 const epoch
= common
.dateToEpoch();
826 const almanacResult
= this.statement
.almanacUpsert
.run({ event: almanacEvent
, epoch
});
827 if (almanacResult
.changes
!= 1) {
828 this.constructor._almanacErrorThrow();
832 this.logger
.error(_scope
, 'failed', { error: e
});
837 static _redeemedTicketToNative(redeemedTicket
) {
838 redeemedTicket
.created
= new Date(Number(redeemedTicket
.created
) * 1000);
839 if (redeemedTicket
.published
) {
840 redeemedTicket
.published
= new Date(Number(redeemedTicket
.published
) * 1000);
842 return redeemedTicket
;
845 ticketTokenGetUnpublished() {
846 const _scope
= _fileScope('ticketTokenGetUnpublished');
847 this.logger
.debug(_scope
, 'called');
850 const unpublished
= this.statement
.ticketTokenGetUnpublished
.all();
851 return unpublished
.map((x
) => DatabaseSQLite
._redeemedTicketToNative(x
));
853 this.logger
.error(_scope
, 'failed', { error: e
});
860 module
.exports
= DatabaseSQLite
;