diff --git a/README.md b/README.md index 77bdfe1f..db969d96 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ npm install -D @types/better-sqlite3 For Postgres: ```bash -npm install pg pg-connection-string +npm install pg npm install -D @types/pg ``` diff --git a/packages/cli/src/actions/migrate.ts b/packages/cli/src/actions/migrate.ts index 6a667a16..896cc991 100644 --- a/packages/cli/src/actions/migrate.ts +++ b/packages/cli/src/actions/migrate.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { CliError } from '../cli-error'; import { execPackage } from '../utils/exec-utils'; import { generateTempPrismaSchema, getSchemaFile } from './action-utils'; @@ -21,6 +22,11 @@ type DeployOptions = CommonOptions; type StatusOptions = CommonOptions; +type ResolveOptions = CommonOptions & { + applied?: string; + rolledBack?: string; +}; + /** * CLI action for migration-related commands */ @@ -46,6 +52,10 @@ export async function run(command: string, options: CommonOptions) { case 'status': await runStatus(prismaSchemaFile, options as StatusOptions); break; + + case 'resolve': + await runResolve(prismaSchemaFile, options as ResolveOptions); + break; } } finally { if (fs.existsSync(prismaSchemaFile)) { @@ -100,6 +110,25 @@ async function runStatus(prismaSchemaFile: string, _options: StatusOptions) { } } +async function runResolve(prismaSchemaFile: string, options: ResolveOptions) { + if (!options.applied && !options.rolledBack) { + throw new CliError('Either --applied or --rolled-back option must be provided'); + } + + try { + const cmd = [ + 'prisma migrate resolve', + ` --schema "${prismaSchemaFile}"`, + options.applied ? ` --applied ${options.applied}` : '', + options.rolledBack ? ` --rolled-back ${options.rolledBack}` : '', + ].join(''); + + await execPackage(cmd); + } catch (err) { + handleSubProcessError(err); + } +} + function handleSubProcessError(err: unknown) { if (err instanceof Error && 'status' in err && typeof err.status === 'number') { process.exit(err.status); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index fc154121..fd5ad01e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -95,9 +95,18 @@ export function createProgram() { .command('status') .addOption(schemaOption) .addOption(migrationsOption) - .description('check the status of your database migrations.') + .description('Check the status of your database migrations.') .action((options) => migrateAction('status', options)); + migrateCommand + .command('resolve') + .addOption(schemaOption) + .addOption(migrationsOption) + .addOption(new Option('--applied ', 'record a specific migration as applied')) + .addOption(new Option('--rolled-back ', 'record a specific migration as rolled back')) + .description('Resolve issues with database migrations in deployment databases') + .action((options) => migrateAction('resolve', options)); + const dbCommand = program.command('db').description('Manage your database schema during development.'); dbCommand diff --git a/packages/cli/test/migrate.test.ts b/packages/cli/test/migrate.test.ts index 85c2a928..56a0fec8 100644 --- a/packages/cli/test/migrate.test.ts +++ b/packages/cli/test/migrate.test.ts @@ -38,4 +38,35 @@ describe('CLI migrate commands test', () => { runCli('migrate dev --name init', workDir); runCli('migrate status', workDir); }); + + it('supports migrate resolve', () => { + const workDir = createProject(model); + runCli('migrate dev --name init', workDir); + + // find the migration record "timestamp_init" + const migrationRecords = fs.readdirSync(path.join(workDir, 'zenstack/migrations')); + const migration = migrationRecords.find((f) => f.endsWith('_init')); + + // force a migration failure + fs.writeFileSync(path.join(workDir, 'zenstack/migrations', migration!, 'migration.sql'), 'invalid content'); + + // redeploy the migration, which will fail + fs.rmSync(path.join(workDir, 'zenstack/dev.db'), { force: true }); + try { + runCli('migrate deploy', workDir); + } catch { + // noop + } + + // --rolled-back + runCli(`migrate resolve --rolled-back ${migration}`, workDir); + + // --applied + runCli(`migrate resolve --applied ${migration}`, workDir); + }); + + it('should throw error when neither applied nor rolled-back is provided', () => { + const workDir = createProject(model); + expect(() => runCli('migrate resolve', workDir)).toThrow(); + }); });