diff --git a/README.md b/README.md index 7223a54..8904673 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,9 @@ Commands: graphile-migrate watch Runs any un-executed committed migrations and then runs and watches the current migration, re-running it on any change. For development. + graphile-migrate current Runs any un-executed committed migrations and + then runs the current migration. For + development. graphile-migrate commit Commits the current migration into the `committed/` folder, resetting the current migration. Resets the shadow database. @@ -287,6 +290,24 @@ Options: ``` +## graphile-migrate current + +``` +graphile-migrate current + +Runs any un-executed committed migrations and then runs the current migration. +For development. + +Options: + --help Show help [boolean] + --config, -c Optional path to gmrc file string] [default: .gmrc[.js]] + --shadow Applies changes to shadow DB. [boolean] [default: false] + --forceActions Run beforeAllMigrations, afterAllMigrations, beforeCurrent, + and afterCurrent actions even if no migration was necessary. + [boolean] [default: false] +``` + + ## graphile-migrate commit ``` diff --git a/__tests__/current.test.ts b/__tests__/current.test.ts new file mode 100644 index 0000000..7b5cd7f --- /dev/null +++ b/__tests__/current.test.ts @@ -0,0 +1,149 @@ +import "./helpers"; // Has side-effects; must come first + +import * as mockFs from "mock-fs"; + +import { current } from "../src"; +import { withClient } from "../src/pg"; +import { ParsedSettings, parseSettings } from "../src/settings"; +import { makeMigrations, resetDb, settings } from "./helpers"; + +beforeEach(resetDb); +beforeEach(async () => { + mockFs({ migrations: mockFs.directory() }); +}); +afterEach(() => { + mockFs.restore(); +}); +const { + MIGRATION_1_COMMITTED, + MIGRATION_ENUM_COMMITTED, + MIGRATION_NOTRX_TEXT, + MIGRATION_NOTRX_COMMITTED, +} = makeMigrations(); + +function getStuff(parsedSettings: ParsedSettings) { + return withClient( + parsedSettings.connectionString, + parsedSettings, + async (pgClient, _context) => { + const { rows: migrations } = await pgClient.query( + "select * from graphile_migrate.migrations", + ); + const { rows: tables } = await pgClient.query( + "select * from pg_class where relnamespace = 'public'::regnamespace and relkind = 'r'", + ); + const { rows: enums } = await pgClient.query( + "select typname, (select count(*) from pg_enum where enumtypid = pg_type.oid) as value_count from pg_type where typnamespace = 'public'::regnamespace and typtype = 'e'", + ); + return { migrations, tables, enums }; + }, + ); +} + +it("runs migrations", async () => { + mockFs({ + "migrations/current.sql": "", + }); + + await current(settings); + const parsedSettings = await parseSettings(settings); + + { + const { migrations, tables, enums } = await getStuff(parsedSettings); + expect(migrations).toHaveLength(0); + expect(tables).toHaveLength(0); + expect(enums).toHaveLength(0); + } + + mockFs({ + [`migrations/committed/000001.sql`]: MIGRATION_1_COMMITTED, + [`migrations/committed/000002.sql`]: MIGRATION_ENUM_COMMITTED, + "migrations/current.sql": MIGRATION_NOTRX_TEXT, + }); + + await current(settings); + + { + const { migrations, tables, enums } = await getStuff(parsedSettings); + + expect(migrations).toHaveLength(2); + expect(migrations.map(({ date, ...rest }) => rest)).toMatchInlineSnapshot(` + Array [ + Object { + "filename": "000001.sql", + "hash": "sha1:e00ec93314a423ee5cc68d1182ad52f16442d7df", + "previous_hash": null, + }, + Object { + "filename": "000002.sql", + "hash": "sha1:bddc1ead3310dc1c42cdc7f63537ebdff2e9fd7b", + "previous_hash": "sha1:e00ec93314a423ee5cc68d1182ad52f16442d7df", + }, + ] + `); + expect(tables).toHaveLength(1); + expect(tables.map(t => t.relname)).toMatchInlineSnapshot(` + Array [ + "foo", + ] + `); + expect(enums).toHaveLength(1); + expect(enums).toMatchInlineSnapshot(` +Array [ + Object { + "typname": "user_role", + "value_count": "2", + }, +] +`); + } + + mockFs({ + [`migrations/committed/000001.sql`]: MIGRATION_1_COMMITTED, + [`migrations/committed/000002.sql`]: MIGRATION_ENUM_COMMITTED, + [`migrations/committed/000003.sql`]: MIGRATION_NOTRX_COMMITTED, + "migrations/current.sql": "", + }); + + await current(settings); + + { + const { migrations, tables, enums } = await getStuff(parsedSettings); + + expect(migrations).toHaveLength(3); + expect(migrations.map(({ date, ...rest }) => rest)).toMatchInlineSnapshot(` + Array [ + Object { + "filename": "000001.sql", + "hash": "sha1:e00ec93314a423ee5cc68d1182ad52f16442d7df", + "previous_hash": null, + }, + Object { + "filename": "000002.sql", + "hash": "sha1:bddc1ead3310dc1c42cdc7f63537ebdff2e9fd7b", + "previous_hash": "sha1:e00ec93314a423ee5cc68d1182ad52f16442d7df", + }, + Object { + "filename": "000003.sql", + "hash": "sha1:2d248344ac299ebbad2aeba5bfec2ae3c3cb0a4f", + "previous_hash": "sha1:bddc1ead3310dc1c42cdc7f63537ebdff2e9fd7b", + }, + ] + `); + expect(tables).toHaveLength(1); + expect(tables.map(t => t.relname)).toMatchInlineSnapshot(` + Array [ + "foo", + ] + `); + expect(enums).toHaveLength(1); + expect(enums).toMatchInlineSnapshot(` +Array [ + Object { + "typname": "user_role", + "value_count": "2", + }, +] +`); + } +}); diff --git a/__tests__/watch.test.ts b/__tests__/watch.test.ts index df5f085..17973e1 100644 --- a/__tests__/watch.test.ts +++ b/__tests__/watch.test.ts @@ -30,6 +30,7 @@ it("doesn't run current.sql if it's already up to date", async () => { parsedSettings, false, false, + false, ); expect(getActionCalls()).toEqual([]); @@ -81,6 +82,7 @@ it("watches symlinked files", async () => { parsedSettings, false, false, + false, ); expect(getActionCalls()).toEqual([]); diff --git a/src/cli.ts b/src/cli.ts index 6d8f975..98c50cd 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,6 +6,7 @@ import * as yargs from "yargs"; import { version } from "../package.json"; import { commitCommand } from "./commands/commit"; import { compileCommand } from "./commands/compile"; +import { currentCommand } from "./commands/current"; import { initCommand } from "./commands/init"; import { migrateCommand } from "./commands/migrate"; import { resetCommand } from "./commands/reset"; @@ -76,6 +77,7 @@ yargs .command(wrapHandler(statusCommand)) .command(wrapHandler(resetCommand)) .command(wrapHandler(compileCommand)) + .command(wrapHandler(currentCommand)) .command(wrapHandler(runCommand)) // Make sure options added here are represented in CommonArgv diff --git a/src/commands/current.ts b/src/commands/current.ts new file mode 100644 index 0000000..0197842 --- /dev/null +++ b/src/commands/current.ts @@ -0,0 +1,73 @@ +import { CommandModule } from "yargs"; + +import { getCurrentMigrationLocation, writeCurrentMigration } from "../current"; +import { ParsedSettings, parseSettings, Settings } from "../settings"; +import { CommonArgv, getSettings } from "./_common"; +import { _migrate } from "./migrate"; +import { _makeCurrentMigrationRunner } from "./watch"; + +interface CurrentArgv extends CommonArgv { + shadow: boolean; + forceActions: boolean; +} + +export async function _current( + parsedSettings: ParsedSettings, + shadow = false, + forceActions = false, +): Promise { + await _migrate(parsedSettings, shadow); + + const currentLocation = await getCurrentMigrationLocation(parsedSettings); + if (!currentLocation.exists) { + await writeCurrentMigration( + parsedSettings, + currentLocation, + parsedSettings.blankMigrationContent.trim() + "\n", + ); + } + + const run = _makeCurrentMigrationRunner( + parsedSettings, + false, + shadow, + forceActions, + ); + return run(); +} + +export async function current( + settings: Settings, + shadow = false, + forceActions = false, +): Promise { + const parsedSettings = await parseSettings(settings, shadow); + return _current(parsedSettings, shadow, forceActions); +} + +export const currentCommand: CommandModule = { + command: "current", + aliases: [], + describe: + "Runs any un-executed committed migrations, as well as the current migration. For development.", + builder: { + shadow: { + type: "boolean", + default: false, + description: "Apply migrations to the shadow DB (for development).", + }, + forceActions: { + type: "boolean", + default: false, + description: + "Run beforeAllMigrations and afterAllMigrations actions even if no migration was necessary.", + }, + }, + handler: async argv => { + await current( + await getSettings({ configFile: argv.config }), + argv.shadow, + argv.forceActions, + ); + }, +}; diff --git a/src/commands/index.ts b/src/commands/index.ts index 725c5cd..8821ec6 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -7,3 +7,4 @@ export { status } from "./status"; export { reset } from "./reset"; export { compile } from "./compile"; export { run } from "./run"; +export { current } from "./current"; diff --git a/src/commands/watch.ts b/src/commands/watch.ts index ba52642..1e2ca36 100644 --- a/src/commands/watch.ts +++ b/src/commands/watch.ts @@ -25,6 +25,7 @@ export function _makeCurrentMigrationRunner( parsedSettings: ParsedSettings, _once = false, shadow = false, + forceActions = false, ): () => Promise { async function run(): Promise { const currentLocation = await getCurrentMigrationLocation(parsedSettings); @@ -85,7 +86,7 @@ export function _makeCurrentMigrationRunner( currentBodyMinified === previousBodyMinified; // 4: if different - if (!migrationsAreEquivalent) { + if (forceActions || !migrationsAreEquivalent) { await executeActions( parsedSettings, shadow, @@ -185,7 +186,7 @@ export async function _watch( ); } - const run = _makeCurrentMigrationRunner(parsedSettings, once, shadow); + const run = _makeCurrentMigrationRunner(parsedSettings, once, shadow, false); if (once) { return run(); } else {