UNPKG

mgdb-migrator

Version:
252 lines 9.07 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Migrator = void 0; const node_assert_1 = __importDefault(require("node:assert")); const mongodb_1 = require("mongodb"); class Migrator { migratorKey = 'control'; defaultMigration = { down: (_db) => Promise.reject(`Can't go down from default`), name: 'default', up: (_db) => Promise.resolve(), version: 0, }; list; collection; db; options; constructor(opts) { this.list = [this.defaultMigration]; this.options = opts ? opts : ({ collectionName: 'migrations', db: null, log: true, logIfLatest: true, logger: null, }); } async config(opts) { this.options = Object.assign({}, this.options, opts); if (!this.options.logger && this.options.log) { this.options.logger = (level, ...args) => console.log(level, ...args); } if (this.options.log === false) { this.options.logger = (_level, ..._args) => { return; }; } let db = this.options.db || this.db; if (!db) { throw new ReferenceError('db option must be defined'); } if (db.connectionUrl) { const dbProps = db; const options = { ...dbProps.options }; const client = await mongodb_1.MongoClient.connect(dbProps.connectionUrl, options); db = client.db(dbProps.name); } this.collection = db.collection(this.options.collectionName); this.db = db; } add(migration) { if (typeof migration.up !== 'function') { throw new Error('Migration must supply an up function.'); } if (typeof migration.down !== 'function') { throw new Error('Migration must supply a down function.'); } if (typeof migration.version !== 'number') { throw new Error('Migration must supply a version number.'); } if (migration.version <= 0) { throw new Error('Migration version must be greater than 0'); } Object.freeze(migration); this.list.push(migration); this.list.sort((a, b) => a.version - b.version); } async migrateTo(command) { if (!this.db) { throw new Error('Migration instance has not be configured/initialized.' + ' Call <instance>.config(..) to initialize this instance'); } if (command == undefined || command === '' || this.list.length === 0) { throw new Error('Cannot migrate using invalid command: ' + command); } let version; let subcommand; if (typeof command === 'number') { version = command; } else { version = command.split(',')[0]; subcommand = command.split(',')[1]; } try { if (version === 'latest') { await this.execute(this.list.at(-1).version); } else { await this.execute(parseInt(version, null), subcommand === 'rerun'); } } catch (e) { this.options.logger('info', `Encountered an error while migrating. Migration failed.`); throw e; } } getNumberOfMigrations() { return this.list.length - 1; } async getVersion() { const control = await this.getControl(); return control.version; } unlock() { this.collection.updateOne({ _id: this.migratorKey }, { $set: { locked: false } }); } async reset() { this.list = [this.defaultMigration]; await this.collection.deleteMany({}); } async execute(version, rerun) { const self = this; const control = await this.getControl(); let currentVersion = control.version; const lock = async () => { const updateResult = await self.collection.findOneAndUpdate({ _id: this.migratorKey, locked: false, }, { $set: { locked: true, lockedAt: new Date(), }, }, { includeResultMetadata: true }); return null != updateResult.value && 1 === updateResult.ok; }; const unlock = () => self.setControl({ locked: false, version: currentVersion, }); const updateVersion = async () => await self.setControl({ locked: true, version: currentVersion, }); const migrate = async (direction, idx) => { const migration = self.list[idx]; if (typeof migration[direction] !== 'function') { unlock(); throw new Error('Cannot migrate ' + direction + ' on version ' + migration.version); } function maybeName() { return migration.name ? ' (' + migration.name + ')' : ''; } this.options.logger('info', 'Running ' + direction + '() on version ' + migration.version + maybeName()); await migration[direction](self.db, this.options.logger); }; if ((await lock()) === false) { this.options.logger('info', 'Not migrating, control is locked.'); return; } if (rerun) { this.options.logger('info', 'Rerunning version ' + version); migrate('up', version); this.options.logger('info', 'Finished migrating.'); await unlock(); return; } if (currentVersion === version) { if (this.options.logIfLatest) { this.options.logger('info', 'Not migrating, already at version ' + version); } await unlock(); return; } const startIdx = this.findIndexByVersion(currentVersion); const endIdx = this.findIndexByVersion(version); this.options.logger('info', 'Migrating from version ' + this.list[startIdx].version + ' -> ' + this.list[endIdx].version); if (currentVersion < version) { for (let i = startIdx; i < endIdx; i++) { try { await migrate('up', i + 1); currentVersion = self.list[i + 1].version; await updateVersion(); } catch (e) { const prevVersion = self.list[i].version; const destVersion = self.list[i + 1].version; this.options.logger('error', `Encountered an error while migrating from ${prevVersion} to ${destVersion}`); throw e; } } } else { for (let i = startIdx; i > endIdx; i--) { try { await migrate('down', i); currentVersion = self.list[i - 1].version; await updateVersion(); } catch (e) { const prevVersion = self.list[i].version; const destVersion = self.list[i - 1].version; this.options.logger('error', `Encountered an error while migrating from ${prevVersion} to ${destVersion}`); throw e; } } } await unlock(); this.options.logger('info', 'Finished migrating.'); } async getControl() { const con = await this.collection.findOne({ _id: this.migratorKey }); return (con || (await this.setControl({ locked: false, version: 0, }))); } async setControl(control) { (0, node_assert_1.default)(control && typeof control === 'object'); (0, node_assert_1.default)(typeof control.version === 'number'); (0, node_assert_1.default)(typeof control.locked === 'boolean'); const updateResult = await this.collection.updateOne({ _id: this.migratorKey, }, { $set: { locked: control.locked, version: control.version, }, }, { upsert: true, }); if (updateResult && updateResult.acknowledged) { return control; } else { return null; } } findIndexByVersion(version) { for (let i = 0; i < this.list.length; i++) { if (this.list[i].version === version) { return i; } } throw new Error("Can't find migration version " + version); } } exports.Migrator = Migrator; //# sourceMappingURL=migrator.js.map