const PostgresCreator = require('./lib/postgres-creator');
const SQLiteCreator = require('./lib/sqlite-creator');
const { validate } = require('./lib/validation');
-const { stubCreator, stubPgp } = require('./test/stub');
+const { interfaceMethods, stubCreator, stubPgp } = require('./test/stub');
const { itChecksImplementation } = require('./test/helpers');
module.exports = {
validate,
test: {
itChecksImplementation,
+ interfaceMethods,
stubCreator,
stubPgp,
},
const { fileScope } = require('@squeep/log-helper');
const DatabaseErrors = require('./errors');
-const { schemaVersionObjectToNumber } = require('./schema-version-helper');
+const { schemaVersionObjectCmp } = require('./schema-version-helper');
const _fileScope = fileScope(__filename);
* @property {number} minor semver minor
* @property {number} patch semver patch
*/
+
+ /**
+ * @returns {SchemaVersionObject} minimum supported schema version
+ */
+ // eslint-disable-next-line class-methods-use-this
+ get schemaVersionMin() {
+ return {
+ major: 0,
+ minor: 0,
+ patch: 0,
+ };
+ }
+
+ /**
+ * @returns {SchemaVersionObject} maximum supported schema version
+ */
+ // eslint-disable-next-line class-methods-use-this, sonarjs/no-identical-functions
+ get schemaVersionMax() {
+ return {
+ major: 0,
+ minor: 0,
+ patch: 0,
+ };
+ }
+
/**
* @typedef {object} VersionRange
* @property {SchemaVersionObject} min schema minimum supported
* @returns {VersionRange} schema version range supported
*/
get schemaVersionsSupported() {
- return this._notImplemented('schemaVersionsSupported', arguments);
+ return {
+ min: this.schemaVersionMin,
+ max: this.schemaVersionMax,
+ };
}
const _scope = _fileScope('initialize');
this.currentSchema = await this._currentSchema();
- const current = schemaVersionObjectToNumber(this.currentSchema);
- const min = schemaVersionObjectToNumber(this.schemaVersionsSupported.min);
- const max = schemaVersionObjectToNumber(this.schemaVersionsSupported.max);
- if (current >= min && current <= max) {
- this.logger.debug(_scope, 'schema supported', { currentSchema: this.currentSchema, schemaVersionsSupported: this.schemaVersionsSupported });
+ // Special case of version 0.0.0 interpreted as no installed schema.
+ if (!this.currentSchema || schemaVersionObjectCmp(this.currentSchema, { major: 0, minor: 0, patch: 0 }) == 0) {
+ this.logger.debug(_scope, 'no installed schema', { currentSchema: this.currentSchema, schemaVersionsSupported: this.schemaVersionsSupported });
+ return;
+ }
+ // Installed db schema must satisfy min >= db >= max, otherwise bail.
+ // Reasoning: we would expect engine implementation to have performed all migrations, so if db is outside implemented range, something is confused.
+ const currentSatisfiesMinimum = schemaVersionObjectCmp(this.currentSchema, this.schemaVersionsSupported.min) >= 0;
+ const currentSatisfiesMaxium = schemaVersionObjectCmp(this.currentSchema, this.schemaVersionsSupported.max) <= 0;
+ if (currentSatisfiesMinimum && currentSatisfiesMaxium) {
+ this.logger.debug(_scope, 'installed schema is supported', { currentSchema: this.currentSchema, schemaVersionsSupported: this.schemaVersionsSupported });
} else {
- this.logger.error(_scope, 'schema not supported', { currentSchema: this.currentSchema, schemaVersionsSupported: this.schemaVersionsSupported });
+ this.logger.error(_scope, 'installed schema not supported', { currentSchema: this.currentSchema, schemaVersionsSupported: this.schemaVersionsSupported });
throw new DatabaseErrors.MigrationNeeded();
}
}
/**
* @returns {string[]} supported engines for connection string
*/
- get supportedProtocols() { // eslint-disable-line class-methods-use-this
+ static get supportedProtocols() {
return [
'postgresql',
'sqlite',
* @returns {string} engine path
*/
enginePath(enginePathPrefix, protocol) {
- if (!this.supportedProtocols.includes(protocol)) {
+ if (!this.constructor.supportedProtocols.includes(protocol)) {
throw new UnsupportedEngine(protocol);
}
return [enginePathPrefix, protocol].join(path.sep);
};
- // eslint-disable-next-line class-methods-use-this
- get schemaVersionsSupported() {
- return {
- min: {
- major: 0,
- minor: 0,
- patch: 0,
- },
- max: {
- major: 0,
- minor: 0,
- patch: 0,
- },
- };
- }
-
-
/**
* @typedef {object} ConsoleLike
* @property {Function} error log error
/**
* @typedef {object} SchemaVersionObject
- * @property {number} major semver major
- * @property {number} minor semver minor
- * @property {number} patch semver patch
+ * @property {number|bigint} major semver major
+ * @property {number|bigint} minor semver minor
+ * @property {number|bigint} patch semver patch
*/
/**
- * Split a dotted version string into parts.
- * @param {string} v version string
- * @returns {SchemaVersionObject} version object
+ * Integer-type-agnostic comparison helper.
+ * @param {number|bigint} a a
+ * @param {number|bigint} b b
+ * @returns {number} sort value
*/
-function schemaVersionStringToObject(v) {
- const [ major, minor, patch ] = v.split('.', 3).map((x) => parseInt(x, 10));
- return { major, minor, patch };
+function _intCmp(a, b) {
+ const diff = BigInt(a) - BigInt(b);
+ if (diff > 0n) {
+ return 1;
+ } else if (diff < 0n) {
+ return -1;
+ }
+ return 0;
}
/**
- * Render a version object numerically.
- * Version components must be <1000.
- * @param {SchemaVersionObject} v version object
- * @returns {number} numeric version
+ * Compare two version objects, returns numeric value suitable for sorting.
+ * @param {SchemaVersionObject} a version object
+ * @param {SchemaVersionObject} b version object
+ * @returns {number} version difference, 0 if a=b, 1 if a>b, -1 if a<b
*/
-function schemaVersionObjectToNumber(v) {
- if (!v) {
- return 0;
+function schemaVersionObjectCmp(a, b) {
+ const diffMajor = _intCmp(a.major, b.major);
+ if (diffMajor) {
+ return diffMajor;
}
- const vScale = 1000;
- if (v.major >= vScale || v.minor >= vScale || v.patch >= vScale) {
- throw new RangeError('version component too large');
+ const diffMinor = _intCmp(a.minor, b.minor);
+ if (diffMinor) {
+ return diffMinor;
}
- return parseInt(v.major) * vScale * vScale + parseInt(v.minor) * vScale + parseInt(v.patch);
+ return _intCmp(a.patch, b.patch);
}
/**
- * Convert dotted version string into number.
+ * Split a dotted version string into parts.
* @param {string} v version string
- * @returns {number} numeric version
+ * @returns {SchemaVersionObject} version object
*/
-function schemaVersionStringToNumber(v) {
- return schemaVersionObjectToNumber(schemaVersionStringToObject(v));
+function schemaVersionStringToObject(v) {
+ const [ major, minor, patch ] = v.split('.', 3).map((x) => parseInt(x, 10));
+ return { major, minor, patch };
}
* @returns {number} version difference
*/
function schemaVersionStringCmp(a, b) {
- return schemaVersionStringToNumber(a) - schemaVersionStringToNumber(b);
+ return schemaVersionObjectCmp(schemaVersionStringToObject(a), schemaVersionStringToObject(b));
}
* @returns {string[]} list of un-applied migration versions
*/
function unappliedSchemaVersions(engineDir, current, supported) {
- const min = schemaVersionObjectToNumber(supported.min);
- const max = schemaVersionObjectToNumber(supported.max);
- const cur = schemaVersionObjectToNumber(current);
+ const { min, max } = supported;
const available = allSchemaVersions(engineDir);
return available.filter((a) => {
- a = schemaVersionStringToNumber(a);
- return a >= min && a <= max && a > cur;
+ a = schemaVersionStringToObject(a);
+ const satisfiesMinimum = schemaVersionObjectCmp(a, min) >= 0n;
+ const satisfiesMaximum = schemaVersionObjectCmp(a, max) <= 0n;
+ const isNeeded = (!current) || schemaVersionObjectCmp(a, current) > 0n;
+ return satisfiesMinimum && satisfiesMaximum && isNeeded;
});
}
module.exports = {
+ schemaVersionObjectCmp,
schemaVersionStringToObject,
- schemaVersionObjectToNumber,
- schemaVersionStringToNumber,
schemaVersionStringCmp,
isSchemaMigrationDirectory,
allSchemaVersions,
const SQLiteCreator = (Abstract) => {
class DatabaseSQLite extends Abstract {
- // eslint-disable-next-line class-methods-use-this
- get schemaVersionsSupported() {
- return {
- min: {
- major: 0,
- minor: 0,
- patch: 0,
- },
- max: {
- major: 0,
- minor: 0,
- patch: 0,
- },
- };
- }
/**
* @typedef {object} ConsoleLike
}
- context(fn) {
+ async context(fn) {
return fn(this.db);
}
- transaction(dbCtx, fn) {
+ async transaction(dbCtx, fn) {
dbCtx = dbCtx || this.db;
return dbCtx.transaction(fn).immediate();
}
+/* eslint-disable no-unused-vars */
'use strict';
const { Abstract } = require('../');
}
}
+ // eslint-disable-next-line class-methods-use-this
+ get schemaVersionMin() {
+ return {
+ major: 1,
+ minor: 0,
+ patch: 0,
+ };
+ }
+
+ // eslint-disable-next-line class-methods-use-this, sonarjs/no-identical-functions
+ get schemaVersionMax() {
+ return {
+ major: 1,
+ minor: 0,
+ patch: 0,
+ };
+ }
+
almanacGetAll(dbCtx) {
this._notImplemented('almanacGetAll', arguments);
}
constructor(...args) {
super('.', ...args);
}
+ // eslint-disable-next-line class-methods-use-this
localRequire(enginePath) {
+ // eslint-disable-next-line security/detect-non-literal-require
return require(enginePath);
}
}
*/
class PostgresDB extends PostgresCreator(Abstract) {
- // eslint-disable-next-line class-methods-use-this
- get schemaVersionsSupported() {
- return {
- min: {
- major: 0,
- minor: 0,
- patch: 0,
- },
- max: {
- major: 1,
- minor: 0,
- patch: 0,
- },
- };
- }
-
constructor(...args) {
super(...args);
if (!this._isProduction) {
assert.deepStrictEqual(result, expected);
}
- async almanacGetAll(dbCtx) { // eslint-disable-line no-unused-vars
+ async almanacGetAll(dbCtx) {
const _scope = 'almanacGetAll';
this.logger.debug(_scope, 'called');
try {
*/
class SQLiteDB extends SQLiteCreator(Abstract) {
- // eslint-disable-next-line class-methods-use-this
- get schemaVersionsSupported() {
- return {
- min: {
- major: 0,
- minor: 0,
- patch: 0,
- },
- max: {
- major: 1,
- minor: 0,
- patch: 0,
- },
- };
- }
-
constructor(...args) {
super(...args);
if (!this._isProduction) {
// eslint-disable-next-line class-methods-use-this
_engineSpecificTests() {
- // none
+ assert(true);
}
almanacGetAll(dbCtx) { // eslint-disable-line no-unused-vars
'use strict';
const assert = require('node:assert');
-const sinon = require('sinon');
const DBErrors = require('../lib/errors');
const nop = () => undefined;
-class StubLogger {
- constructor(backend) {
- const logger = backend || (process.env['VERBOSE_TESTS'] && console) || StubLogger._nopLogger;
- for (const level of StubLogger._levels) {
- this[level] = logger[level]; // eslint-disable-line security/detect-object-injection
- }
- }
-
- static get _levels() {
- return ['error', 'warn', 'info', 'log', 'debug'];
- }
-
- static get _nopLogger() {
- return Object.fromEntries(StubLogger._levels.map((level) => [level, nop]));
- }
-
- _spy() {
- StubLogger._levels.forEach((level) => sinon.spy(this, level));
- }
-}
-
-const stubPgp = () => {
- const stub = {
- result: () => ({ rows: [] }),
- all: nop,
- get: nop,
- run: nop,
- one: nop,
- manyOrNone: nop,
- oneOrNone: nop,
- query: nop,
- batch: nop,
- multiResult: nop,
- connect: nop,
- };
- stub.tx = (fn) => fn(stub);
- stub.txIf = (fn) => fn(stub);
- stub.task = (fn) => fn(stub);
- return stub;
-};
-stubPgp.pg = {
- types: {
- getTypeParser: nop,
- setTypeParser: nop,
- },
-};
-stubPgp.QueryFile = class {};
-stubPgp.utils = {
- enumSql: () => ({}),
-};
-stubPgp.end = nop;
-
-
/**
* Mocha function to validate all interface methods are implemented.
* @param {*} db db instance
module.exports = {
nop,
- StubLogger,
- stubPgp,
itChecksImplementation,
};
*/
const assert = require('node:assert');
+const sinon = require('sinon');
const { step } = require('mocha-steps');
-const { StubLogger } = require('./helpers');
+const { StubLogger } = require('@squeep/test-helper');
describe('Database Integration', function () {
const implementations = [];
before(async function () {
this.timeout(10 * 1000); // Allow some time for creating tables et cetera.
- logger = new StubLogger();
- logger._spy();
+ logger = new StubLogger(sinon);
// eslint-disable-next-line security/detect-non-literal-require
DB = require(i.module);
db = new DB(logger, i.config);
const assert = require('node:assert');
const sinon = require('sinon');
-const { StubLogger } = require('../helpers');
+const { StubLogger } = require('@squeep/test-helper');
const Abstract = require('../../lib/abstract');
const { MigrationNeeded, NotImplemented } = require('../../lib/errors');
options = {
db: {},
};
- stubLogger = new StubLogger();
- stubLogger._spy();
+ stubLogger = new StubLogger(sinon);
db = new Abstract(stubLogger, options);
});
+ describe('schemaVersionsSupported', function () {
+ it('default', function () {
+ const result = db.schemaVersionsSupported;
+ assert.deepStrictEqual(result, {
+ min: { major: 0, minor: 0, patch: 0 },
+ max: { major: 0, minor: 0, patch: 0 },
+ });
+ });
+ }); // schemaVersionsSupported
+
describe('initialize', function () {
let schemaVersionsSupported, currentSchema;
beforeEach(function () {
max: { major: 2, minor: 0, patch: 0 },
};
currentSchema = { major: 1, minor: 5, patch: 0 };
- sinon.stub(db, 'schemaVersionsSupported').get(() => schemaVersionsSupported);
+ sinon.stub(db, 'schemaVersionMin').get(() => schemaVersionsSupported.min);
+ sinon.stub(db, 'schemaVersionMax').get(() => schemaVersionsSupported.max);
sinon.stub(db, '_currentSchema').callsFake(() => Promise.resolve(currentSchema));
});
-
it('validates supported schema version', async function () {
await db.initialize();
assert.deepStrictEqual(db.currentSchema, currentSchema);
});
-
it('reports unsupported low schema version', async function () {
schemaVersionsSupported.min.major = 2;
assert.rejects(() => db.initialize(), MigrationNeeded);
});
-
it('reports unsupported high schema version', async function () {
schemaVersionsSupported.max.major = 1;
assert.rejects(() => db.initialize(), MigrationNeeded);
});
-
+ it('covers no current', async function () {
+ currentSchema = undefined;
+ await db.initialize();
+ });
}); // initialize
describe('_camelfy', function () {
});
}); // abstract methods
- describe('abstract getters', function () {
- const abstractGetters = [
- 'schemaVersionsSupported',
- ];
-
- it('need implementation', function () {
- for (const m of abstractGetters) {
- assert.throws(() => db[m], NotImplemented);
- }
- });
- }); // abstract getters
-
}); // Abstract
const Factory = require('../../lib/factory');
const { UnsupportedEngine } = require('../../lib/errors');
const pgp = require('pg-promise');
-
-const nop = () => undefined;
+const { StubLogger } = require('@squeep/test-helper');
describe('Factory', function () {
let logger, options, enginePathPrefix;
beforeEach(function () {
- logger = process.env['VERBOSE_TESTS'] ? console : { log: nop, debug: nop };
+ logger = new StubLogger(sinon);
options = {
db: {
connectionString: '',
const { interfaceMethods, stubPgp } = require('../stub');
const PostgresCreator = require('../../lib/postgres-creator');
const DBErrors = require('../../lib/errors');
-const { StubLogger, nop, itChecksImplementation } = require('../helpers');
+const { nop, itChecksImplementation } = require('../helpers');
+const { StubLogger } = require('@squeep/test-helper');
describe('Postgres Creator', function () {
let db, options, stubLogger;
noWarnings: true,
},
};
- stubLogger = new StubLogger();
- stubLogger._spy();
+ stubLogger = new StubLogger(sinon);
db = new DatabasePostgres(stubLogger, options, stubPgp);
});
});
}); // schemaVersionStringToObject
- describe('schemaVersionObjectToNumber', function () {
- it('covers', function () {
- const expected = 1002003;
- const result = svh.schemaVersionObjectToNumber({
- major: 1,
- minor: 2,
- patch: 3,
- });
- assert.strictEqual(result, expected);
- });
- it('has numeric limitations', function () {
- assert.throws(() => svh.schemaVersionObjectToNumber({
- major: 1000,
- minor: 1000,
- patch: 1000,
- }), RangeError);
- });
- it('returns zero for unknown', function () {
- const expected = 0;
- const result = svh.schemaVersionObjectToNumber();
- assert.strictEqual(result, expected);
- });
- }); // schemaVersionObjectToNumber
-
- describe('schemaVersionStringToNumber', function () {
- it('covers', function () {
- const expected = 1002003;
- const result = svh.schemaVersionStringToNumber('1.2.3');
- assert.strictEqual(result, expected);
- });
- }); // schemaVersionStringToNumber
-
describe('schemaVersionStringCmp', function () {
it('sorts', function () {
- const expected = ['0.0.0', '1.0.0', '1.5.3', '64.123.998', '64.123.999'];
- const source = ['1.5.3', '64.123.998', '1.0.0', '64.123.999', '0.0.0'];
+ const expected = ['0.0.0', '1.0.0', '1.5.3', '64.123.998', '64.123.999', '9999.9999.9999'];
+ const source = ['1.5.3', '64.123.998', '9999.9999.9999', '1.0.0', '64.123.999', '0.0.0'];
source.sort(svh.schemaVersionStringCmp);
assert.deepStrictEqual(source, expected);
});
it('covers', function () {
const expected = ['1.1.0', '1.1.1'];
fs.readdirSync.returns(['1.1.2', 'file.txt', '1.1.0', '1.1.1', 'init.sql', '1.0.1', '1.0.0']);
- // cannot seem to stub isSchemaMigration, so here are the internals of it stubbed
+ // cannot seem to stub isSchemaMigrationDirectory, so here are the internals of it stubbed
+ let i = 0;
+ fs.statSync
+ .onCall(i++).returns(isDir).onCall(i++).returns(isMig) // '1.1.2'
+ .onCall(i++).returns(notDir) // 'file.txt'
+ .onCall(i++).returns(isDir).onCall(i++).returns(isMig) // '1.1.0'
+ .onCall(i++).returns(isDir).onCall(i++).returns(isMig) // '1.1.1'
+ .onCall(i++).returns(notDir) // 'init.sql'
+ .onCall(i++).returns(isDir).onCall(i++).returns(isMig) // '1.0.1'
+ .onCall(i++).returns(isDir).onCall(i++).returns(isMig) // '1.0.0'
+ ;
+ const result = svh.unappliedSchemaVersions('path', current, supported);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers no current version', function () {
+ current = undefined;
+ const expected = ['1.0.1', '1.1.0', '1.1.1'];
+ fs.readdirSync.returns(['1.1.2', 'file.txt', '1.1.0', '1.1.1', 'init.sql', '1.0.1', '1.0.0']);
+ // cannot seem to stub isSchemaMigrationDirectory, so here are the internals of it stubbed
let i = 0;
fs.statSync
.onCall(i++).returns(isDir).onCall(i++).returns(isMig) // '1.1.2'
;
const result = svh.unappliedSchemaVersions('path', current, supported);
assert.deepStrictEqual(result, expected);
+
});
}); // unappliedSchemaVersions
const { interfaceMethods } = require('../stub');
const SQLiteCreator = require('../../lib/sqlite-creator');
const DBErrors = require('../../lib/errors');
-const { StubLogger, nop, itChecksImplementation } = require('../helpers');
+const { nop, itChecksImplementation } = require('../helpers');
+const { StubLogger } = require('@squeep/test-helper');
describe('DatabaseSQLite', function () {
let db, options, stubLogger;
queryLogLevel: 'debug',
},
};
- stubLogger = new StubLogger();
- stubLogger._spy();
+ stubLogger = new StubLogger(sinon);
db = new DatabaseSQLite(stubLogger, options);
sinon.stub(fs, 'readdirSync').returns([]);
sinon.stub(fs, 'statSync').returns({
constructor(...args) {
super(...args);
this._stubMetadata.interfaceMethods = interfaceMethods(Abstract, metadata);
- this._stubMetadata.spyMethods.forEach((m) => sinon.spy(this, m));
- this._sinonReset();
+ this._sinonInit();
}
async context(fn) {
await fn(dbCtx);
}
- _sinonReset() {
+ _sinonInit() {
this._stubMetadata.abstractGetters
.forEach((m) => sinon.stub(this, m).get())
;
.forEach((m) => sinon.stub(this, m))
;
this._stubMetadata.spyMethods
- .forEach((m) => this[m].resetHistory()) // eslint-disable-line security/detect-object-injection
+ .forEach((m) => sinon.spy(this, m))
;
}
}