diff --git a/README.md b/README.md index 9cb3122..44364e4 100644 --- a/README.md +++ b/README.md @@ -302,10 +302,14 @@ Runs any un-executed committed migrations, as well as the current migration. For development. Options: - --help Show help [boolean] - -c, --config Optional path to gmrc file [string] [default: .gmrc[.js|.cjs]] - --shadow Apply migrations to the shadow DB (for development). + --help Show help [boolean] + -c, --config Optional path to gmrc file + [string] [default: .gmrc[.js|.cjs]] + --shadow Apply migrations to the shadow DB (for development). [boolean] [default: false] + --forceActions Run beforeAllMigrations, afterAllMigrations, + beforeCurrent, and afterCurrent actions even if no + migration was necessary. [boolean] [default: false] ``` diff --git a/__tests__/current.test.ts b/__tests__/current.test.ts index aa98590..8174da9 100644 --- a/__tests__/current.test.ts +++ b/__tests__/current.test.ts @@ -1,10 +1,12 @@ +jest.mock("child_process"); import "./helpers"; // Has side-effects; must come first +import { exec } from "child_process"; import mockFs from "mock-fs"; import { current } from "../src"; import { withClient } from "../src/pg"; -import { ParsedSettings, parseSettings } from "../src/settings"; +import { ParsedSettings, parseSettings, Settings } from "../src/settings"; import { makeMigrations, resetDb, settings } from "./helpers"; beforeEach(resetDb); @@ -134,3 +136,69 @@ it("runs migrations", async () => { expect(newTables).toEqual(tables); expect(newEnums).toEqual(enums); }); + +it("runs actions when forceActions is set", async () => { + const ACTIONS = { + initial: { + forceActions: false, + currentSql: "", + expectedActions: [ + "beforeAllMigrations", + "afterAllMigrations", + "beforeCurrent", + "afterCurrent", + ], + }, + currentChange: { + forceActions: false, + currentSql: MIGRATION_NOTRX_TEXT, + expectedActions: ["beforeCurrent", "afterCurrent"], + }, + noop: { + forceActions: false, + currentSql: MIGRATION_NOTRX_TEXT, + expectedActions: [], + }, + forceActions: { + forceActions: true, + currentSql: MIGRATION_NOTRX_TEXT, + expectedActions: [ + "beforeAllMigrations", + "afterAllMigrations", + "beforeCurrent", + "afterCurrent", + ], + }, + } as const; + const settingsWithHooks: Settings = { + ...settings, + beforeAllMigrations: [ + { _: "command", command: "echo did_beforeAllMigrations" }, + ], + afterAllMigrations: [ + { _: "command", command: "echo did_afterAllMigrations" }, + ], + beforeCurrent: [{ _: "command", command: "echo did_beforeCurrent" }], + afterCurrent: [{ _: "command", command: "echo did_afterCurrent" }], + }; + for (const mode of Object.keys(ACTIONS) as Array) { + const { forceActions, currentSql, expectedActions } = ACTIONS[mode]; + + mockFs({ + [`migrations/committed/000001.sql`]: MIGRATION_1_COMMITTED, + [`migrations/committed/000002.sql`]: MIGRATION_ENUM_COMMITTED, // Creates enum with 1 value + "migrations/current.sql": currentSql, + }); + + const mockedExec: jest.Mock = exec as any; + mockedExec.mockClear(); + mockedExec.mockImplementation((_cmd, _options, callback) => + callback(null, { stdout: "", stderr: "" }), + ); + await current(settingsWithHooks, { forceActions }); + const calledActions = mockedExec.mock.calls.map((c) => + c[0].substring("echo did_".length), + ); + expect(calledActions).toEqual(expectedActions); + } +}); diff --git a/src/commands/current.ts b/src/commands/current.ts index 29e4241..57c8eea 100644 --- a/src/commands/current.ts +++ b/src/commands/current.ts @@ -9,15 +9,16 @@ import { _migrate } from "./migrate"; interface CurrentArgv extends CommonArgv { shadow?: boolean; + forceActions?: boolean; } export async function current( settings: Settings, options: Partial = {}, ): Promise { - const { shadow = false } = options; + const { shadow = false, forceActions = false } = options; const parsedSettings = await parseSettings(settings, shadow); - await _migrate(parsedSettings, shadow); + await _migrate(parsedSettings, shadow, forceActions); const currentLocation = await getCurrentMigrationLocation(parsedSettings); if (!currentLocation.exists) { @@ -31,6 +32,7 @@ export async function current( const run = makeCurrentMigrationRunner(parsedSettings, { once: true, shadow, + forceActions, }); return run(); } @@ -49,6 +51,12 @@ export const currentCommand: CommandModule< default: false, description: "Apply migrations to the shadow DB (for development).", }, + forceActions: { + type: "boolean", + default: false, + description: + "Run beforeAllMigrations, afterAllMigrations, beforeCurrent, and afterCurrent actions even if no migration was necessary.", + }, }, handler: async (argv) => { const settings = await getSettings({ configFile: argv.config }); diff --git a/src/currentRunner.ts b/src/currentRunner.ts index 35758c1..4333b52 100644 --- a/src/currentRunner.ts +++ b/src/currentRunner.ts @@ -13,9 +13,10 @@ export function makeCurrentMigrationRunner( options: { once?: boolean; shadow?: boolean; + forceActions?: boolean; } = {}, ): () => Promise { - const { shadow = false } = options; + const { shadow = false, forceActions = false } = options; async function run(): Promise { const currentLocation = await getCurrentMigrationLocation(parsedSettings); const body = await readCurrentMigration(parsedSettings, currentLocation); @@ -74,14 +75,17 @@ export function makeCurrentMigrationRunner( migrationsAreEquivalent = currentBodyMinified === previousBodyMinified; - // 4: if different - if (!migrationsAreEquivalent) { + // 3a: Run actions if the migrations are different OR if forced. + if (forceActions || !migrationsAreEquivalent) { await executeActions( parsedSettings, shadow, parsedSettings.beforeCurrent, ); + } + // 4: if different + if (!migrationsAreEquivalent) { // 4a: invert previous current; on success delete from graphile_migrate.current; on failure rollback and abort if (previousBody) { await reverseMigration(lockingPgClient, previousBody); @@ -135,7 +139,7 @@ export function makeCurrentMigrationRunner( ); const interval = process.hrtime(start); const duration = interval[0] * 1e3 + interval[1] * 1e-6; - if (!migrationsAreEquivalent) { + if (forceActions || !migrationsAreEquivalent) { await executeActions( parsedSettings, shadow,