UNPKG

umzug

Version:

Framework-agnostic migration tool for Node

414 lines 20.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _a; Object.defineProperty(exports, "__esModule", { value: true }); exports.Umzug = exports.MigrationError = void 0; const emittery_1 = __importDefault(require("emittery")); const fast_glob_1 = require("fast-glob"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const errorCause = __importStar(require("pony-cause")); const url_1 = require("url"); const cli_1 = require("./cli"); const storage_1 = require("./storage"); const templates = __importStar(require("./templates")); const types_1 = require("./types"); class MigrationError extends errorCause.ErrorWithCause { // TODO [>=4.0.0] Take a `{ cause: ... }` options bag like the default `Error`, it looks like this because of verror backwards-compatibility. constructor(migration, original) { super(`Migration ${migration.name} (${migration.direction}) failed: ${MigrationError.errorString(original)}`, { cause: original, }); this.name = 'MigrationError'; this.jse_cause = original; this.migration = migration; } // TODO [>=4.0.0] Remove this backwards-compatibility alias get info() { return this.migration; } static errorString(cause) { return cause instanceof Error ? `Original error: ${cause.message}` : `Non-error value thrown. See info for full props: ${cause}`; } } exports.MigrationError = MigrationError; class Umzug extends emittery_1.default { /** creates a new Umzug instance */ constructor(options) { var _b; super(); this.options = options; this.storage = (0, storage_1.verifyUmzugStorage)((_b = options.storage) !== null && _b !== void 0 ? _b : new storage_1.JSONStorage()); this.migrations = this.getMigrationsResolver(this.options.migrations); } logging(message) { var _b; (_b = this.options.logger) === null || _b === void 0 ? void 0 : _b.info(message); } /** * Get an UmzugCLI instance. This can be overriden in a subclass to add/remove commands - only use if you really know you need this, * and are OK to learn about/interact with the API of @rushstack/ts-command-line. */ getCli(options) { return new cli_1.UmzugCLI(this, options); } /** * 'Run' an umzug instance as a CLI. This will read `process.argv`, execute commands based on that, and call * `process.exit` after running. If that isn't what you want, stick to the programmatic API. * You probably want to run only if a file is executed as the process's 'main' module with something like: * @example * if (require.main === module) { * myUmzugInstance.runAsCLI() * } */ async runAsCLI(argv) { const cli = this.getCli(); return cli.execute(argv); } /** Get the list of migrations which have already been applied */ async executed() { return this.runCommand('executed', async ({ context }) => { const list = await this._executed(context); // We do the following to not expose the `up` and `down` functions to the user return list.map(m => ({ name: m.name, path: m.path })); }); } /** Get the list of migrations which have already been applied */ async _executed(context) { const [migrations, executedNames] = await Promise.all([this.migrations(context), this.storage.executed({ context })]); const executedSet = new Set(executedNames); return migrations.filter(m => executedSet.has(m.name)); } /** Get the list of migrations which are yet to be applied */ async pending() { return this.runCommand('pending', async ({ context }) => { const list = await this._pending(context); // We do the following to not expose the `up` and `down` functions to the user return list.map(m => ({ name: m.name, path: m.path })); }); } async _pending(context) { const [migrations, executedNames] = await Promise.all([this.migrations(context), this.storage.executed({ context })]); const executedSet = new Set(executedNames); return migrations.filter(m => !executedSet.has(m.name)); } async runCommand(command, cb) { const context = await this.getContext(); await this.emit('beforeCommand', { command, context }); try { return await cb({ context }); } finally { await this.emit('afterCommand', { command, context }); } } /** * Apply migrations. By default, runs all pending migrations. * @see MigrateUpOptions for other use cases using `to`, `migrations` and `rerun`. */ async up(options = {}) { const eligibleMigrations = async (context) => { var _b; if (options.migrations && options.rerun === types_1.RerunBehavior.ALLOW) { // Allow rerun means the specified migrations should be run even if they've run before - so get all migrations, not just pending const list = await this.migrations(context); return this.findMigrations(list, options.migrations); } if (options.migrations && options.rerun === types_1.RerunBehavior.SKIP) { const executedNames = new Set((await this._executed(context)).map(m => m.name)); const filteredMigrations = options.migrations.filter(m => !executedNames.has(m)); return this.findMigrations(await this.migrations(context), filteredMigrations); } if (options.migrations) { return this.findMigrations(await this._pending(context), options.migrations); } const allPending = await this._pending(context); let sliceIndex = (_b = options.step) !== null && _b !== void 0 ? _b : allPending.length; if (options.to) { sliceIndex = this.findNameIndex(allPending, options.to) + 1; } return allPending.slice(0, sliceIndex); }; return this.runCommand('up', async ({ context }) => { const toBeApplied = await eligibleMigrations(context); for (const m of toBeApplied) { const start = Date.now(); const params = { name: m.name, path: m.path, context }; this.logging({ event: 'migrating', name: m.name }); await this.emit('migrating', params); try { await m.up(params); } catch (e) { throw new MigrationError({ direction: 'up', ...params }, e); } await this.storage.logMigration(params); const duration = (Date.now() - start) / 1000; this.logging({ event: 'migrated', name: m.name, durationSeconds: duration }); await this.emit('migrated', params); } return toBeApplied.map(m => ({ name: m.name, path: m.path })); }); } /** * Revert migrations. By default, the last executed migration is reverted. * @see MigrateDownOptions for other use cases using `to`, `migrations` and `rerun`. */ async down(options = {}) { const eligibleMigrations = async (context) => { var _b; if (options.migrations && options.rerun === types_1.RerunBehavior.ALLOW) { const list = await this.migrations(context); return this.findMigrations(list, options.migrations); } if (options.migrations && options.rerun === types_1.RerunBehavior.SKIP) { const pendingNames = new Set((await this._pending(context)).map(m => m.name)); const filteredMigrations = options.migrations.filter(m => !pendingNames.has(m)); return this.findMigrations(await this.migrations(context), filteredMigrations); } if (options.migrations) { return this.findMigrations(await this._executed(context), options.migrations); } const executedReversed = (await this._executed(context)).slice().reverse(); let sliceIndex = (_b = options.step) !== null && _b !== void 0 ? _b : 1; if (options.to === 0 || options.migrations) { sliceIndex = executedReversed.length; } else if (options.to) { sliceIndex = this.findNameIndex(executedReversed, options.to) + 1; } return executedReversed.slice(0, sliceIndex); }; return this.runCommand('down', async ({ context }) => { var _b; const toBeReverted = await eligibleMigrations(context); for (const m of toBeReverted) { const start = Date.now(); const params = { name: m.name, path: m.path, context }; this.logging({ event: 'reverting', name: m.name }); await this.emit('reverting', params); try { await ((_b = m.down) === null || _b === void 0 ? void 0 : _b.call(m, params)); } catch (e) { throw new MigrationError({ direction: 'down', ...params }, e); } await this.storage.unlogMigration(params); const duration = Number.parseFloat(((Date.now() - start) / 1000).toFixed(3)); this.logging({ event: 'reverted', name: m.name, durationSeconds: duration }); await this.emit('reverted', params); } return toBeReverted.map(m => ({ name: m.name, path: m.path })); }); } async create(options) { await this.runCommand('create', async ({ context }) => { var _b, _c, _d, _e; const isoDate = new Date().toISOString(); const prefixes = { TIMESTAMP: isoDate.replace(/\.\d{3}Z$/, '').replace(/\W/g, '.'), DATE: isoDate.split('T')[0].replace(/\W/g, '.'), NONE: '', }; const prefixType = (_b = options.prefix) !== null && _b !== void 0 ? _b : 'TIMESTAMP'; const fileBasename = [prefixes[prefixType], options.name].filter(Boolean).join('.'); const allowedExtensions = options.allowExtension ? [options.allowExtension] : ['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.sql']; const existing = await this.migrations(context); const last = existing.slice(-1)[0]; const folder = options.folder || ((_c = this.options.create) === null || _c === void 0 ? void 0 : _c.folder) || ((last === null || last === void 0 ? void 0 : last.path) && path.dirname(last.path)); if (!folder) { throw new Error(`Couldn't infer a directory to generate migration file in. Pass folder explicitly`); } const filepath = path.join(folder, fileBasename); if (!options.allowConfusingOrdering) { const confusinglyOrdered = existing.find(e => e.path && e.path >= filepath); if (confusinglyOrdered) { throw new Error(`Can't create ${fileBasename}, since it's unclear if it should run before or after existing migration ${confusinglyOrdered.name}. Use allowConfusingOrdering to bypass this error.`); } } const template = typeof options.content === 'string' ? async () => [[filepath, options.content]] : // eslint-disable-next-line @typescript-eslint/unbound-method (_e = (_d = this.options.create) === null || _d === void 0 ? void 0 : _d.template) !== null && _e !== void 0 ? _e : Umzug.defaultCreationTemplate; const toWrite = await template(filepath); if (toWrite.length === 0) { toWrite.push([filepath, '']); } toWrite.forEach(pair => { if (!Array.isArray(pair) || pair.length !== 2) { throw new Error(`Expected [filepath, content] pair. Check that the file template function returns an array of pairs.`); } const ext = path.extname(pair[0]); if (!allowedExtensions.includes(ext)) { const allowStr = allowedExtensions.join(', '); const message = `Extension ${ext} not allowed. Allowed extensions are ${allowStr}. See help for allowExtension to avoid this error.`; throw new Error(message); } fs.mkdirSync(path.dirname(pair[0]), { recursive: true }); fs.writeFileSync(pair[0], pair[1]); this.logging({ event: 'created', path: pair[0] }); }); if (!options.skipVerify) { const [firstFilePath] = toWrite[0]; const pending = await this._pending(context); if (!pending.some(p => p.path && path.resolve(p.path) === path.resolve(firstFilePath))) { const paths = pending.map(p => p.path).join(', '); throw new Error(`Expected ${firstFilePath} to be a pending migration but it wasn't! Pending migration paths: ${paths}. You should investigate this. Use skipVerify to bypass this error.`); } } }); } static defaultCreationTemplate(filepath) { const ext = path.extname(filepath); if ((ext === '.js' && typeof require.main === 'object') || ext === '.cjs') { return [[filepath, templates.js]]; } if (ext === '.ts' || ext === '.mts' || ext === '.cts') { return [[filepath, templates.ts]]; } if ((ext === '.js' && require.main === undefined) || ext === '.mjs') { return [[filepath, templates.mjs]]; } if (ext === '.sql') { const downFilepath = path.join(path.dirname(filepath), 'down', path.basename(filepath)); return [ [filepath, templates.sqlUp], [downFilepath, templates.sqlDown], ]; } return []; } findNameIndex(migrations, name) { const index = migrations.findIndex(m => m.name === name); if (index === -1) { throw new Error(`Couldn't find migration to apply with name ${JSON.stringify(name)}`); } return index; } findMigrations(migrations, names) { const map = new Map(migrations.map(m => [m.name, m])); return names.map(name => { const migration = map.get(name); if (!migration) { throw new Error(`Couldn't find migration to apply with name ${JSON.stringify(name)}`); } return migration; }); } async getContext() { const { context = {} } = this.options; // eslint-disable-next-line @typescript-eslint/no-unsafe-return return typeof context === 'function' ? context() : context; } /** helper for parsing input migrations into a callback returning a list of ready-to-run migrations */ getMigrationsResolver(inputMigrations) { var _b; if (Array.isArray(inputMigrations)) { return async () => inputMigrations; } if (typeof inputMigrations === 'function') { // Lazy migrations definition, recurse. return async (ctx) => { const resolved = await inputMigrations(ctx); return this.getMigrationsResolver(resolved)(ctx); }; } const fileGlob = inputMigrations.glob; const [globString, globOptions] = Array.isArray(fileGlob) ? fileGlob : [fileGlob]; const ignore = typeof (globOptions === null || globOptions === void 0 ? void 0 : globOptions.ignore) === 'string' ? [globOptions.ignore] : globOptions === null || globOptions === void 0 ? void 0 : globOptions.ignore; const resolver = (_b = inputMigrations.resolve) !== null && _b !== void 0 ? _b : Umzug.defaultResolver; return async (context) => { const paths = await (0, fast_glob_1.glob)(globString, { ...globOptions, ignore, absolute: true }); paths.sort(); // glob returns results in reverse alphabetical order these days, but it has never guaranteed not to do that https://github.com/isaacs/node-glob/issues/570 return paths.map(unresolvedPath => { const filepath = path.resolve(unresolvedPath); const name = path.basename(filepath); return { path: filepath, ...resolver({ name, path: filepath, context }), }; }); }; } } exports.Umzug = Umzug; _a = Umzug; Umzug.defaultResolver = ({ name, path: filepath }) => { if (!filepath) { throw new Error(`Can't use default resolver for non-filesystem migrations`); } const ext = path.extname(filepath); const languageSpecificHelp = { '.ts': "TypeScript files can be required by adding `ts-node` as a dependency and calling `require('ts-node/register')` at the program entrypoint before running migrations.", '.sql': 'Try writing a resolver which reads file content and executes it as a sql query.', }; languageSpecificHelp['.cts'] = languageSpecificHelp['.ts']; languageSpecificHelp['.mts'] = languageSpecificHelp['.ts']; let loadModule; const jsExt = ext.replace(/\.([cm]?)ts$/, '.$1js'); const getModule = async () => { try { return await loadModule(); } catch (e) { if ((e instanceof SyntaxError || e instanceof MissingResolverError) && ext in languageSpecificHelp) { e.message += '\n\n' + languageSpecificHelp[ext]; } throw e; } }; if ((jsExt === '.js' && typeof require.main === 'object') || jsExt === '.cjs') { // eslint-disable-next-line @typescript-eslint/no-var-requires loadModule = async () => require(filepath); } else if (jsExt === '.js' || jsExt === '.mjs') { const fileUrl = (0, url_1.pathToFileURL)(filepath).href; loadModule = async () => import(fileUrl); } else { loadModule = async () => { throw new MissingResolverError(filepath); }; } return { name, path: filepath, up: async ({ context }) => (await getModule()).up({ path: filepath, name, context }), down: async ({ context }) => { var _b, _c; return (_c = (_b = (await getModule())).down) === null || _c === void 0 ? void 0 : _c.call(_b, { path: filepath, name, context }); }, }; }; class MissingResolverError extends Error { constructor(filepath) { super(`No resolver specified for file ${filepath}. See docs for guidance on how to write a custom resolver.`); } } //# sourceMappingURL=umzug.js.map