UNPKG

arango-migrate

Version:
427 lines (417 loc) 15 kB
var glob = require('glob'); var path = require('path'); var arangojs = require('arangojs'); var fs = require('fs'); var slugify = require('slugify'); var url = require('url'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return n; } var glob__default = /*#__PURE__*/_interopDefaultLegacy(glob); var path__default = /*#__PURE__*/_interopDefaultLegacy(path); var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var slugify__default = /*#__PURE__*/_interopDefaultLegacy(slugify); const isString = s => { return typeof s === 'string' || s instanceof String; }; const MIGRATION_TEMPLATE_JS = `/** * @typedef { import("arango-migrate").Migration } Migration */ /** * @type { Migration } */ const migration = { async collections () { return [] }, async up (db, step) { } } export default migration `; const MIGRATION_TEMPLATE_TS = `import { Collections, Migration, StepFunction } from 'arango-migrate' import { Database } from 'arangojs' const migration: Migration = { async collections (): Promise<Collections> { return [] }, async up (db: Database, step: StepFunction) {} } export default migration `; const DEFAULT_CONFIG_PATH = './config.migrate.js'; const DEFAULT_MIGRATIONS_PATH = './migrations'; const DEFAULT_MIGRATION_HISTORY_COLLECTION = 'migration_history'; class ArangoMigrate { constructor(options) { this.options = void 0; this.migrationHistoryCollection = void 0; this.db = void 0; this.migrationPaths = void 0; this.migrationsPath = void 0; this.options = options; this.migrationsPath = this.options.migrationsPath || DEFAULT_MIGRATIONS_PATH; this.migrationHistoryCollection = this.options.migrationHistoryCollection || DEFAULT_MIGRATION_HISTORY_COLLECTION; this.migrationPaths = this.loadMigrationPaths(this.migrationsPath); } static async loadConfig(configPath = DEFAULT_CONFIG_PATH) { const p = path__default["default"].resolve(configPath); if (!fs__default["default"].existsSync(p)) { throw new Error(`Config file ${p} not found.`); } const importedConfig = await (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })(url.pathToFileURL(p).href); const config = importedConfig.default; if (!config.dbConfig) { throw new Error('Config object must contain a dbConfig property.'); } return config; } loadMigrationPaths(migrationsPath) { return glob__default["default"].sync(migrationsPath + '/*').reduce((acc, filePath) => { return [...acc, path__default["default"].resolve(filePath)]; }, []).sort((a, b) => a.localeCompare(b)); } getMigrationPaths() { return this.migrationPaths; } async initialize() { const name = this.options.dbConfig.databaseName; try { this.db = new arangojs.Database({ ...this.options.dbConfig, databaseName: undefined }); this.db = await this.db.createDatabase(name); } catch (err) { this.db = new arangojs.Database(this.options.dbConfig); this.db = this.db.database(name); } } migrationExists(version) { return this.getMigrationPathFromVersion(version) !== undefined; } getMigrationPathFromVersion(version) { return this.migrationPaths.find(x => { const basename = path__default["default"].basename(x); return version === Number(basename.split('_')[0]); }); } async getMigrationFromVersion(version) { const migrationPath = this.migrationPaths.find(x => { const basename = path__default["default"].basename(x); return version === Number(basename.split('_')[0]) && fs__default["default"].existsSync(path__default["default"].resolve(x)); }); const importedMigration = await (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })(url.pathToFileURL(migrationPath).href); return importedMigration.default; } async getMigrationHistoryCollection() { let collection; try { collection = await this.db.createCollection(this.migrationHistoryCollection); } catch { collection = this.db.collection(this.migrationHistoryCollection); } return collection; } async getMigrationHistory(direction = 'ASC') { const collection = await this.getMigrationHistoryCollection(); return await (await this.db.query(arangojs.aql` FOR x IN ${collection} SORT x.counter ${direction} RETURN x`)).all(); } async getLatestMigration() { const collection = await this.getMigrationHistoryCollection(); return await (await this.db.query(arangojs.aql` FOR x IN ${collection} SORT x.counter DESC LIMIT 1 RETURN x`)).next(); } async writeMigrationHistory(direction, name, description, version) { const collection = await this.getMigrationHistoryCollection(); const latest = await this.getLatestMigration(); await collection.save({ name, description, version, direction, counter: latest ? latest.counter + 1 : 1, createdAt: new Date() }); } async initializeTransactionCollections(collections) { const newCollections = new Set(); const allCollectionNames = new Set(); const transactionCollections = []; let createdCollectionCount = 0; for (const collectionData of collections) { const data = isString(collectionData) ? { collectionName: collectionData } : collectionData; allCollectionNames.add(data.collectionName); let collection; try { if (this.options.autoCreateNewCollections !== false) { /** * NOTE: arangojs *.d.ts invites user to pass "literal" options object * to infer typeof collection. Thus there is no another way to support * collections() API but using this ugly "as any" cast */ collection = await this.db.createCollection(data.collectionName, data.options); createdCollectionCount++; newCollections.add(collection); } } catch { collection = this.db.collection(data.collectionName); if (!collection) { throw new Error(`Collection ${data.collectionName} not found.`); } } if (collection) { transactionCollections.push(collection); } } return { transactionCollections, newCollections, allCollectionNames, createdCollectionCount }; } async runUpMigrations(to, dryRun, noHistory) { const versions = this.getVersionsFromMigrationPaths(); if (!to) { to = versions[versions.length - 1]; } const history = await this.getMigrationHistory('DESC'); const versionsToRun = versions.filter(version => { const migration = history.find(migration => migration.version === version); return (migration == null ? void 0 : migration.direction) !== 'up' && version <= to; }); let appliedMigrations = 0; let createdCollections = 0; for (const i of versionsToRun) { let migration; try { migration = await this.getMigrationFromVersion(i); } catch (err) { console.log(err); return; } const name = path__default["default"].basename(this.getMigrationPathFromVersion(i)); const collectionNames = migration.collections ? await migration.collections() : []; const { transactionCollections, newCollections, createdCollectionCount } = await this.initializeTransactionCollections(collectionNames); createdCollections += createdCollectionCount; let beforeUpData; if (migration.beforeUp) { beforeUpData = await migration.beforeUp(this.db); } const transactionOptions = await (migration.transactionOptions == null ? void 0 : migration.transactionOptions()); const transaction = await this.db.beginTransaction(transactionCollections, transactionOptions); let error; let upResult; if (migration.up) { try { upResult = await migration.up(this.db, callback => transaction.step(callback), beforeUpData); } catch (err) { console.log(err); error = new Error(`Running up failed for migration ${i}.`); } } if (!dryRun) { try { const transactionStatus = await transaction.commit(); if (transactionStatus.status !== 'committed') { error = new Error(`Transaction failed with status ${transactionStatus.status} for migration ${name}.`); } } catch (err) { error = new Error('Transaction failed.'); } } try { if (migration.afterUp) { await migration.afterUp(this.db, upResult); } } catch (err) { error = new Error(`afterUp threw an error ${err}.`); } if (error) { for (const collection of Array.from(newCollections)) { await collection.drop(); } } if (!error) { if (!dryRun && noHistory !== true) { await this.writeMigrationHistory('up', name, migration.description, i); } } if (error) { throw error; } appliedMigrations += 1; } return { appliedMigrations, createdCollections }; } async runDownMigrations(to, dryRun, noHistory, disallowMissingVersions) { const latestMigration = await this.getLatestMigration(); if (!latestMigration) { throw new Error('No migrations have been applied.'); } if (!to) { to = 1; } let appliedMigrations = 0; let createdCollections = 0; let version = latestMigration.version; while (version >= to) { if (!this.migrationExists(version) && !disallowMissingVersions) { version--; continue; } let migration; try { migration = await this.getMigrationFromVersion(version); } catch (err) { console.log(err); return; } const name = path__default["default"].basename(this.getMigrationPathFromVersion(version)); const collectionNames = migration.collections ? await migration.collections() : []; const { transactionCollections, newCollections, createdCollectionCount } = await this.initializeTransactionCollections(collectionNames); createdCollections += createdCollectionCount; let error; let beforeDownData; if (migration.beforeDown) { beforeDownData = await migration.beforeDown(this.db); } const transaction = await this.db.beginTransaction(transactionCollections); let downResult; if (migration.down) { try { downResult = await migration.down(this.db, callback => transaction.step(callback), beforeDownData); } catch (err) { console.log(err); error = new Error(`Running up failed for migration ${version}.`); } } if (!dryRun) { try { const transactionStatus = await transaction.commit(); if (transactionStatus.status !== 'committed') { error = new Error(`Transaction failed with status ${transactionStatus.status} for migration ${name}.`); } } catch (err) { error = new Error('Transaction failed.'); } } try { if (migration.afterDown) { await migration.afterDown(this.db, downResult); } } catch (err) { error = new Error(`afterDown threw an error ${err}.`); } if (error) { for (const collection of Array.from(newCollections)) { await collection.drop(); } } if (!error) { if (!dryRun && noHistory !== true) { await this.writeMigrationHistory('down', name, migration.description, version); } } if (error) { throw error; } appliedMigrations += 1; version--; } return { appliedMigrations, createdCollections }; } getVersionsFromMigrationPaths() { return this.migrationPaths.map(migrationPath => { return Number(path__default["default"].basename(migrationPath).split('_')[0]); }).sort((a, b) => a - b); } validateMigrationFolderNotEmpty() { if (this.migrationPaths.length === 0) { throw new Error('No migrations.'); } } validateMigrationVersions() { const versions = this.getVersionsFromMigrationPaths(); if (!versions || versions.length !== new Set(versions).size) { throw new Error('Migration versions must be unique.'); } } async validateMigrationVersion(version) { const latestMigration = await this.getLatestMigration(); if (!latestMigration && version > 1) { throw new Error(`Migration sequence must start with 1, not ${version}.`); } if (latestMigration && version > Number(latestMigration.version) + 1) { throw new Error(`Migration must be ran in sequence. ${version} must immediately follow ${latestMigration.version}.`); } if (latestMigration && version <= Number(latestMigration.version)) { const name = this.getMigrationPathFromVersion(version); throw new Error(`Cannot run up migration ${name} because migration has already been applied.`); } } writeNewMigration(name, typescript) { name = slugify__default["default"](name, '_'); const version = Date.now(); if (!fs__default["default"].existsSync(path__default["default"].resolve(this.migrationsPath))) { fs__default["default"].mkdirSync(path__default["default"].resolve(this.migrationsPath)); } const res = path__default["default"].resolve(`${this.migrationsPath}/${version}_${name}${typescript ? '.ts' : '.js'}`); fs__default["default"].writeFileSync(res, typescript ? MIGRATION_TEMPLATE_TS : MIGRATION_TEMPLATE_JS); return res; } async hasNewMigrations() { const history = await this.getMigrationHistory('DESC'); if (!history.length) { return true; } const versions = this.getVersionsFromMigrationPaths(); return versions.filter(version => { const migration = history.find(migration => migration.version === version); return (migration == null ? void 0 : migration.direction) !== 'up'; }).length !== 0; } } exports.ArangoMigrate = ArangoMigrate; exports.DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_PATH; exports.DEFAULT_MIGRATIONS_PATH = DEFAULT_MIGRATIONS_PATH; exports.DEFAULT_MIGRATION_HISTORY_COLLECTION = DEFAULT_MIGRATION_HISTORY_COLLECTION; //# sourceMappingURL=index.cjs.map