UNPKG

abstract-migrate

Version:
262 lines (219 loc) 7.38 kB
import fs from 'fs'; import path from 'path'; import chalk from 'chalk'; import config from '../config'; import storage from './storage'; import { nameOrNumber } from './arguments'; import { mapSeries, promiseback } from './promise'; export const DIRECTION_UP = 'direction/up'; export const DIRECTION_DOWN = 'direction/down'; export const DIRECTION_ROLLBACK = 'direction/rollback'; export async function loadFromFs(migrationPath) { return new Promise((resolve, reject) => { fs.readdir(migrationPath, (err, files) => { if (err) { reject(err); } else { resolve( files .filter(file => file.match(/^\d+-[\w\d_-]+\.js$/)) .map(file => file.slice(0, -3)) .sort() ); } }); }); } export async function run(filename, direction, timestamp) { const migration = require(path.join(config.migrationPath, filename)); if (direction === DIRECTION_UP) { await promiseback(migration.up); } else if (direction === DIRECTION_DOWN || direction === DIRECTION_ROLLBACK) { await promiseback(migration.down); } else { throw new Error('Unknown migration direction'); } return { name: filename, timestamp }; } export function filterUp(ranMigrations, files, { ignorePast = false, until, count, only }) { let migrationsToRun; if (!ranMigrations.length) { // We have no ran migrations, run them all! migrationsToRun = files; } else if (ignorePast) { // Only grab migrations since the most recently ran const timestamp = ranMigrations[0].name.match(/^(\d+)-/)[1]; migrationsToRun = files.filter(filename => filename.match(/^(\d+)-/)[1] > timestamp ); } else { // Grab all of the unran migrations migrationsToRun = files.filter(file => !ranMigrations.find(migration => file === migration.name) ); } // If we're requesting a specific migration, check for validity and return if (only) { if (!until) { throw new Error('A migration name must be provided with the --only flag'); } return migrationsToRun.filter(file => file === until); } if (until) { return migrationsToRun.filter(file => file <= until); } if (count) { return migrationsToRun.slice(0, count); } return migrationsToRun; } export function filterDown(ranMigrations, files, { until, count, only }) { if (!ranMigrations) { return []; } let migrationsToRun; if (only) { if (!until) { throw new Error('A migration name must be provided with the --only flag'); } migrationsToRun = ranMigrations.filter(({ name }) => name === until); } else if (count) { migrationsToRun = ranMigrations.slice(0, count); } else if (until) { migrationsToRun = ranMigrations.filter(({ name }) => name >= until); } else { throw new Error('A downward migration requires either a migration name or count'); } migrationsToRun = migrationsToRun.map(({ name }) => name); migrationsToRun.forEach((migration) => { if (files.indexOf(migration) === -1) { throw new Error(`Migration '${migration}' cannot be downgraded because it does not have a migration file`); } }); return migrationsToRun; } export function filterRollback(ranMigrations, files) { if (!ranMigrations || !ranMigrations.length) { return []; } const ranMigrationsByDate = ranMigrations .slice() .sort(({ timestamp: left }, { timestamp: right }) => right - left); const migrationsToRun = ranMigrationsByDate .filter(({ timestamp }) => timestamp === ranMigrationsByDate[0].timestamp) .map(({ name }) => name) .sort() .reverse(); migrationsToRun.forEach((migration) => { if (files.indexOf(migration) === -1) { throw new Error(`Migration '${migration}' cannot be downgraded because it does not have a migration file`); } }); return migrationsToRun; } export async function needsToRun(direction, options) { // Fetch the migrations we've ran and the ones that are available const ranMigrations = await storage.load(); // Sort the ran migrations by their name -- newest migration first ranMigrations.sort(({ name: left }, { name: right }) => right.localeCompare(left)); const files = await loadFromFs(config.migrationPath); switch (direction) { case DIRECTION_UP: return filterUp(ranMigrations, files, options); case DIRECTION_DOWN: return filterDown(ranMigrations, files, options); case DIRECTION_ROLLBACK: return filterRollback(ranMigrations, files, options); default: throw new Error(`Unknown migration direct '${direction}`); } } const wordMap = { intro: { [DIRECTION_UP]: 'Running migrations', [DIRECTION_DOWN]: 'Running down migrations', [DIRECTION_ROLLBACK]: 'Rolling back migrations', }, nothing: { [DIRECTION_UP]: 'There are no pending migrations', [DIRECTION_DOWN]: 'There are no migrations to run down', [DIRECTION_ROLLBACK]: 'There are no migrations to roll back', }, }; const mapWords = direction => topic => wordMap[topic][direction]; export async function framework(direction, name, options) { const t = mapWords(direction); const UP = direction === DIRECTION_UP; const needsLock = !options.dryRun; console.log(chalk.gray(t('intro'))); if (needsLock) { // Attempt to acquire a lock before we figure out what to run const lockAcquired = await storage.acquireLock(); if (!lockAcquired) { console.log( chalk.yellow.bold('Could not lock') + chalk.yellow(' Lock could not be acquired, quitting') ); return; } } try { const nameValue = nameOrNumber(name); const migrationsToRun = await needsToRun( direction, { ignorePast: UP ? options.ignorePast : undefined, only: !!options.only, until: typeof nameValue !== 'number' ? nameValue : null, count: typeof nameValue === 'number' ? nameValue : null, } ); if (!migrationsToRun.length) { console.log(t('nothing')); return; } // If they're asking for a dry run, print it and exit if (options.dryRun) { console.log( chalk.yellow.bold('Dry run') + chalk.yellow(' No migrations will be executed') ); migrationsToRun.forEach((file) => { console.log('[✓] ' + chalk.cyan(file)); }); return; } // Record time once so rollback functions properly const now = Date.now(); // Run the migrations serially const ranMigrations = await mapSeries( migrationsToRun, async (file) => { try { const migration = await run(file, direction, now); console.log('[✓] ' + chalk.cyan(file)); return migration; } catch (err) { console.log('[✘] ' + chalk.red(file)); console.log( chalk.yellow.bold('Migration failed!') + chalk.yellow(' Your database may be in an invalid state.') ); throw err; } } ); if (UP) { // Write the new migrations to the storage await storage.add(ranMigrations.slice().reverse()); } else { // Tell the storage to remove the migrations await storage.remove(ranMigrations); } } finally { if (needsLock) { // Always release the lock await storage.releaseLock(); } } }