diff --git a/README.md b/README.md index 5f2aac1..9cb3122 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, as + well as the current migration. For + development. graphile-migrate commit Commits the current migration into the `committed/` folder, resetting the current migration. Resets the shadow database. @@ -284,12 +287,28 @@ migration, re-running it on any change. For development. Options: --help Show help [boolean] -c, --config Optional path to gmrc file [string] [default: .gmrc[.js|.cjs]] - --once Runs the current migration and then exits. - [boolean] [default: false] + --once Runs the current migration and then exits (equivalent to + `graphile-migrate current`). [boolean] [default: false] --shadow Applies changes to shadow DB. [boolean] [default: false] ``` +## graphile-migrate current + +``` +graphile-migrate current + +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). + [boolean] [default: false] +``` + + ## graphile-migrate commit ``` diff --git a/__tests__/current.test.ts b/__tests__/current.test.ts new file mode 100644 index 0000000..aa98590 --- /dev/null +++ b/__tests__/current.test.ts @@ -0,0 +1,136 @@ +import "./helpers"; // Has side-effects; must come first + +import 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, // Creates enum with 1 value + "migrations/current.sql": MIGRATION_NOTRX_TEXT, // Adds a value to the enum - total = 2 + }); + + await current(settings); + + const { migrations, tables, enums } = await getStuff(parsedSettings); + + expect(migrations).toHaveLength(2); + expect(migrations.map(({ date, ...rest }) => rest)).toMatchInlineSnapshot(` + [ + { + "filename": "000001.sql", + "hash": "sha1:e00ec93314a423ee5cc68d1182ad52f16442d7df", + "previous_hash": null, + }, + { + "filename": "000002.sql", + "hash": "sha1:bddc1ead3310dc1c42cdc7f63537ebdff2e9fd7b", + "previous_hash": "sha1:e00ec93314a423ee5cc68d1182ad52f16442d7df", + }, + ] + `); + expect(tables).toHaveLength(1); + expect(tables.map((t) => t.relname)).toMatchInlineSnapshot(` + [ + "foo", + ] + `); + expect(enums).toHaveLength(1); + expect(enums).toMatchInlineSnapshot(` + [ + { + "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: newMigrations, + tables: newTables, + enums: newEnums, + } = await getStuff(parsedSettings); + + expect(newMigrations).toHaveLength(3); + expect(newMigrations.map(({ date, ...rest }) => rest)).toMatchInlineSnapshot(` + [ + { + "filename": "000001.sql", + "hash": "sha1:e00ec93314a423ee5cc68d1182ad52f16442d7df", + "previous_hash": null, + }, + { + "filename": "000002.sql", + "hash": "sha1:bddc1ead3310dc1c42cdc7f63537ebdff2e9fd7b", + "previous_hash": "sha1:e00ec93314a423ee5cc68d1182ad52f16442d7df", + }, + { + "filename": "000003.sql", + "hash": "sha1:2d248344ac299ebbad2aeba5bfec2ae3c3cb0a4f", + "previous_hash": "sha1:bddc1ead3310dc1c42cdc7f63537ebdff2e9fd7b", + }, + ] + `); + expect(newTables).toEqual(tables); + expect(newEnums).toEqual(enums); +}); diff --git a/scripts/usage b/scripts/usage index 7b2addc..6cb6fe0 100755 --- a/scripts/usage +++ b/scripts/usage @@ -21,6 +21,10 @@ echo -e '## graphile-migrate watch\n\n```' $GRAPHILE_MIGRATE watch --help echo -e '```\n\n' +echo -e '## graphile-migrate current\n\n```' +$GRAPHILE_MIGRATE current --help +echo -e '```\n\n' + echo -e '## graphile-migrate commit\n\n```' $GRAPHILE_MIGRATE commit --help echo -e '```\n\n' diff --git a/src/cli.ts b/src/cli.ts index 347767d..cb48bc4 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,6 +3,7 @@ import * as yargs from "yargs"; 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"; @@ -67,6 +68,7 @@ const f = yargs .command(wrapHandler(initCommand)) .command(wrapHandler(migrateCommand)) .command(wrapHandler(watchCommand)) + .command(wrapHandler(currentCommand)) .command(wrapHandler(commitCommand)) .command(wrapHandler(uncommitCommand)) .command(wrapHandler(statusCommand)) diff --git a/src/commands/current.ts b/src/commands/current.ts new file mode 100644 index 0000000..29e4241 --- /dev/null +++ b/src/commands/current.ts @@ -0,0 +1,57 @@ +import { CommandModule } from "yargs"; + +import { getCurrentMigrationLocation, writeCurrentMigration } from "../current"; +import { makeCurrentMigrationRunner } from "../currentRunner"; +import { parseSettings, Settings } from "../settings"; +import type { CommonArgv } from "./_common"; +import { getSettings } from "./_common"; +import { _migrate } from "./migrate"; + +interface CurrentArgv extends CommonArgv { + shadow?: boolean; +} + +export async function current( + settings: Settings, + options: Partial = {}, +): Promise { + const { shadow = false } = options; + const parsedSettings = await parseSettings(settings, shadow); + await _migrate(parsedSettings, shadow); + + const currentLocation = await getCurrentMigrationLocation(parsedSettings); + if (!currentLocation.exists) { + await writeCurrentMigration( + parsedSettings, + currentLocation, + parsedSettings.blankMigrationContent.trim() + "\n", + ); + } + + const run = makeCurrentMigrationRunner(parsedSettings, { + once: true, + shadow, + }); + return run(); +} + +export const currentCommand: CommandModule< + Record, + CurrentArgv +> = { + 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).", + }, + }, + handler: async (argv) => { + const settings = await getSettings({ configFile: argv.config }); + await current(settings, argv); + }, +}; diff --git a/src/commands/index.ts b/src/commands/index.ts index c5eb4a8..732712e 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,5 +1,6 @@ export { commit } from "./commit"; export { compile } from "./compile"; +export { current } from "./current"; export { init } from "./init"; export { migrate } from "./migrate"; export { reset } from "./reset"; diff --git a/src/commands/run.ts b/src/commands/run.ts index 486b207..1dd8b27 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -24,15 +24,7 @@ export async function run( settings: Settings, rawContent: string, filename: string, - { - shadow = false, - root = false, - rootDatabase = false, - }: { - shadow?: boolean; - root?: boolean; - rootDatabase?: boolean; - } = {}, + { shadow = false, root = false, rootDatabase = false }: RunArgv = {}, ): Promise { const parsedSettings = await parseSettings(settings, shadow); const content = await compileIncludes( diff --git a/src/commands/watch.ts b/src/commands/watch.ts index b178b44..7034860 100644 --- a/src/commands/watch.ts +++ b/src/commands/watch.ts @@ -110,7 +110,8 @@ export const watchCommand: CommandModule, WatchArgv> = { once: { type: "boolean", default: false, - description: "Runs the current migration and then exits.", + description: + "Runs the current migration and then exits (equivalent to `graphile-migrate current`).", }, shadow: { type: "boolean",