arango-migrate
Version:
Migration tools for ArangoDB
427 lines (417 loc) • 15 kB
JavaScript
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