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
})
302 subscriptionsByTopicId(dbCtx
, topicId
) {
303 const _scope
= _fileScope('subscriptionsByTopicId');
304 this.logger
.debug(_scope
, 'called', { topicId
});
307 return this.statement
.subscriptionsByTopicId
.all({ topicId
});
309 this.logger
.error(_scope
, 'failed', { error: e
, topicId
});
315 subscriptionCountByTopicUrl(dbCtx
, topicUrl
) {
316 const _scope
= _fileScope('subscriptionCountByTopicUrl');
317 this.logger
.debug(_scope
, 'called', { topicUrl
});
320 return this.statement
.subscriptionCountByTopicUrl
.get({ topicUrl
});
322 this.logger
.error(_scope
, 'failed', { error: e
, topicUrl
});
328 subscriptionDelete(dbCtx
, callback
, topicId
) {
329 const _scope
= _fileScope('subscriptionDelete');
330 this.logger
.debug(_scope
, 'called', { callback
, topicId
});
333 const result
= this.statement
.subscriptionDelete
.run({ callback
, topicId
});
334 if (result
.changes
!= 1) {
335 throw new DBErrors
.UnexpectedResult('did not delete subscription');
337 return this._engineInfo(result
);
339 this.logger
.error(_scope
, 'failed', { error: e
, callback
, topicId
});
345 subscriptionDeliveryClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
) {
346 const _scope
= _fileScope('subscriptionDeliveryClaim');
347 this.logger
.debug(_scope
, 'called', { wanted
, claimTimeoutSeconds
, claimant
});
351 this.db
.transaction(() => {
352 subscriptionIds
= this.statement
.subscriptionDeliveryNeeded
.all({ wanted
}).map((claim
) => claim
.id
);
353 subscriptionIds
.forEach((subscriptionId
) => {
354 const result
= this.statement
.subscriptionDeliveryClaimById
.run({ subscriptionId
, claimTimeoutSeconds
, claimant
});
355 if (result
.changes
!= 1) {
356 throw new DBErrors
.UnexpectedResult('did not claim subscription delivery');
360 return subscriptionIds
;
362 this.logger
.error(_scope
, 'failed', { error: e
, wanted
, claimTimeoutSeconds
, claimant
, subscriptionIds
});
368 subscriptionDeliveryClaimById(dbCtx
, subscriptionId
, claimTimeoutSeconds
, claimant
) {
369 const _scope
= _fileScope('subscriptionDeliveryClaimById');
370 this.logger
.debug(_scope
, 'called', { subscriptionId
, claimTimeoutSeconds
, claimant
});
373 const result
= this.statement
.subscriptionDeliveryClaimById
.run({ subscriptionId
, claimTimeoutSeconds
, claimant
});
374 if (result
.changes
!= 1) {
375 throw new DBErrors
.UnexpectedResult('did not claim subscription delivery');
377 return this._engineInfo(result
);
379 this.logger
.error(_scope
, 'failed', { error: e
, subscriptionId
, claimTimeoutSeconds
, claimant
});
385 subscriptionDeliveryComplete(dbCtx
, callback
, topicId
) {
386 const _scope
= _fileScope('subscriptionDeliveryComplete');
387 this.logger
.debug(_scope
, 'called', { callback
, topicId
});
391 this.db
.transaction(() => {
392 result
= this.statement
.subscriptionDeliverySuccess
.run({ callback
, topicId
});
393 if (result
.changes
!= 1) {
394 throw new DBErrors
.UnexpectedResult('did not set subscription delivery success');
396 result
= this.statement
.subscriptionDeliveryDone
.run({ callback
, topicId
});
397 if (result
.changes
!= 1) {
398 throw new DBErrors
.UnexpectedResult('did not complete subscription delivery');
401 return this._engineInfo(result
);
403 this.logger
.error(_scope
, 'failed', { error: e
, callback
, topicId
});
409 subscriptionDeliveryGone(dbCtx
, callback
, topicId
) {
410 const _scope
= _fileScope('subscriptionDeliveryGone');
411 this.logger
.debug(_scope
, 'called', { callback
, topicId
});
415 this.db
.transaction(() => {
416 result
= this.statement
.subscriptionDelete
.run({ callback
, topicId
});
417 if (result
.changes
!= 1) {
418 throw new DBErrors
.UnexpectedResult('did not delete subscription');
420 // Delete cascades to delivery
421 // result = this.statement.subscriptionDeliveryDone.run({ callback, topicId });
422 // if (result.changes != 1) {
423 // throw new DBErrors.UnexpectedResult('did not complete subscription delivery');
426 return this._engineInfo(result
);
428 this.logger
.error(_scope
, 'failed', { error: e
, callback
, topicId
});
434 subscriptionDeliveryIncomplete(dbCtx
, callback
, topicId
, retryDelays
= [60]) {
435 const _scope
= _fileScope('subscriptionDeliveryIncomplete');
436 this.logger
.debug(_scope
, 'called', { callback
, topicId
, retryDelays
});
440 this.db
.transaction(() => {
441 const { currentAttempt
} = this.statement
.subscriptionDeliveryAttempts
.get({ callback
, topicId
});
442 const nextAttemptDelaySeconds
= common
.attemptRetrySeconds(currentAttempt
, retryDelays
);
443 result
= this.statement
.subscriptionDeliveryFailure
.run({ nextAttemptDelaySeconds
, callback
, topicId
});
444 if (result
.changes
!= 1) {
445 throw new DBErrors
.UnexpectedResult('did not set delivery failure');
447 result
= this.statement
.subscriptionDeliveryDone
.run({ callback
, topicId
});
448 if (result
.changes
!= 1) {
449 throw new DBErrors
.UnexpectedResult('did not complete subscription delivery');
453 this.logger
.error(_scope
, 'failed', { error: e
, callback
, topicId
});
459 subscriptionGet(dbCtx
, callback
, topicId
) {
460 const _scope
= _fileScope('subscriptionGet');
461 this.logger
.debug(_scope
, 'called', { callback
, topicId
});
465 subscription
= this.statement
.subscriptionGet
.get({ callback
, topicId
});
468 this.logger
.error(_scope
, 'failed', { error: e
, callback
, topicId
});
474 subscriptionGetById(dbCtx
, subscriptionId
) {
475 const _scope
= _fileScope('subscriptionGetById');
476 this.logger
.debug(_scope
, 'called', { subscriptionId
});
480 subscription
= this.statement
.subscriptionGetById
.get({ subscriptionId
});
483 this.logger
.error(_scope
, 'failed', { error: e
, subscriptionId
});
489 subscriptionUpdate(dbCtx
, data
) {
490 const _scope
= _fileScope('subscriptionUpdate');
491 this.logger
.debug(_scope
, 'called', { data
});
493 const subscriptionData
= {
497 this._subscriptionUpdateDataValidate(subscriptionData
);
500 const result
= this.statement
.subscriptionUpdate
.run(subscriptionData
);
501 if (result
.changes
!= 1) {
502 throw new DBErrors
.UnexpectedResult('did not update subscription');
505 this.logger
.error(_scope
, 'failed', { error: e
, subscriptionData
});
511 subscriptionUpsert(dbCtx
, data
) {
512 const _scope
= _fileScope('subscriptionUpsert');
513 this.logger
.debug(_scope
, 'called', { ...data
});
515 const subscriptionData
= {
517 httpRemoteAddr: null,
521 this._subscriptionUpsertDataValidate(subscriptionData
);
525 result
= this.statement
.subscriptionUpsert
.run(subscriptionData
);
526 if (result
.changes
!= 1) {
527 throw new DBErrors
.UnexpectedResult('did not upsert subscription');
529 return this._engineInfo(result
);
531 this.logger
.error(_scope
, 'failed', { error: e
, subscriptionData
});
537 topicDeleted(dbCtx
, topicId
) {
538 const _scope
= _fileScope('topicDeleted');
539 this.logger
.debug(_scope
, 'called', { topicId
});
543 result
= this.statement
.topicDeleted
.run({ topicId
});
544 if (result
.changes
!= 1) {
545 throw new DBErrors
.UnexpectedResult('did not update topic as deleted');
548 this.logger
.error(_scope
, 'failed', { error: e
, topicId
});
554 topicFetchClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
) {
555 const _scope
= _fileScope('topicFetchClaim');
556 this.logger
.debug(_scope
, 'called', { wanted
, claimTimeoutSeconds
});
560 this.db
.transaction(() => {
561 topicIds
= this.statement
.topicContentFetchNeeded
.all({ wanted
}).map((claim
) => claim
.id
);
562 topicIds
.forEach((topicId
) => {
563 const result
= this.statement
.topicContentFetchClaimById
.run({ topicId
, claimTimeoutSeconds
, claimant
});
564 if (result
.changes
!= 1) {
565 throw new DBErrors
.UnexpectedResult('did not claim topic fetch');
571 this.logger
.error(_scope
, 'failed to claim topics for fetch', { error: e
, wanted
, claimTimeoutSeconds
, claimant
, topicIds
});
577 topicFetchClaimById(dbCtx
, topicId
, claimTimeoutSeconds
, claimant
) {
578 const _scope
= _fileScope('topicFetchClaimById');
579 this.logger
.debug(_scope
, 'called', { topicId
, claimTimeoutSeconds
, claimant
});
583 result
= this.statement
.topicContentFetchClaimById
.run({ topicId
, claimTimeoutSeconds
, claimant
});
584 if (result
.changes
!= 1) {
585 throw new DBErrors
.UnexpectedResult('did not claim topic fetch');
587 return this._engineInfo(result
);
589 this.logger
.error(_scope
, 'failed to claim topics for fetch', { error: e
, topicId
, claimTimeoutSeconds
, claimant
});
595 topicFetchComplete(dbCtx
, topicId
) {
596 const _scope
= _fileScope('topicFetchComplete');
597 this.logger
.debug(_scope
, 'called', { topicId
});
601 this.db
.transaction(() => {
602 result
= this.statement
.topicAttemptsReset
.run({ topicId
, forever: EPOCH_FOREVER
});
603 if (result
.changes
!= 1) {
604 throw new DBErrors
.UnexpectedResult('did not reset topic attempts');
606 result
= this.statement
.topicContentFetchDone
.run({ topicId
});
607 if (result
.changes
!= 1) {
608 throw new DBErrors
.UnexpectedResult('did not release topic fetch');
611 return this._engineInfo(result
);
613 this.logger
.error(_scope
, 'failed', { error: e
, result
, topicId
});
619 topicFetchIncomplete(dbCtx
, topicId
, retryDelays
= [60]) {
620 const _scope
= _fileScope('topicFetchIncomplete');
621 this.logger
.debug(_scope
, 'called', { topicId
});
625 this.db
.transaction(() => {
626 const { contentFetchAttemptsSinceSuccess: currentAttempt
} = this.statement
.topicAttempts
.get({ topicId
});
627 const nextAttemptDelaySeconds
= common
.attemptRetrySeconds(currentAttempt
, retryDelays
);
628 result
= this.statement
.topicAttemptsIncrement
.run({ topicId
, nextAttemptDelaySeconds
});
629 if (result
.changes
!= 1) {
630 throw new DBErrors
.UnexpectedResult('did not set topic attempts');
632 result
= this.statement
.topicContentFetchDone
.run({ topicId
});
633 if (result
.changes
!= 1) {
634 throw new DBErrors
.UnexpectedResult('did not release topic fetch');
638 return this._engineInfo(result
);
640 this.logger
.error(_scope
, 'failed', { error: e
, result
, topicId
});
646 topicFetchRequested(dbCtx
, topicId
) {
647 const _scope
= _fileScope('topicFetchRequested');
648 this.logger
.debug(_scope
, 'called', { topicId
});
652 result
= this.statement
.topicContentFetchRequested
.run({ topicId
});
653 if (result
.changes
!= 1) {
654 throw new DBErrors
.UnexpectedResult('did not set topic fetch requested');
656 return this._engineInfo(result
);
658 this.logger
.error(_scope
, 'failed', { error: e
, topicId
});
665 * Converts engine topic fields to native types.
666 * @param {Object} data
668 static _topicDataToNative(data
) {
669 const epochToDate
= (epoch
) => new Date(Number(epoch
) * 1000);
671 data
.isActive
= !!data
.isActive
;
672 data
.isDeleted
= !!data
.isDeleted
;
673 ['created', 'lastPublish', 'contentFetchNextAttempt', 'contentUpdated'].forEach((field
) => {
674 // eslint-disable-next-line security/detect-object-injection
675 data
[field
] = epochToDate(data
[field
]);
682 // eslint-disable-next-line no-unused-vars
684 const _scope
= _fileScope('topicGetAll');
685 this.logger
.debug(_scope
, 'called');
689 topics
= this.statement
.topicGetInfoAll
.all();
691 this.logger
.error(_scope
, 'failed', { error: e
, topics
});
696 .map(DatabaseSQLite
._topicDataToNative
)
697 .map(this._topicDefaults
.bind(this));
703 topicGetById(dbCtx
, topicId
, applyDefaults
= true) {
704 const _scope
= _fileScope('topicGetById');
705 this.logger
.debug(_scope
, 'called', { topicId
});
709 topic
= this.statement
.topicGetById
.get({ topicId
});
710 DatabaseSQLite
._topicDataToNative(topic
);
712 topic
= this._topicDefaults(topic
);
716 this.logger
.error(_scope
, 'failed', { error: e
, topic
, topicId
});
722 topicGetByUrl(dbCtx
, topicUrl
) {
723 const _scope
= _fileScope('topicGetByUrl');
724 this.logger
.debug(_scope
, 'called', { topicUrl
});
728 topic
= this.statement
.topicGetByUrl
.get({ topicUrl
});
729 DatabaseSQLite
._topicDataToNative(topic
);
730 return this._topicDefaults(topic
);
732 this.logger
.error(_scope
, 'failed', { error: e
, topic
, topicUrl
});
738 topicGetContentById(dbCtx
, topicId
) {
739 const _scope
= _fileScope('topicGetContentById');
740 this.logger
.debug(_scope
, 'called', { topicId
});
744 topic
= this.statement
.topicGetContentById
.get({ topicId
});
745 DatabaseSQLite
._topicDataToNative(topic
);
746 return this._topicDefaults(topic
);
748 this.logger
.error(_scope
, 'failed', { error: e
, topic
, topicId
});
754 topicSet(dbCtx
, data
) {
755 const _scope
= _fileScope('topicSet');
756 this.logger
.debug(_scope
, 'called', data
);
758 const topicSetData
= {
759 publisherValidationUrl: null,
760 leaseSecondsPreferred: null,
761 leaseSecondsMin: null,
762 leaseSecondsMax: null,
768 this._topicSetDataValidate(topicSetData
);
769 result
= this.statement
.topicUpsert
.run(topicSetData
);
770 if (result
.changes
!= 1) {
771 throw new DBErrors
.UnexpectedResult('did not set topic data');
773 return this._engineInfo(result
);
775 this.logger
.error(_scope
, 'failed', { error: e
, result
});
781 topicSetContent(dbCtx
, data
) {
782 const _scope
= _fileScope('topicSetContent');
783 const topicSetContentData
= {
788 ...topicSetContentData
,
789 content: common
.logTruncate(topicSetContentData
.content
, 100),
791 this.logger
.debug(_scope
, 'called', logData
);
795 this._topicSetContentDataValidate(topicSetContentData
);
796 result
= this.statement
.topicSetContent
.run(topicSetContentData
);
797 logData
.result
= result
;
798 if (result
.changes
!= 1) {
799 throw new DBErrors
.UnexpectedResult('did not set topic content');
801 return this._engineInfo(result
);
803 this.logger
.error(_scope
, 'failed', { error: e
, ...logData
});
809 topicUpdate(dbCtx
, data
) {
810 const _scope
= _fileScope('topicUpdate');
811 this.logger
.debug(_scope
, 'called', { data
});
814 leaseSecondsPreferred: null,
815 leaseSecondsMin: null,
816 leaseSecondsMax: null,
817 publisherValidationUrl: null,
821 this._topicUpdateDataValidate(topicData
);
824 const result
= this.statement
.topicUpdate
.run(topicData
);
825 if (result
.changes
!= 1) {
826 throw new DBErrors
.UnexpectedResult('did not update topic');
829 this.logger
.error(_scope
, 'failed', { error: e
, topicData
});
835 verificationClaim(dbCtx
, wanted
, claimTimeoutSeconds
, claimant
) {
836 const _scope
= _fileScope('verificationClaim');
837 this.logger
.debug(_scope
, 'called', { wanted
, claimTimeoutSeconds
});
841 this.db
.transaction(() => {
842 verificationIds
= this.statement
.verificationNeeded
.all({ wanted
}).map((claim
) => claim
.id
);
843 verificationIds
.forEach((verificationId
) => {
844 const result
= this.statement
.verificationClaimById
.run({ verificationId
, claimTimeoutSeconds
, claimant
});
845 if (result
.changes
!= 1) {
846 throw new DBErrors
.UnexpectedResult('did not claim verification');
850 return verificationIds
;
852 this.logger
.error(_scope
, 'failed to claim verifications', { wanted
, claimTimeoutSeconds
});
858 verificationClaimById(dbCtx
, verificationId
, claimTimeoutSeconds
, claimant
) {
859 const _scope
= _fileScope('verificationClaimById');
860 this.logger
.debug(_scope
, 'called', { verificationId
, claimTimeoutSeconds
, claimant
});
864 result
= this.statement
.verificationClaimById
.run({ verificationId
, claimTimeoutSeconds
, claimant
});
865 if (result
.changes
!= 1) {
866 throw new DBErrors
.UnexpectedResult('did not claim verification');
868 return this._engineInfo(result
);
870 this.logger
.error(_scope
, 'failed to claim verification', { error: e
, verificationId
, claimTimeoutSeconds
, claimant
});
876 verificationComplete(dbCtx
, verificationId
, callback
, topicId
) {
877 const _scope
= _fileScope('verificationComplete');
878 this.logger
.debug(_scope
, 'called', { verificationId
});
882 this.db
.transaction(() => {
883 result
= this.statement
.verificationScrub
.run({ verificationId
, callback
, topicId
});
884 if (result
.changes
< 1) {
885 throw new DBErrors
.UnexpectedResult('did not remove verifications');
889 this.logger
.error(_scope
, 'failed', { verificationId
});
892 return this._engineInfo(result
);
897 * Converts engine verification fields to native types.
898 * @param {Object} data
900 static _verificationDataToNative(data
) {
902 data
.isPublisherValidated
= !!data
.isPublisherValidated
;
907 verificationGetById(dbCtx
, verificationId
) {
908 const _scope
= _fileScope('verificationGetById');
909 this.logger
.debug(_scope
, 'called', { verificationId
});
913 verification
= this.statement
.verificationGetById
.get({ verificationId
});
914 DatabaseSQLite
._verificationDataToNative(verification
);
917 this.logger
.error(_scope
, 'failed', { error: e
, verificationId
});
923 verificationIncomplete(dbCtx
, verificationId
, retryDelays
= [60]) {
924 const _scope
= _fileScope('verificationIncomplete');
925 this.logger
.debug(_scope
, 'called', { verificationId
});
929 this.db
.transaction(() => {
930 const { attempts: currentAttempt
} = this.statement
.verificationAttempts
.get({ verificationId
});
931 const nextAttemptDelaySeconds
= common
.attemptRetrySeconds(currentAttempt
, retryDelays
);
932 result
= this.statement
.verificationAttemptsIncrement
.run({ verificationId
, nextAttemptDelaySeconds
});
933 if (result
.changes
!= 1) {
934 throw new DBErrors
.UnexpectedResult('did not increment verification attempts');
936 result
= this.statement
.verificationDone
.run({ verificationId
});
937 if (result
.changes
!= 1) {
938 throw new DBErrors
.UnexpectedResult('did not release verification in progress');
942 return this._engineInfo(result
);
944 this.logger
.error(_scope
, 'failed', { error: e
, result
, verificationId
});
951 * Convert native verification fields to engine types.
953 static _verificationDataToEngine(data
) {
955 data
.isPublisherValidated
= data
.isPublisherValidated
? 1 : 0;
960 verificationInsert(dbCtx
, verification
) {
961 const _scope
= _fileScope('verificationInsert');
962 this.logger
.debug(_scope
, 'called', { verification
});
964 const verificationData
= {
966 httpRemoteAddr: null,
972 let result
, verificationId
;
974 this._verificationDataValidate(verificationData
);
975 DatabaseSQLite
._verificationDataToEngine(verificationData
);
976 result
= this.statement
.verificationInsert
.run(verificationData
);
977 if (result
.changes
!= 1) {
978 throw new DBErrors
.UnexpectedResult('did not insert verification');
980 verificationId
= result
.lastInsertRowid
;
981 this.logger
.debug(_scope
, 'inserted verification', { verificationId
});
983 return verificationId
;
985 this.logger
.error(_scope
, 'failed', { error: e
, verificationData
});
991 verificationRelease(dbCtx
, verificationId
) {
992 const _scope
= _fileScope('verificationRelease');
993 this.logger
.debug(_scope
, 'called', { verificationId
});
997 result
= this.statement
.verificationDone
.run({ verificationId
});
998 if (result
.changes
!= 1) {
999 throw new DBErrors
.UnexpectedResult('did not release verification');
1002 this.logger
.error(_scope
, 'failed', { error: e
, verificationId
});
1008 verificationUpdate(dbCtx
, verificationId
, data
) {
1009 const _scope
= _fileScope('verificationUpdate');
1010 this.logger
.debug(_scope
, 'called', { verificationId
, data
});
1012 const verificationData
= {
1020 this._verificationUpdateDataValidate(verificationData
);
1021 DatabaseSQLite
._verificationDataToEngine(verificationData
);
1022 result
= this.statement
.verificationUpdate
.run(verificationData
);
1023 if (result
.changes
!= 1) {
1024 throw new DBErrors
.UnexpectedResult('did not update verification');
1027 this.logger
.error(_scope
, 'failed', { error: e
, verificationData
});
1033 verificationValidated(dbCtx
, verificationId
) {
1034 const _scope
= _fileScope('verificationValidated');
1035 this.logger
.debug(_scope
, 'called', { verificationId
});
1039 result
= this.statement
.verificationValidate
.run({ verificationId
});
1040 if (result
.changes
!= 1) {
1041 throw new DBErrors
.UnexpectedResult('did not set verification validation');
1044 this.logger
.error(_scope
, 'failed', { error: e
, verificationId
});
1052 module
.exports
= DatabaseSQLite
;