3 const common
= require('../../common');
4 const Database
= require('../base');
5 const DBErrors
= require('../errors');
6 const svh
= require('../schema-version-helper');
7 const SQLite
= require('better-sqlite3');
8 const fs
= require('fs');
9 const path
= require('path');
10 const { performance
} = require('perf_hooks');
12 const _fileScope
= common
.fileScope(__filename
);
14 const schemaVersionsSupported
= {
27 // max of signed int64 (2^63 - 1), should be enough
28 const EPOCH_FOREVER
= BigInt('9223372036854775807');
30 class DatabaseSQLite
extends Database
{
31 constructor(logger
, options
) {
32 super(logger
, options
);
34 const connectionString
= options
.db
.connectionString
|| 'sqlite://:memory:';
35 const csDelim
= '://';
36 const dbFilename
= connectionString
.slice(connectionString
.indexOf(csDelim
) + csDelim
.length
);
38 const queryLogLevel
= options
.db
.queryLogLevel
;
40 const sqliteOptions
= {
41 ...(queryLogLevel
&& {
42 // eslint-disable-next-line security/detect-object-injection
43 verbose: (query
) => this.logger
[queryLogLevel
](_fileScope('SQLite:verbose'), '', { query
}),
46 this.db
= new SQLite(dbFilename
, sqliteOptions
);
47 this.schemaVersionsSupported
= schemaVersionsSupported
;
48 this.changesSinceLastOptimize
= BigInt(0);
49 this.optimizeAfterChanges
= options
.db
.connectionString
.optimizeAfterChanges
;
50 this.db
.pragma('foreign_keys = on'); // Enforce consistency.
51 this.db
.pragma('journal_mode = WAL'); // Be faster, expect local filesystem.
52 this.db
.defaultSafeIntegers(true); // This probably isn't necessary, but by using these BigInts we keep weird floats out of the query logs.
55 this._initStatements();
60 * SQLite cannot prepare its statements without a schema, ensure such exists.
63 const _scope
= _fileScope('_initTables');
65 // Migrations rely upon this table, ensure it exists.
66 const metaVersionTable
= '_meta_schema_version';
67 const tableExists
= this.db
.prepare('SELECT name FROM sqlite_master WHERE type=:type AND name=:name').pluck(true).bind({ type: 'table', name: metaVersionTable
});
68 let metaExists
= tableExists
.get();
69 if (metaExists
=== undefined) {
70 const fPath
= path
.join(__dirname
, 'sql', 'schema', 'init.sql');
71 // eslint-disable-next-line security/detect-non-literal-fs-filename
72 const fSql
= fs
.readFileSync(fPath
, { encoding: 'utf8' });
74 metaExists
= tableExists
.get();
75 /* istanbul ignore if */
76 if (metaExists
=== undefined) {
77 throw new DBErrors
.UnexpectedResult(`did not create ${metaVersionTable} table`);
79 this.logger
.info(_scope
, 'created schema version table', { metaVersionTable
});
83 const currentSchema
= this._currentSchema();
84 const migrationsWanted
= svh
.unappliedSchemaVersions(__dirname
, currentSchema
, this.schemaVersionsSupported
);
85 this.logger
.debug(_scope
, 'schema migrations wanted', { migrationsWanted
});
86 migrationsWanted
.forEach((v
) => {
87 const fPath
= path
.join(__dirname
, 'sql', 'schema', v
, 'apply.sql');
88 // eslint-disable-next-line security/detect-non-literal-fs-filename
89 const fSql
= fs
.readFileSync(fPath
, { encoding: 'utf8' });
90 this.logger
.info(_scope
, 'applying migration', { version: v
});
97 const _scope
= _fileScope('_initStatements');
98 const sqlDir
= path
.join(__dirname
, 'sql');
101 // Decorate the statement calls we use with timing and logging.
102 const wrapFetch
= (logName
, statementName
, fn
) => {
103 const _wrapScope
= _fileScope(logName
);
104 return (...args
) => {
105 const startTimestampMs
= performance
.now();
106 const rows
= fn(...args
);
107 DatabaseSQLite
._deOphidiate(rows
);
108 const elapsedTimeMs
= performance
.now() - startTimestampMs
;
109 this.logger
.debug(_wrapScope
, 'complete', { statementName
, elapsedTimeMs
});
113 const wrapRun
= (logName
, statementName
, fn
) => {
114 const _wrapScope
= _fileScope(logName
);
115 return (...args
) => {
116 const startTimestampMs
= performance
.now();
117 const result
= fn(...args
);
118 const elapsedTimeMs
= performance
.now() - startTimestampMs
;
119 this.logger
.debug(_wrapScope
, 'complete', { ...result
, statementName
, elapsedTimeMs
});
120 result
.duration
= elapsedTimeMs
;
125 // eslint-disable-next-line security/detect-non-literal-fs-filename
126 for (const f
of fs
.readdirSync(sqlDir
)) {
127 const fPath
= path
.join(sqlDir
, f
);
128 const { name: fName
, ext: fExt
} = path
.parse(f
);
129 // eslint-disable-next-line security/detect-non-literal-fs-filename
130 const stat
= fs
.statSync(fPath
);
132 || fExt
.toLowerCase() !== '.sql') {
135 // eslint-disable-next-line security/detect-non-literal-fs-filename
136 const fSql
= fs
.readFileSync(fPath
, { encoding: 'utf8' });
137 const statementName
= Database
._camelfy(fName
.toLowerCase(), '-');
140 statement
= this.db
.prepare(fSql
);
142 /* istanbul ignore next */
143 this.logger
.error(_scope
, 'failed to prepare statement', { error: e
, file: f
});
144 /* istanbul ignore next */
147 // eslint-disable-next-line security/detect-object-injection
148 this.statement
[statementName
] = statement
;
149 const { get: origGet
, all: origAll
, run: origRun
} = statement
;
150 statement
.get = wrapFetch('SQLite:get', statementName
, origGet
.bind(statement
));
151 statement
.all
= wrapFetch('SQLite:all', statementName
, origAll
.bind(statement
));
152 statement
.run
= wrapRun('SQLite:run', statementName
, origRun
.bind(statement
));
154 this.statement
._optimize
= this.db
.prepare('SELECT * FROM pragma_optimize(0x03)');
156 this.logger
.debug(_scope
, 'statements initialized', { statements: Object
.keys(this.statement
).length
});
160 static _deOphidiate(rows
) {
161 const rowsIsArray
= Array
.isArray(rows
);
165 const exemplaryRow
= rows
[0];
166 for (const prop
in exemplaryRow
) {
167 const camel
= Database
._camelfy(prop
);
168 if (!(camel
in exemplaryRow
)) {
169 for (const d
of rows
) {
170 // eslint-disable-next-line security/detect-object-injection
172 // eslint-disable-next-line security/detect-object-injection
177 return rowsIsArray
? rows : rows
[0];
182 return this.db
.prepare('SELECT major, minor, patch FROM _meta_schema_version ORDER BY major DESC, minor DESC, patch DESC LIMIT 1').get();
187 const _scope
= _fileScope('healthCheck');
188 this.logger
.debug(_scope
, 'called', {});
190 throw new DBErrors
.UnexpectedResult('database is not open');
192 return { open: this.db
.open
};
196 _engineInfo(result
) {
197 if (result
.changes
) {
198 this.changesSinceLastOptimize
+= BigInt(result
.changes
);
202 changes: Number(result
.changes
),
203 lastInsertRowid: result
.lastInsertRowid
,
214 const _scope
= _fileScope('_optimize');
216 if (this.optimizeAfterChanges
217 && this.changesSinceLastOptimize
>= this.optimizeAfterChanges
) {
218 const optimize
= this.statement
._optimize
.all();
219 this.logger
.debug(_scope
, 'optimize', { optimize
});
220 this.db
.pragma('optimize');
221 this.changesSinceLastOptimize
= BigInt(0);
226 _purgeTables(really
) {
230 'topic_fetch_in_progress',
232 'verification_in_progress',
234 'subscription_delivery_in_progress',
236 const result
= this.db
.prepare(`DELETE FROM ${table}`).run();
237 this.logger
.debug(_fileScope('_purgeTables'), 'success', { table
, result
});
248 transaction(dbCtx
, fn
) {
249 dbCtx
= dbCtx
|| this.db
;
250 return dbCtx
.transaction(fn
)();
254 authenticationSuccess(dbCtx
, identifier
) {
255 const _scope
= _fileScope('authenticationSuccess');
256 this.logger
.debug(_scope
, 'called', { identifier
});
260 result
= this.statement
.authenticationSuccess
.run({ identifier
});
261 if (result
.changes
!= 1) {
262 throw new DBErrors
.UnexpectedResult('did not update authentication success');
265 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
271 authenticationGet(dbCtx
, identifier
) {
272 const _scope
= _fileScope('authenticationGet');
273 this.logger
.debug(_scope
, 'called', { identifier
});
276 return this.statement
.authenticationGet
.get({ identifier
});
278 this.logger
.error(_scope
, 'failed', { error: e
, identifier
});
284 authenticationUpsert(dbCtx
, identifier
, credential
) {
285 const _scope
= _fileScope('authenticationUpsert');
286 const scrubbedCredential
= '*'.repeat((credential
|| '').length
);
287 this.logger
.debug(_scope
, 'called', { identifier
, scrubbedCredential
});
291 result
= this.statement
.authenticationUpsert
.run({ identifier
, credential
});
292 if (result
.changes
!= 1) {
293 throw new DBErrors
.UnexpectedResult('did not upsert authentication');
296 this.logger
.error(_scope
, 'failed', { error: e
, identifier
, scrubbedCredential
})
303 * Converts engine subscription fields to native types.
304 * @param {Object} data
306 static _subscriptionDataToNative(data
) {
307 const epochToDate
= (epoch
) => new Date(Number(epoch
) * 1000);
309 ['created', 'verified', 'expires', 'contentDelivered'].forEach((field
) => {
310 // eslint-disable-next-line security/detect-object-injection
311 data
[field
] = epochToDate(data
[field
]);
318 subscriptionsByTopicId(dbCtx
, topicId
) {
319 const _scope
= _fileScope('subscriptionsByTopicId');
320 this.logger
.debug(_scope
, 'called', { topicId
});
323 const subscriptions
= this.statement
.subscriptionsByTopicId
.all({ topicId
});
324 return subscriptions
.map((s
) => DatabaseSQLite
._subscriptionDataToNative(s
));
326 this.logger
.error(_scope
, 'failed', { error: e
, topicId
});
332 subscriptionCountByTopicUrl(dbCtx
, topicUrl
) {
333 const _scope
= _fileScope('subscriptionCountByTopicUrl');
334 this.logger
.debug(_scope
, 'called', { topicUrl
});
337 return this.statement
.subscriptionCountByTopicUrl
.get({ topicUrl
});
339 this.logger
.error(_scope
, 'failed', { error: e
, topicUrl
});
345 subscriptionDelete(dbCtx
, callback
, topicId
) {
346 const _scope
= _fileScope('subscriptionDelete');
347 this.logger
.debug(_scope
, 'called', { callback
, topicId
});
350 const result
= this.statement
.subscriptionDelete
.run({ callback
, topicId
});
351 if (result
.changes
!= 1) {
352 throw new DBErrors
.UnexpectedResult('did not delete subscription');
354 return this._engineInfo(result
);
356 this.logger
.error(_scope
, 'failed', { error: e
, callback
, topicId
});
362 subscriptionDeliveryClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
) {
363 const _scope
= _fileScope('subscriptionDeliveryClaim');
364 this.logger
.debug(_scope
, 'called', { wanted
, claimTimeoutSeconds
, claimant
});
368 this.db
.transaction(() => {
369 subscriptionIds
= this.statement
.subscriptionDeliveryNeeded
.all({ wanted
}).map((claim
) => claim
.id
);
370 subscriptionIds
.forEach((subscriptionId
) => {
371 const result
= this.statement
.subscriptionDeliveryClaimById
.run({ subscriptionId
, claimTimeoutSeconds
, claimant
});
372 if (result
.changes
!= 1) {
373 throw new DBErrors
.UnexpectedResult('did not claim subscription delivery');
377 return subscriptionIds
;
379 this.logger
.error(_scope
, 'failed', { error: e
, wanted
, claimTimeoutSeconds
, claimant
, subscriptionIds
});
385 subscriptionDeliveryClaimById(dbCtx
, subscriptionId
, claimTimeoutSeconds
, claimant
) {
386 const _scope
= _fileScope('subscriptionDeliveryClaimById');
387 this.logger
.debug(_scope
, 'called', { subscriptionId
, claimTimeoutSeconds
, claimant
});
390 const result
= this.statement
.subscriptionDeliveryClaimById
.run({ subscriptionId
, claimTimeoutSeconds
, claimant
});
391 if (result
.changes
!= 1) {
392 throw new DBErrors
.UnexpectedResult('did not claim subscription delivery');
394 return this._engineInfo(result
);
396 this.logger
.error(_scope
, 'failed', { error: e
, subscriptionId
, claimTimeoutSeconds
, claimant
});
402 subscriptionDeliveryComplete(dbCtx
, callback
, topicId
) {
403 const _scope
= _fileScope('subscriptionDeliveryComplete');
404 this.logger
.debug(_scope
, 'called', { callback
, topicId
});
408 this.db
.transaction(() => {
409 result
= this.statement
.subscriptionDeliverySuccess
.run({ callback
, topicId
});
410 if (result
.changes
!= 1) {
411 throw new DBErrors
.UnexpectedResult('did not set subscription delivery success');
413 result
= this.statement
.subscriptionDeliveryDone
.run({ callback
, topicId
});
414 if (result
.changes
!= 1) {
415 throw new DBErrors
.UnexpectedResult('did not complete subscription delivery');
418 return this._engineInfo(result
);
420 this.logger
.error(_scope
, 'failed', { error: e
, callback
, topicId
});
426 subscriptionDeliveryGone(dbCtx
, callback
, topicId
) {
427 const _scope
= _fileScope('subscriptionDeliveryGone');
428 this.logger
.debug(_scope
, 'called', { callback
, topicId
});
432 this.db
.transaction(() => {
433 result
= this.statement
.subscriptionDelete
.run({ callback
, topicId
});
434 if (result
.changes
!= 1) {
435 throw new DBErrors
.UnexpectedResult('did not delete subscription');
437 // Delete cascades to delivery
438 // result = this.statement.subscriptionDeliveryDone.run({ callback, topicId });
439 // if (result.changes != 1) {
440 // throw new DBErrors.UnexpectedResult('did not complete subscription delivery');
443 return this._engineInfo(result
);
445 this.logger
.error(_scope
, 'failed', { error: e
, callback
, topicId
});
451 subscriptionDeliveryIncomplete(dbCtx
, callback
, topicId
, retryDelays
= [60]) {
452 const _scope
= _fileScope('subscriptionDeliveryIncomplete');
453 this.logger
.debug(_scope
, 'called', { callback
, topicId
, retryDelays
});
457 this.db
.transaction(() => {
458 const { currentAttempt
} = this.statement
.subscriptionDeliveryAttempts
.get({ callback
, topicId
});
459 const nextAttemptDelaySeconds
= common
.attemptRetrySeconds(currentAttempt
, retryDelays
);
460 result
= this.statement
.subscriptionDeliveryFailure
.run({ nextAttemptDelaySeconds
, callback
, topicId
});
461 if (result
.changes
!= 1) {
462 throw new DBErrors
.UnexpectedResult('did not set delivery failure');
464 result
= this.statement
.subscriptionDeliveryDone
.run({ callback
, topicId
});
465 if (result
.changes
!= 1) {
466 throw new DBErrors
.UnexpectedResult('did not complete subscription delivery');
470 this.logger
.error(_scope
, 'failed', { error: e
, callback
, topicId
});
476 subscriptionGet(dbCtx
, callback
, topicId
) {
477 const _scope
= _fileScope('subscriptionGet');
478 this.logger
.debug(_scope
, 'called', { callback
, topicId
});
482 subscription
= this.statement
.subscriptionGet
.get({ callback
, topicId
});
483 return DatabaseSQLite
._subscriptionDataToNative(subscription
);
485 this.logger
.error(_scope
, 'failed', { error: e
, callback
, topicId
});
491 subscriptionGetById(dbCtx
, subscriptionId
) {
492 const _scope
= _fileScope('subscriptionGetById');
493 this.logger
.debug(_scope
, 'called', { subscriptionId
});
497 subscription
= this.statement
.subscriptionGetById
.get({ subscriptionId
});
498 return DatabaseSQLite
._subscriptionDataToNative(subscription
);
500 this.logger
.error(_scope
, 'failed', { error: e
, subscriptionId
});
506 subscriptionUpdate(dbCtx
, data
) {
507 const _scope
= _fileScope('subscriptionUpdate');
508 this.logger
.debug(_scope
, 'called', { data
});
510 const subscriptionData
= {
514 this._subscriptionUpdateDataValidate(subscriptionData
);
517 const result
= this.statement
.subscriptionUpdate
.run(subscriptionData
);
518 if (result
.changes
!= 1) {
519 throw new DBErrors
.UnexpectedResult('did not update subscription');
522 this.logger
.error(_scope
, 'failed', { error: e
, subscriptionData
});
528 subscriptionUpsert(dbCtx
, data
) {
529 const _scope
= _fileScope('subscriptionUpsert');
530 this.logger
.debug(_scope
, 'called', { ...data
});
532 const subscriptionData
= {
534 httpRemoteAddr: null,
538 this._subscriptionUpsertDataValidate(subscriptionData
);
542 result
= this.statement
.subscriptionUpsert
.run(subscriptionData
);
543 if (result
.changes
!= 1) {
544 throw new DBErrors
.UnexpectedResult('did not upsert subscription');
546 return this._engineInfo(result
);
548 this.logger
.error(_scope
, 'failed', { error: e
, subscriptionData
});
554 topicDeleted(dbCtx
, topicId
) {
555 const _scope
= _fileScope('topicDeleted');
556 this.logger
.debug(_scope
, 'called', { topicId
});
560 result
= this.statement
.topicDeleted
.run({ topicId
});
561 if (result
.changes
!= 1) {
562 throw new DBErrors
.UnexpectedResult('did not update topic as deleted');
565 this.logger
.error(_scope
, 'failed', { error: e
, topicId
});
571 topicFetchClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
) {
572 const _scope
= _fileScope('topicFetchClaim');
573 this.logger
.debug(_scope
, 'called', { wanted
, claimTimeoutSeconds
});
577 this.db
.transaction(() => {
578 topicIds
= this.statement
.topicContentFetchNeeded
.all({ wanted
}).map((claim
) => claim
.id
);
579 topicIds
.forEach((topicId
) => {
580 const result
= this.statement
.topicContentFetchClaimById
.run({ topicId
, claimTimeoutSeconds
, claimant
});
581 if (result
.changes
!= 1) {
582 throw new DBErrors
.UnexpectedResult('did not claim topic fetch');
588 this.logger
.error(_scope
, 'failed to claim topics for fetch', { error: e
, wanted
, claimTimeoutSeconds
, claimant
, topicIds
});
594 topicFetchClaimById(dbCtx
, topicId
, claimTimeoutSeconds
, claimant
) {
595 const _scope
= _fileScope('topicFetchClaimById');
596 this.logger
.debug(_scope
, 'called', { topicId
, claimTimeoutSeconds
, claimant
});
600 result
= this.statement
.topicContentFetchClaimById
.run({ topicId
, claimTimeoutSeconds
, claimant
});
601 if (result
.changes
!= 1) {
602 throw new DBErrors
.UnexpectedResult('did not claim topic fetch');
604 return this._engineInfo(result
);
606 this.logger
.error(_scope
, 'failed to claim topics for fetch', { error: e
, topicId
, claimTimeoutSeconds
, claimant
});
612 topicFetchComplete(dbCtx
, topicId
) {
613 const _scope
= _fileScope('topicFetchComplete');
614 this.logger
.debug(_scope
, 'called', { topicId
});
618 this.db
.transaction(() => {
619 result
= this.statement
.topicAttemptsReset
.run({ topicId
, forever: EPOCH_FOREVER
});
620 if (result
.changes
!= 1) {
621 throw new DBErrors
.UnexpectedResult('did not reset topic attempts');
623 result
= this.statement
.topicContentFetchDone
.run({ topicId
});
624 if (result
.changes
!= 1) {
625 throw new DBErrors
.UnexpectedResult('did not release topic fetch');
628 return this._engineInfo(result
);
630 this.logger
.error(_scope
, 'failed', { error: e
, result
, topicId
});
636 topicFetchIncomplete(dbCtx
, topicId
, retryDelays
= [60]) {
637 const _scope
= _fileScope('topicFetchIncomplete');
638 this.logger
.debug(_scope
, 'called', { topicId
});
642 this.db
.transaction(() => {
643 const { contentFetchAttemptsSinceSuccess: currentAttempt
} = this.statement
.topicAttempts
.get({ topicId
});
644 const nextAttemptDelaySeconds
= common
.attemptRetrySeconds(currentAttempt
, retryDelays
);
645 result
= this.statement
.topicAttemptsIncrement
.run({ topicId
, nextAttemptDelaySeconds
});
646 if (result
.changes
!= 1) {
647 throw new DBErrors
.UnexpectedResult('did not set topic attempts');
649 result
= this.statement
.topicContentFetchDone
.run({ topicId
});
650 if (result
.changes
!= 1) {
651 throw new DBErrors
.UnexpectedResult('did not release topic fetch');
655 return this._engineInfo(result
);
657 this.logger
.error(_scope
, 'failed', { error: e
, result
, topicId
});
663 topicFetchRequested(dbCtx
, topicId
) {
664 const _scope
= _fileScope('topicFetchRequested');
665 this.logger
.debug(_scope
, 'called', { topicId
});
669 result
= this.statement
.topicContentFetchRequested
.run({ topicId
});
670 if (result
.changes
!= 1) {
671 throw new DBErrors
.UnexpectedResult('did not set topic fetch requested');
673 return this._engineInfo(result
);
675 this.logger
.error(_scope
, 'failed', { error: e
, topicId
});
682 * Converts engine topic fields to native types.
683 * @param {Object} data
685 static _topicDataToNative(data
) {
686 const epochToDate
= (epoch
) => new Date(Number(epoch
) * 1000);
688 data
.isActive
= !!data
.isActive
;
689 data
.isDeleted
= !!data
.isDeleted
;
690 ['created', 'lastPublish', 'contentFetchNextAttempt', 'contentUpdated'].forEach((field
) => {
691 // eslint-disable-next-line security/detect-object-injection
692 data
[field
] = epochToDate(data
[field
]);
699 // eslint-disable-next-line no-unused-vars
701 const _scope
= _fileScope('topicGetAll');
702 this.logger
.debug(_scope
, 'called');
706 topics
= this.statement
.topicGetInfoAll
.all();
708 this.logger
.error(_scope
, 'failed', { error: e
, topics
});
713 .map(DatabaseSQLite
._topicDataToNative
)
714 .map(this._topicDefaults
.bind(this));
720 topicGetById(dbCtx
, topicId
, applyDefaults
= true) {
721 const _scope
= _fileScope('topicGetById');
722 this.logger
.debug(_scope
, 'called', { topicId
});
726 topic
= this.statement
.topicGetById
.get({ topicId
});
727 DatabaseSQLite
._topicDataToNative(topic
);
729 topic
= this._topicDefaults(topic
);
733 this.logger
.error(_scope
, 'failed', { error: e
, topic
, topicId
});
739 topicGetByUrl(dbCtx
, topicUrl
) {
740 const _scope
= _fileScope('topicGetByUrl');
741 this.logger
.debug(_scope
, 'called', { topicUrl
});
745 topic
= this.statement
.topicGetByUrl
.get({ topicUrl
});
746 DatabaseSQLite
._topicDataToNative(topic
);
747 return this._topicDefaults(topic
);
749 this.logger
.error(_scope
, 'failed', { error: e
, topic
, topicUrl
});
755 topicGetContentById(dbCtx
, topicId
) {
756 const _scope
= _fileScope('topicGetContentById');
757 this.logger
.debug(_scope
, 'called', { topicId
});
761 topic
= this.statement
.topicGetContentById
.get({ topicId
});
762 DatabaseSQLite
._topicDataToNative(topic
);
763 return this._topicDefaults(topic
);
765 this.logger
.error(_scope
, 'failed', { error: e
, topic
, topicId
});
771 topicSet(dbCtx
, data
) {
772 const _scope
= _fileScope('topicSet');
773 this.logger
.debug(_scope
, 'called', data
);
775 const topicSetData
= {
776 publisherValidationUrl: null,
777 leaseSecondsPreferred: null,
778 leaseSecondsMin: null,
779 leaseSecondsMax: null,
785 this._topicSetDataValidate(topicSetData
);
786 result
= this.statement
.topicUpsert
.run(topicSetData
);
787 if (result
.changes
!= 1) {
788 throw new DBErrors
.UnexpectedResult('did not set topic data');
790 return this._engineInfo(result
);
792 this.logger
.error(_scope
, 'failed', { error: e
, result
});
798 topicSetContent(dbCtx
, data
) {
799 const _scope
= _fileScope('topicSetContent');
800 const topicSetContentData
= {
805 ...topicSetContentData
,
806 content: common
.logTruncate(topicSetContentData
.content
, 100),
808 this.logger
.debug(_scope
, 'called', logData
);
812 this._topicSetContentDataValidate(topicSetContentData
);
813 result
= this.statement
.topicSetContent
.run(topicSetContentData
);
814 logData
.result
= result
;
815 if (result
.changes
!= 1) {
816 throw new DBErrors
.UnexpectedResult('did not set topic content');
818 return this._engineInfo(result
);
820 this.logger
.error(_scope
, 'failed', { error: e
, ...logData
});
826 topicUpdate(dbCtx
, data
) {
827 const _scope
= _fileScope('topicUpdate');
828 this.logger
.debug(_scope
, 'called', { data
});
831 leaseSecondsPreferred: null,
832 leaseSecondsMin: null,
833 leaseSecondsMax: null,
834 publisherValidationUrl: null,
838 this._topicUpdateDataValidate(topicData
);
841 const result
= this.statement
.topicUpdate
.run(topicData
);
842 if (result
.changes
!= 1) {
843 throw new DBErrors
.UnexpectedResult('did not update topic');
846 this.logger
.error(_scope
, 'failed', { error: e
, topicData
});
852 verificationClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
) {
853 const _scope
= _fileScope('verificationClaim');
854 this.logger
.debug(_scope
, 'called', { wanted
, claimTimeoutSeconds
});
858 this.db
.transaction(() => {
859 verificationIds
= this.statement
.verificationNeeded
.all({ wanted
}).map((claim
) => claim
.id
);
860 verificationIds
.forEach((verificationId
) => {
861 const result
= this.statement
.verificationClaimById
.run({ verificationId
, claimTimeoutSeconds
, claimant
});
862 if (result
.changes
!= 1) {
863 throw new DBErrors
.UnexpectedResult('did not claim verification');
867 return verificationIds
;
869 this.logger
.error(_scope
, 'failed to claim verifications', { wanted
, claimTimeoutSeconds
});
875 verificationClaimById(dbCtx
, verificationId
, claimTimeoutSeconds
, claimant
) {
876 const _scope
= _fileScope('verificationClaimById');
877 this.logger
.debug(_scope
, 'called', { verificationId
, claimTimeoutSeconds
, claimant
});
881 result
= this.statement
.verificationClaimById
.run({ verificationId
, claimTimeoutSeconds
, claimant
});
882 if (result
.changes
!= 1) {
883 throw new DBErrors
.UnexpectedResult('did not claim verification');
885 return this._engineInfo(result
);
887 this.logger
.error(_scope
, 'failed to claim verification', { error: e
, verificationId
, claimTimeoutSeconds
, claimant
});
893 verificationComplete(dbCtx
, verificationId
, callback
, topicId
) {
894 const _scope
= _fileScope('verificationComplete');
895 this.logger
.debug(_scope
, 'called', { verificationId
});
899 this.db
.transaction(() => {
900 result
= this.statement
.verificationScrub
.run({ verificationId
, callback
, topicId
});
901 if (result
.changes
< 1) {
902 throw new DBErrors
.UnexpectedResult('did not remove verifications');
906 this.logger
.error(_scope
, 'failed', { verificationId
});
909 return this._engineInfo(result
);
914 * Converts engine verification fields to native types.
915 * @param {Object} data
917 static _verificationDataToNative(data
) {
919 data
.isPublisherValidated
= !!data
.isPublisherValidated
;
924 verificationGetById(dbCtx
, verificationId
) {
925 const _scope
= _fileScope('verificationGetById');
926 this.logger
.debug(_scope
, 'called', { verificationId
});
930 verification
= this.statement
.verificationGetById
.get({ verificationId
});
931 DatabaseSQLite
._verificationDataToNative(verification
);
934 this.logger
.error(_scope
, 'failed', { error: e
, verificationId
});
940 verificationIncomplete(dbCtx
, verificationId
, retryDelays
= [60]) {
941 const _scope
= _fileScope('verificationIncomplete');
942 this.logger
.debug(_scope
, 'called', { verificationId
});
946 this.db
.transaction(() => {
947 const { attempts: currentAttempt
} = this.statement
.verificationAttempts
.get({ verificationId
});
948 const nextAttemptDelaySeconds
= common
.attemptRetrySeconds(currentAttempt
, retryDelays
);
949 result
= this.statement
.verificationAttemptsIncrement
.run({ verificationId
, nextAttemptDelaySeconds
});
950 if (result
.changes
!= 1) {
951 throw new DBErrors
.UnexpectedResult('did not increment verification attempts');
953 result
= this.statement
.verificationDone
.run({ verificationId
});
954 if (result
.changes
!= 1) {
955 throw new DBErrors
.UnexpectedResult('did not release verification in progress');
959 return this._engineInfo(result
);
961 this.logger
.error(_scope
, 'failed', { error: e
, result
, verificationId
});
968 * Convert native verification fields to engine types.
970 static _verificationDataToEngine(data
) {
972 data
.isPublisherValidated
= data
.isPublisherValidated
? 1 : 0;
977 verificationInsert(dbCtx
, verification
) {
978 const _scope
= _fileScope('verificationInsert');
979 this.logger
.debug(_scope
, 'called', { verification
});
981 const verificationData
= {
983 httpRemoteAddr: null,
989 let result
, verificationId
;
991 this._verificationDataValidate(verificationData
);
992 DatabaseSQLite
._verificationDataToEngine(verificationData
);
993 result
= this.statement
.verificationInsert
.run(verificationData
);
994 if (result
.changes
!= 1) {
995 throw new DBErrors
.UnexpectedResult('did not insert verification');
997 verificationId
= result
.lastInsertRowid
;
998 this.logger
.debug(_scope
, 'inserted verification', { verificationId
});
1000 return verificationId
;
1002 this.logger
.error(_scope
, 'failed', { error: e
, verificationData
});
1008 verificationRelease(dbCtx
, verificationId
) {
1009 const _scope
= _fileScope('verificationRelease');
1010 this.logger
.debug(_scope
, 'called', { verificationId
});
1014 result
= this.statement
.verificationDone
.run({ verificationId
});
1015 if (result
.changes
!= 1) {
1016 throw new DBErrors
.UnexpectedResult('did not release verification');
1019 this.logger
.error(_scope
, 'failed', { error: e
, verificationId
});
1025 verificationUpdate(dbCtx
, verificationId
, data
) {
1026 const _scope
= _fileScope('verificationUpdate');
1027 this.logger
.debug(_scope
, 'called', { verificationId
, data
});
1029 const verificationData
= {
1037 this._verificationUpdateDataValidate(verificationData
);
1038 DatabaseSQLite
._verificationDataToEngine(verificationData
);
1039 result
= this.statement
.verificationUpdate
.run(verificationData
);
1040 if (result
.changes
!= 1) {
1041 throw new DBErrors
.UnexpectedResult('did not update verification');
1044 this.logger
.error(_scope
, 'failed', { error: e
, verificationData
});
1050 verificationValidated(dbCtx
, verificationId
) {
1051 const _scope
= _fileScope('verificationValidated');
1052 this.logger
.debug(_scope
, 'called', { verificationId
});
1056 result
= this.statement
.verificationValidate
.run({ verificationId
});
1057 if (result
.changes
!= 1) {
1058 throw new DBErrors
.UnexpectedResult('did not set verification validation');
1061 this.logger
.error(_scope
, 'failed', { error: e
, verificationId
});
1069 module
.exports
= DatabaseSQLite
;