@prisma-next/cli 0.7.0 → 0.8.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -8
- package/dist/{cli-errors-D3_sMh2K.mjs → cli-errors-CF60g2cG.mjs} +40 -2
- package/dist/cli-errors-CF60g2cG.mjs.map +1 -0
- package/dist/cli.mjs +70 -21
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-BCnP7cHo.mjs → client-XkUw4xD0.mjs} +17 -13
- package/dist/client-XkUw4xD0.mjs.map +1 -0
- package/dist/{command-helpers-BeZHkxV8.mjs → command-helpers-D3vL5yi8.mjs} +29 -6
- package/dist/command-helpers-D3vL5yi8.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +7 -7
- package/dist/commands/db-schema.mjs +5 -5
- package/dist/commands/db-sign.d.mts.map +1 -1
- package/dist/commands/db-sign.mjs +67 -25
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +37 -9
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +28 -0
- package/dist/commands/migrate.d.mts.map +1 -0
- package/dist/commands/{migration-apply.mjs → migrate.mjs} +54 -36
- package/dist/commands/migrate.mjs.map +1 -0
- package/dist/commands/migration-check.d.mts +18 -0
- package/dist/commands/migration-check.d.mts.map +1 -0
- package/dist/commands/migration-check.mjs +284 -0
- package/dist/commands/migration-check.mjs.map +1 -0
- package/dist/commands/migration-graph.d.mts +16 -0
- package/dist/commands/migration-graph.d.mts.map +1 -0
- package/dist/commands/migration-graph.mjs +141 -0
- package/dist/commands/migration-graph.mjs.map +1 -0
- package/dist/commands/migration-list.d.mts +20 -0
- package/dist/commands/migration-list.d.mts.map +1 -0
- package/dist/commands/migration-list.mjs +107 -0
- package/dist/commands/migration-list.mjs.map +1 -0
- package/dist/commands/migration-log.d.mts +21 -0
- package/dist/commands/migration-log.d.mts.map +1 -0
- package/dist/commands/migration-log.mjs +146 -0
- package/dist/commands/migration-log.mjs.map +1 -0
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +21 -20
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +2 -2
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +85 -47
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +3 -15
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +732 -1
- package/dist/commands/migration-status.mjs.map +1 -0
- package/dist/commands/ref.d.mts +34 -0
- package/dist/commands/ref.d.mts.map +1 -0
- package/dist/commands/{migration-ref.mjs → ref.mjs} +28 -57
- package/dist/commands/ref.mjs.map +1 -0
- package/dist/{contract-emit-B77TsJqf.mjs → contract-emit-CgoFk9AU.mjs} +8 -4
- package/dist/contract-emit-CgoFk9AU.mjs.map +1 -0
- package/dist/{contract-emit-9DBda5Ou.mjs → contract-emit-GpxW5RLe.mjs} +6 -6
- package/dist/{contract-emit-9DBda5Ou.mjs.map → contract-emit-GpxW5RLe.mjs.map} +1 -1
- package/dist/{contract-infer-ByxhPjpW.mjs → contract-infer-D8edZOCi.mjs} +5 -5
- package/dist/{contract-infer-ByxhPjpW.mjs.map → contract-infer-D8edZOCi.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-BrwKK6Q6.mjs → contract-space-aggregate-loader-D68YpuPR.mjs} +3 -3
- package/dist/{contract-space-aggregate-loader-BrwKK6Q6.mjs.map → contract-space-aggregate-loader-D68YpuPR.mjs.map} +1 -1
- package/dist/{db-verify-Czm5T-J4.mjs → db-verify-DtRB9iHJ.mjs} +7 -7
- package/dist/{db-verify-Czm5T-J4.mjs.map → db-verify-DtRB9iHJ.mjs.map} +1 -1
- package/dist/errors-Cw6kyTyV.mjs +56 -0
- package/dist/errors-Cw6kyTyV.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +1 -1
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +2 -2
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{framework-components-ChqVUxR-.mjs → framework-components-xFLFpZUO.mjs} +2 -2
- package/dist/{framework-components-ChqVUxR-.mjs.map → framework-components-xFLFpZUO.mjs.map} +1 -1
- package/dist/{global-flags-Icqpxk23.d.mts → global-flags-DGmw6Kqg.d.mts} +1 -1
- package/dist/{global-flags-Icqpxk23.d.mts.map → global-flags-DGmw6Kqg.d.mts.map} +1 -1
- package/dist/{migration-status-By9G5p2H.mjs → graph-render-eJDcLWny.mjs} +3 -692
- package/dist/graph-render-eJDcLWny.mjs.map +1 -0
- package/dist/{init-BRKnARU6.mjs → init-Dm0QZPUA.mjs} +412 -208
- package/dist/init-Dm0QZPUA.mjs.map +1 -0
- package/dist/{inspect-live-schema-DxdBd4Er.mjs → inspect-live-schema-CPPqCips.mjs} +4 -4
- package/dist/{inspect-live-schema-DxdBd4Er.mjs.map → inspect-live-schema-CPPqCips.mjs.map} +1 -1
- package/dist/migration-cli.mjs +1 -1
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-BdV8JYXV.mjs → migration-command-scaffold-B_ezTTwX.mjs} +4 -4
- package/dist/{migration-command-scaffold-BdV8JYXV.mjs.map → migration-command-scaffold-B_ezTTwX.mjs.map} +1 -1
- package/dist/{migration-plan-mRu5K81L.mjs → migration-plan-DWB-NTxH.mjs} +62 -30
- package/dist/migration-plan-DWB-NTxH.mjs.map +1 -0
- package/dist/migration-types-D2FW63pr.d.mts +15 -0
- package/dist/migration-types-D2FW63pr.d.mts.map +1 -0
- package/dist/{migrations-CTsyBXCA.mjs → migrations-DyUf5lTt.mjs} +2 -2
- package/dist/migrations-DyUf5lTt.mjs.map +1 -0
- package/dist/{output-B16Kefzx.mjs → output-B60Gw5fu.mjs} +12 -11
- package/dist/{output-B16Kefzx.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
- package/dist/{result-handler-rmPVKIP2.mjs → result-handler-Bm_6dDYg.mjs} +2 -2
- package/dist/{result-handler-rmPVKIP2.mjs.map → result-handler-Bm_6dDYg.mjs.map} +1 -1
- package/dist/{terminal-ui-C_hFNbAn.mjs → terminal-ui-XtOQsqe9.mjs} +2 -54
- package/dist/terminal-ui-XtOQsqe9.mjs.map +1 -0
- package/dist/{types-LItU7E4l.d.mts → types-BS_wpjAY.d.mts} +2 -2
- package/dist/{types-LItU7E4l.d.mts.map → types-BS_wpjAY.d.mts.map} +1 -1
- package/dist/{verify-CiwNWM9N.mjs → verify-D7ypCCe6.mjs} +1 -1
- package/dist/{verify-CiwNWM9N.mjs.map → verify-D7ypCCe6.mjs.map} +1 -1
- package/package.json +41 -25
- package/src/cli.ts +78 -15
- package/src/commands/db-sign.ts +102 -32
- package/src/commands/db-update.ts +56 -4
- package/src/commands/init/agent-skill-install.ts +232 -0
- package/src/commands/init/errors.ts +32 -0
- package/src/commands/init/exit-codes.ts +10 -0
- package/src/commands/init/index.ts +9 -1
- package/src/commands/init/init.ts +82 -7
- package/src/commands/init/inputs.ts +23 -0
- package/src/commands/init/output.ts +22 -17
- package/src/commands/init/templates/code-templates.ts +4 -1
- package/src/commands/{migration-apply.ts → migrate.ts} +54 -70
- package/src/commands/migration-check/exit-codes.ts +3 -0
- package/src/commands/migration-check.ts +369 -0
- package/src/commands/migration-graph.ts +184 -0
- package/src/commands/migration-list.ts +155 -0
- package/src/commands/migration-log.ts +218 -0
- package/src/commands/migration-new.ts +17 -9
- package/src/commands/migration-plan.ts +77 -27
- package/src/commands/migration-show.ts +132 -60
- package/src/commands/migration-status.ts +77 -64
- package/src/commands/{migration-ref.ts → ref.ts} +32 -86
- package/src/control-api/client.ts +11 -4
- package/src/control-api/operations/apply-aggregate.ts +4 -4
- package/src/control-api/operations/contract-emit.ts +13 -1
- package/src/control-api/operations/db-apply-aggregate.ts +3 -2
- package/src/control-api/operations/db-verify.ts +1 -1
- package/src/control-api/operations/migration-apply.ts +4 -3
- package/src/control-api/types.ts +1 -2
- package/src/migration-cli.ts +1 -1
- package/src/utils/cli-errors.ts +37 -0
- package/src/utils/command-helpers.ts +28 -3
- package/src/utils/contract-space-aggregate-loader.ts +2 -2
- package/src/utils/contract-space-seed-phase.ts +2 -2
- package/src/utils/formatters/help.ts +12 -2
- package/src/utils/formatters/migrations.ts +2 -2
- package/dist/agent-skill-mongo.md +0 -138
- package/dist/agent-skill-postgres.md +0 -106
- package/dist/cli-errors-D3_sMh2K.mjs.map +0 -1
- package/dist/client-BCnP7cHo.mjs.map +0 -1
- package/dist/command-helpers-BeZHkxV8.mjs.map +0 -1
- package/dist/commands/migration-apply.d.mts +0 -51
- package/dist/commands/migration-apply.d.mts.map +0 -1
- package/dist/commands/migration-apply.mjs.map +0 -1
- package/dist/commands/migration-ref.d.mts +0 -45
- package/dist/commands/migration-ref.d.mts.map +0 -1
- package/dist/commands/migration-ref.mjs.map +0 -1
- package/dist/contract-emit-B77TsJqf.mjs.map +0 -1
- package/dist/init-BRKnARU6.mjs.map +0 -1
- package/dist/migration-plan-mRu5K81L.mjs.map +0 -1
- package/dist/migration-status-By9G5p2H.mjs.map +0 -1
- package/dist/migrations-CTsyBXCA.mjs.map +0 -1
- package/dist/terminal-ui-C_hFNbAn.mjs.map +0 -1
- package/src/commands/init/templates/agent-skill-mongo.md +0 -138
- package/src/commands/init/templates/agent-skill-postgres.md +0 -106
- package/src/commands/init/templates/agent-skill.ts +0 -41
- /package/dist/{cli-errors-B9OBbled.d.mts → cli-errors-DdcjVLJV.d.mts} +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
|
|
2
|
+
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
3
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
4
|
+
import { findPath } from '@prisma-next/migration-tools/migration-graph';
|
|
5
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { loadConfig } from '../config-loader';
|
|
8
|
+
import {
|
|
9
|
+
type CliStructuredError,
|
|
10
|
+
errorUnexpected,
|
|
11
|
+
mapMigrationToolsError,
|
|
12
|
+
} from '../utils/cli-errors';
|
|
13
|
+
import {
|
|
14
|
+
addGlobalOptions,
|
|
15
|
+
loadMigrationPackages,
|
|
16
|
+
resolveMigrationPaths,
|
|
17
|
+
setCommandDescriptions,
|
|
18
|
+
setCommandExamples,
|
|
19
|
+
setCommandSeeAlso,
|
|
20
|
+
} from '../utils/command-helpers';
|
|
21
|
+
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
22
|
+
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
23
|
+
import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
|
|
24
|
+
import { handleResult } from '../utils/result-handler';
|
|
25
|
+
import { TerminalUI } from '../utils/terminal-ui';
|
|
26
|
+
|
|
27
|
+
interface MigrationListOptions extends CommonCommandOptions {
|
|
28
|
+
readonly config?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MigrationListEntry {
|
|
32
|
+
readonly dirName: string;
|
|
33
|
+
readonly from: string;
|
|
34
|
+
readonly to: string;
|
|
35
|
+
readonly migrationHash: string;
|
|
36
|
+
readonly operationCount: number;
|
|
37
|
+
readonly createdAt: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MigrationListResult {
|
|
41
|
+
readonly ok: true;
|
|
42
|
+
readonly migrations: readonly MigrationListEntry[];
|
|
43
|
+
readonly summary: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function executeMigrationListCommand(
|
|
47
|
+
options: MigrationListOptions,
|
|
48
|
+
flags: GlobalFlags,
|
|
49
|
+
ui: TerminalUI,
|
|
50
|
+
): Promise<Result<MigrationListResult, CliStructuredError>> {
|
|
51
|
+
const config = await loadConfig(options.config);
|
|
52
|
+
const { configPath, appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(
|
|
53
|
+
options.config,
|
|
54
|
+
config,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!flags.json && !flags.quiet) {
|
|
58
|
+
const header = formatStyledHeader({
|
|
59
|
+
command: 'migration list',
|
|
60
|
+
description: 'List on-disk migrations in topological order',
|
|
61
|
+
details: [
|
|
62
|
+
{ label: 'config', value: configPath },
|
|
63
|
+
{ label: 'migrations', value: appMigrationsRelative },
|
|
64
|
+
],
|
|
65
|
+
flags,
|
|
66
|
+
});
|
|
67
|
+
ui.stderr(header);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let bundles: Awaited<ReturnType<typeof loadMigrationPackages>>['bundles'];
|
|
71
|
+
let graph: Awaited<ReturnType<typeof loadMigrationPackages>>['graph'];
|
|
72
|
+
try {
|
|
73
|
+
({ bundles, graph } = await loadMigrationPackages(appMigrationsDir));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
|
|
76
|
+
return notOk(
|
|
77
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
78
|
+
why: `Failed to read migrations: ${error instanceof Error ? error.message : String(error)}`,
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (bundles.length === 0) {
|
|
84
|
+
return ok({ ok: true, migrations: [], summary: 'No migrations found' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const leaves = [...graph.nodes].filter(
|
|
88
|
+
(n) => !graph.forwardChain.has(n) || graph.forwardChain.get(n)!.length === 0,
|
|
89
|
+
);
|
|
90
|
+
const targetHash =
|
|
91
|
+
leaves.length === 1 ? leaves[0]! : ([...graph.nodes].values().next().value as string);
|
|
92
|
+
const chain = findPath(graph, EMPTY_CONTRACT_HASH, targetHash) ?? [];
|
|
93
|
+
|
|
94
|
+
const pkgByDirName = new Map(bundles.map((p) => [p.dirName, p]));
|
|
95
|
+
const entries: MigrationListEntry[] = chain.map((edge) => {
|
|
96
|
+
const pkg = pkgByDirName.get(edge.dirName);
|
|
97
|
+
const ops = (pkg?.ops ?? []) as readonly MigrationPlanOperation[];
|
|
98
|
+
return {
|
|
99
|
+
dirName: edge.dirName,
|
|
100
|
+
from: edge.from,
|
|
101
|
+
to: edge.to,
|
|
102
|
+
migrationHash: edge.migrationHash,
|
|
103
|
+
operationCount: ops.length,
|
|
104
|
+
createdAt: edge.createdAt,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return ok({
|
|
109
|
+
ok: true,
|
|
110
|
+
migrations: entries,
|
|
111
|
+
summary: `${entries.length} migration(s) on disk`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createMigrationListCommand(): Command {
|
|
116
|
+
const command = new Command('list');
|
|
117
|
+
setCommandDescriptions(
|
|
118
|
+
command,
|
|
119
|
+
'List on-disk migrations in topological order',
|
|
120
|
+
'Enumerates all migration packages under migrations/<space>/ in\n' +
|
|
121
|
+
'topological order. Offline — does not consult the database.',
|
|
122
|
+
);
|
|
123
|
+
setCommandExamples(command, ['prisma-next migration list']);
|
|
124
|
+
setCommandSeeAlso(command, [
|
|
125
|
+
{ verb: 'migration status', oneLiner: 'Show migration path and pending status' },
|
|
126
|
+
{ verb: 'migration log', oneLiner: 'Show executed migration history' },
|
|
127
|
+
{ verb: 'migration graph', oneLiner: 'Show the migration graph topology' },
|
|
128
|
+
{ verb: 'migration show', oneLiner: 'Display migration package contents' },
|
|
129
|
+
]);
|
|
130
|
+
addGlobalOptions(command)
|
|
131
|
+
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
132
|
+
.action(async (options: MigrationListOptions) => {
|
|
133
|
+
const flags = parseGlobalFlags(options);
|
|
134
|
+
const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
|
|
135
|
+
const result = await executeMigrationListCommand(options, flags, ui);
|
|
136
|
+
const exitCode = handleResult(result, flags, ui, (listResult) => {
|
|
137
|
+
if (flags.json) {
|
|
138
|
+
ui.output(JSON.stringify(listResult, null, 2));
|
|
139
|
+
} else if (!flags.quiet) {
|
|
140
|
+
if (listResult.migrations.length === 0) {
|
|
141
|
+
ui.log('No migrations found');
|
|
142
|
+
} else {
|
|
143
|
+
for (const entry of listResult.migrations) {
|
|
144
|
+
ui.log(
|
|
145
|
+
`${entry.dirName} ${entry.migrationHash.slice(0, 16)}… ${entry.operationCount} op(s)`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
ui.log(`\n${listResult.summary}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
process.exit(exitCode);
|
|
153
|
+
});
|
|
154
|
+
return command;
|
|
155
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
|
|
2
|
+
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
3
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
4
|
+
import { findPath } from '@prisma-next/migration-tools/migration-graph';
|
|
5
|
+
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
6
|
+
import { cyan, dim } from 'colorette';
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { loadConfig } from '../config-loader';
|
|
9
|
+
import { createControlClient } from '../control-api/client';
|
|
10
|
+
import {
|
|
11
|
+
type CliStructuredError,
|
|
12
|
+
errorDatabaseConnectionRequired,
|
|
13
|
+
errorDriverRequired,
|
|
14
|
+
errorUnexpected,
|
|
15
|
+
mapMigrationToolsError,
|
|
16
|
+
} from '../utils/cli-errors';
|
|
17
|
+
import {
|
|
18
|
+
addGlobalOptions,
|
|
19
|
+
loadMigrationPackages,
|
|
20
|
+
maskConnectionUrl,
|
|
21
|
+
resolveMigrationPaths,
|
|
22
|
+
setCommandDescriptions,
|
|
23
|
+
setCommandExamples,
|
|
24
|
+
setCommandSeeAlso,
|
|
25
|
+
targetSupportsMigrations,
|
|
26
|
+
} from '../utils/command-helpers';
|
|
27
|
+
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
28
|
+
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
29
|
+
import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
|
|
30
|
+
import { handleResult } from '../utils/result-handler';
|
|
31
|
+
import { TerminalUI } from '../utils/terminal-ui';
|
|
32
|
+
|
|
33
|
+
interface MigrationLogOptions extends CommonCommandOptions {
|
|
34
|
+
readonly db?: string;
|
|
35
|
+
readonly config?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MigrationLogEntry {
|
|
39
|
+
readonly dirName: string;
|
|
40
|
+
readonly from: string;
|
|
41
|
+
readonly to: string;
|
|
42
|
+
readonly migrationHash: string;
|
|
43
|
+
readonly operationCount: number;
|
|
44
|
+
readonly createdAt: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MigrationLogResult {
|
|
48
|
+
readonly ok: true;
|
|
49
|
+
readonly markerHash: string | null;
|
|
50
|
+
readonly applied: readonly MigrationLogEntry[];
|
|
51
|
+
readonly summary: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function executeMigrationLogCommand(
|
|
55
|
+
options: MigrationLogOptions,
|
|
56
|
+
flags: GlobalFlags,
|
|
57
|
+
ui: TerminalUI,
|
|
58
|
+
): Promise<Result<MigrationLogResult, CliStructuredError>> {
|
|
59
|
+
const config = await loadConfig(options.config);
|
|
60
|
+
const { configPath, appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(
|
|
61
|
+
options.config,
|
|
62
|
+
config,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const dbConnection = options.db ?? config.db?.connection;
|
|
66
|
+
if (!dbConnection) {
|
|
67
|
+
return notOk(
|
|
68
|
+
errorDatabaseConnectionRequired({
|
|
69
|
+
why: `Database connection is required for migration log (set db.connection in ${configPath}, or pass --db <url>)`,
|
|
70
|
+
commandName: 'migration log',
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (!config.driver) {
|
|
75
|
+
return notOk(errorDriverRequired({ why: 'Config.driver is required for migration log' }));
|
|
76
|
+
}
|
|
77
|
+
if (!targetSupportsMigrations(config.target)) {
|
|
78
|
+
return notOk(errorUnexpected('Target does not support migrations'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!flags.json && !flags.quiet) {
|
|
82
|
+
const header = formatStyledHeader({
|
|
83
|
+
command: 'migration log',
|
|
84
|
+
description: 'Show executed migration history',
|
|
85
|
+
details: [
|
|
86
|
+
{ label: 'config', value: configPath },
|
|
87
|
+
{ label: 'migrations', value: appMigrationsRelative },
|
|
88
|
+
...(typeof dbConnection === 'string'
|
|
89
|
+
? [{ label: 'database', value: maskConnectionUrl(dbConnection) }]
|
|
90
|
+
: []),
|
|
91
|
+
],
|
|
92
|
+
flags,
|
|
93
|
+
});
|
|
94
|
+
ui.stderr(header);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let bundles: Awaited<ReturnType<typeof loadMigrationPackages>>['bundles'];
|
|
98
|
+
let graph: Awaited<ReturnType<typeof loadMigrationPackages>>['graph'];
|
|
99
|
+
try {
|
|
100
|
+
({ bundles, graph } = await loadMigrationPackages(appMigrationsDir));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
|
|
103
|
+
return notOk(
|
|
104
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
105
|
+
why: `Failed to read migrations: ${error instanceof Error ? error.message : String(error)}`,
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const client = createControlClient({
|
|
111
|
+
family: config.family,
|
|
112
|
+
target: config.target,
|
|
113
|
+
adapter: config.adapter,
|
|
114
|
+
driver: config.driver,
|
|
115
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await client.connect(dbConnection);
|
|
120
|
+
const marker = await client.readMarker();
|
|
121
|
+
const markerHash = marker?.storageHash ?? null;
|
|
122
|
+
|
|
123
|
+
if (!markerHash) {
|
|
124
|
+
return ok({
|
|
125
|
+
ok: true,
|
|
126
|
+
markerHash: null,
|
|
127
|
+
applied: [],
|
|
128
|
+
summary: 'No migrations applied (database has no marker)',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const appliedPath = findPath(graph, EMPTY_CONTRACT_HASH, markerHash);
|
|
133
|
+
if (appliedPath === null) {
|
|
134
|
+
return notOk(
|
|
135
|
+
errorUnexpected('Database marker is not reachable from migration history', {
|
|
136
|
+
why: `Marker hash ${markerHash} is not reachable from the root of the on-disk migration graph.`,
|
|
137
|
+
fix: 'The database may have been migrated outside this project. Use `migration status` to inspect the current state.',
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const pkgByDirName = new Map(bundles.map((p) => [p.dirName, p]));
|
|
142
|
+
const entries: MigrationLogEntry[] = appliedPath.map((edge) => {
|
|
143
|
+
const pkg = pkgByDirName.get(edge.dirName);
|
|
144
|
+
const ops = (pkg?.ops ?? []) as readonly MigrationPlanOperation[];
|
|
145
|
+
return {
|
|
146
|
+
dirName: edge.dirName,
|
|
147
|
+
from: edge.from,
|
|
148
|
+
to: edge.to,
|
|
149
|
+
migrationHash: edge.migrationHash,
|
|
150
|
+
operationCount: ops.length,
|
|
151
|
+
createdAt: edge.createdAt,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return ok({
|
|
156
|
+
ok: true,
|
|
157
|
+
markerHash,
|
|
158
|
+
applied: entries,
|
|
159
|
+
summary: `${entries.length} migration(s) applied`,
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
|
|
163
|
+
return notOk(
|
|
164
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
165
|
+
why: `Failed to read migration log: ${error instanceof Error ? error.message : String(error)}`,
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
} finally {
|
|
169
|
+
await client.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function createMigrationLogCommand(): Command {
|
|
174
|
+
const command = new Command('log');
|
|
175
|
+
setCommandDescriptions(
|
|
176
|
+
command,
|
|
177
|
+
'Show executed migration history',
|
|
178
|
+
'Reads the database marker and displays the applied migration chain\n' +
|
|
179
|
+
'from the initial state to the current marker position.',
|
|
180
|
+
);
|
|
181
|
+
setCommandExamples(command, [
|
|
182
|
+
'prisma-next migration log --db $DATABASE_URL',
|
|
183
|
+
'prisma-next migration log --json --db $DATABASE_URL',
|
|
184
|
+
]);
|
|
185
|
+
setCommandSeeAlso(command, [
|
|
186
|
+
{ verb: 'migration status', oneLiner: 'Show migration path and pending status' },
|
|
187
|
+
{ verb: 'migration list', oneLiner: 'List on-disk migrations' },
|
|
188
|
+
{ verb: 'migration graph', oneLiner: 'Show the migration graph topology' },
|
|
189
|
+
{ verb: 'migration show', oneLiner: 'Display migration package contents' },
|
|
190
|
+
]);
|
|
191
|
+
addGlobalOptions(command)
|
|
192
|
+
.option('--db <url>', 'Database connection string')
|
|
193
|
+
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
194
|
+
.action(async (options: MigrationLogOptions) => {
|
|
195
|
+
const flags = parseGlobalFlags(options);
|
|
196
|
+
const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
|
|
197
|
+
const result = await executeMigrationLogCommand(options, flags, ui);
|
|
198
|
+
const exitCode = handleResult(result, flags, ui, (logResult) => {
|
|
199
|
+
if (flags.json) {
|
|
200
|
+
ui.output(JSON.stringify(logResult, null, 2));
|
|
201
|
+
} else if (!flags.quiet) {
|
|
202
|
+
const c = (fn: (s: string) => string, s: string) => (flags.color !== false ? fn(s) : s);
|
|
203
|
+
if (logResult.applied.length === 0) {
|
|
204
|
+
ui.log(logResult.summary);
|
|
205
|
+
} else {
|
|
206
|
+
for (const entry of logResult.applied) {
|
|
207
|
+
ui.log(
|
|
208
|
+
`${c(cyan, '✓')} ${entry.dirName} ${c(dim, entry.migrationHash.slice(0, 16) + '…')} ${entry.operationCount} op(s)`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
ui.log(`\n${logResult.summary}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
process.exit(exitCode);
|
|
216
|
+
});
|
|
217
|
+
return command;
|
|
218
|
+
}
|
|
@@ -32,6 +32,7 @@ import { join, relative } from 'pathe';
|
|
|
32
32
|
import { loadConfig } from '../config-loader';
|
|
33
33
|
import {
|
|
34
34
|
CliStructuredError,
|
|
35
|
+
errorFileNotFound,
|
|
35
36
|
errorRuntime,
|
|
36
37
|
errorTargetMigrationNotSupported,
|
|
37
38
|
errorUnexpected,
|
|
@@ -115,7 +116,6 @@ async function executeMigrationNewCommand(
|
|
|
115
116
|
);
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
let fromContract: Contract | null = null;
|
|
119
119
|
let fromHash: string | null = null;
|
|
120
120
|
let fromContractSourceDir: string | null = null;
|
|
121
121
|
|
|
@@ -136,7 +136,6 @@ async function executeMigrationNewCommand(
|
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
fromHash = match.metadata.to;
|
|
139
|
-
fromContract = match.metadata.toContract;
|
|
140
139
|
fromContractSourceDir = match.dirPath;
|
|
141
140
|
} else {
|
|
142
141
|
const latestMigration = findLatestMigration(graph);
|
|
@@ -146,7 +145,6 @@ async function executeMigrationNewCommand(
|
|
|
146
145
|
(p) => p.metadata.migrationHash === latestMigration.migrationHash,
|
|
147
146
|
);
|
|
148
147
|
if (leafPkg) {
|
|
149
|
-
fromContract = leafPkg.metadata.toContract;
|
|
150
148
|
fromContractSourceDir = leafPkg.dirPath;
|
|
151
149
|
}
|
|
152
150
|
}
|
|
@@ -180,8 +178,6 @@ async function executeMigrationNewCommand(
|
|
|
180
178
|
const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
|
|
181
179
|
from: fromHash,
|
|
182
180
|
to: toStorageHash,
|
|
183
|
-
fromContract,
|
|
184
|
-
toContract: toContractJson,
|
|
185
181
|
hints: {
|
|
186
182
|
used: [],
|
|
187
183
|
applied: [],
|
|
@@ -222,10 +218,22 @@ async function executeMigrationNewCommand(
|
|
|
222
218
|
const sourceArtifacts = getEmittedArtifactPaths(
|
|
223
219
|
join(fromContractSourceDir, 'end-contract.json'),
|
|
224
220
|
);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
try {
|
|
222
|
+
await copyFilesWithRename(packageDir, [
|
|
223
|
+
{ sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
|
|
224
|
+
{ sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
|
|
225
|
+
]);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
228
|
+
return notOk(
|
|
229
|
+
errorFileNotFound(sourceArtifacts.jsonPath, {
|
|
230
|
+
why: `Predecessor migration is missing its destination contract snapshot at ${sourceArtifacts.jsonPath}`,
|
|
231
|
+
fix: 'Re-emit the predecessor migration (`prisma-next migration plan` from its source) so its sibling `end-contract.json` is restored, then re-run this command.',
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
const stack = createControlStack(config);
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
19
19
|
import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
|
|
20
20
|
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
21
|
+
import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
|
|
22
|
+
import { readRefs } from '@prisma-next/migration-tools/refs';
|
|
21
23
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
22
24
|
import { Command } from 'commander';
|
|
23
25
|
import { join, relative } from 'pathe';
|
|
@@ -28,10 +30,10 @@ import {
|
|
|
28
30
|
errorContractValidationFailed,
|
|
29
31
|
errorFileNotFound,
|
|
30
32
|
errorMigrationPlanningFailed,
|
|
31
|
-
errorRuntime,
|
|
32
33
|
errorTargetMigrationNotSupported,
|
|
33
34
|
errorUnexpected,
|
|
34
35
|
mapMigrationToolsError,
|
|
36
|
+
mapRefResolutionError,
|
|
35
37
|
} from '../utils/cli-errors';
|
|
36
38
|
import {
|
|
37
39
|
addGlobalOptions,
|
|
@@ -58,6 +60,34 @@ interface MigrationPlanOptions extends CommonCommandOptions {
|
|
|
58
60
|
readonly from?: string;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Load a predecessor migration's destination contract from its sibling
|
|
65
|
+
* `end-contract.json` on disk. The manifest no longer inlines the
|
|
66
|
+
* contract; the planner reads it from the canonical on-disk artefact
|
|
67
|
+
* authored by a previous `migration plan` run.
|
|
68
|
+
*
|
|
69
|
+
* Throws `CliStructuredError` with `errorFileNotFound` when the
|
|
70
|
+
* sibling file is missing — the user has likely deleted or never
|
|
71
|
+
* authored the snapshot, and the message names the file and points
|
|
72
|
+
* them at re-emitting from the source.
|
|
73
|
+
*/
|
|
74
|
+
async function readPredecessorEndContract(migrationDir: string): Promise<Contract> {
|
|
75
|
+
const path = join(migrationDir, 'end-contract.json');
|
|
76
|
+
let raw: string;
|
|
77
|
+
try {
|
|
78
|
+
raw = await readFile(path, 'utf-8');
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
81
|
+
throw errorFileNotFound(path, {
|
|
82
|
+
why: `Predecessor migration is missing its destination contract snapshot at ${path}`,
|
|
83
|
+
fix: 'Re-emit the predecessor migration (`prisma-next migration plan` from its source) so its sibling `end-contract.json` is restored, then re-run this command.',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
return JSON.parse(raw) as Contract;
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
export interface MigrationPlanResult {
|
|
62
92
|
readonly ok: boolean;
|
|
63
93
|
readonly noOp: boolean;
|
|
@@ -74,7 +104,7 @@ export interface MigrationPlanResult {
|
|
|
74
104
|
* Surfacing these in the result (rather than only via `ui.step` log
|
|
75
105
|
* lines) makes the cross-space side effect explicit to JSON consumers
|
|
76
106
|
* and the success-summary renderer — the same multi-space side effect
|
|
77
|
-
* that `
|
|
107
|
+
* that `migrate` will replay.
|
|
78
108
|
*/
|
|
79
109
|
readonly emittedExtensionDirs: readonly { readonly spaceId: string; readonly dirName: string }[];
|
|
80
110
|
readonly operations: readonly {
|
|
@@ -186,24 +216,26 @@ async function executeMigrationPlanCommand(
|
|
|
186
216
|
const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
|
|
187
217
|
|
|
188
218
|
if (options.from) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
219
|
+
const refs = await readRefs(resolveMigrationPaths(options.config, config).refsDir);
|
|
220
|
+
const refResult = parseContractRef(options.from, { graph, refs });
|
|
221
|
+
if (!refResult.ok) {
|
|
222
|
+
return notOk(mapRefResolutionError(refResult.failure));
|
|
223
|
+
}
|
|
224
|
+
fromHash = refResult.value.hash;
|
|
225
|
+
const matchingBundle = bundles.find((p) => p.metadata.to === fromHash);
|
|
226
|
+
if (!matchingBundle) {
|
|
192
227
|
return notOk(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
fix: 'Check that the --from hash matches a known migration target hash, or omit --from to use the latest migration target.',
|
|
201
|
-
}),
|
|
228
|
+
errorUnexpected(
|
|
229
|
+
`No migration bundle found for --from "${options.from}" (resolved hash: ${fromHash})`,
|
|
230
|
+
{
|
|
231
|
+
why: `The ref resolved successfully but no on-disk migration package has an end-contract hash matching ${fromHash}.`,
|
|
232
|
+
fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',
|
|
233
|
+
},
|
|
234
|
+
),
|
|
202
235
|
);
|
|
203
236
|
}
|
|
204
|
-
|
|
205
|
-
fromContract =
|
|
206
|
-
fromContractSourceDir = resolved.value.dirPath;
|
|
237
|
+
fromContractSourceDir = matchingBundle.dirPath;
|
|
238
|
+
fromContract = await readPredecessorEndContract(fromContractSourceDir);
|
|
207
239
|
} else {
|
|
208
240
|
const latestMigration = findLatestMigration(graph);
|
|
209
241
|
if (latestMigration) {
|
|
@@ -212,8 +244,8 @@ async function executeMigrationPlanCommand(
|
|
|
212
244
|
(p) => p.metadata.migrationHash === latestMigration.migrationHash,
|
|
213
245
|
);
|
|
214
246
|
if (leafPkg) {
|
|
215
|
-
fromContract = leafPkg.metadata.toContract;
|
|
216
247
|
fromContractSourceDir = leafPkg.dirPath;
|
|
248
|
+
fromContract = await readPredecessorEndContract(fromContractSourceDir);
|
|
217
249
|
}
|
|
218
250
|
}
|
|
219
251
|
}
|
|
@@ -221,6 +253,12 @@ async function executeMigrationPlanCommand(
|
|
|
221
253
|
if (MigrationToolsError.is(error)) {
|
|
222
254
|
return notOk(mapMigrationToolsError(error));
|
|
223
255
|
}
|
|
256
|
+
// `readPredecessorEndContract` raises a `CliStructuredError` directly
|
|
257
|
+
// for the missing-snapshot case so the operator gets a precise
|
|
258
|
+
// why/fix; pass it through unchanged rather than re-wrapping.
|
|
259
|
+
if (CliStructuredError.is(error)) {
|
|
260
|
+
return notOk(error);
|
|
261
|
+
}
|
|
224
262
|
// Wrap unexpected (non-MigrationToolsError) failures from the migration
|
|
225
263
|
// load phase in a structured CLI envelope. Letting them throw would
|
|
226
264
|
// bypass `handleResult()` and crash the command — see CLI structured-
|
|
@@ -289,10 +327,21 @@ async function executeMigrationPlanCommand(
|
|
|
289
327
|
// declaredButUnmigrated precheck always passes here.
|
|
290
328
|
const stack = createControlStack(config);
|
|
291
329
|
const familyInstance = config.family.create(stack);
|
|
330
|
+
let validatedAppContract: Contract;
|
|
331
|
+
try {
|
|
332
|
+
validatedAppContract = familyInstance.validateContract(toContractJson);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
return notOk(
|
|
335
|
+
errorContractValidationFailed(
|
|
336
|
+
`Contract validation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
337
|
+
{ where: { path: contractPathAbsolute } },
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
292
341
|
const aggregateResult = await buildContractSpaceAggregate({
|
|
293
342
|
targetId: config.target.targetId,
|
|
294
343
|
migrationsDir,
|
|
295
|
-
appContract:
|
|
344
|
+
appContract: validatedAppContract,
|
|
296
345
|
extensionPacks: config.extensionPacks ?? [],
|
|
297
346
|
validateContract: (json: unknown) => familyInstance.validateContract(json),
|
|
298
347
|
});
|
|
@@ -316,8 +365,6 @@ async function executeMigrationPlanCommand(
|
|
|
316
365
|
const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
|
|
317
366
|
from: fromHash,
|
|
318
367
|
to: toStorageHash,
|
|
319
|
-
fromContract,
|
|
320
|
-
toContract: toContractJson,
|
|
321
368
|
hints: {
|
|
322
369
|
used: [],
|
|
323
370
|
applied: [],
|
|
@@ -480,7 +527,10 @@ export function createMigrationPlanCommand(): Command {
|
|
|
480
527
|
addGlobalOptions(command)
|
|
481
528
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
482
529
|
.option('--name <slug>', 'Name slug for the migration directory', 'migration')
|
|
483
|
-
.option(
|
|
530
|
+
.option(
|
|
531
|
+
'--from <contract>',
|
|
532
|
+
'Starting contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
|
|
533
|
+
)
|
|
484
534
|
.action(async (options: MigrationPlanOptions) => {
|
|
485
535
|
const flags = parseGlobalFlags(options);
|
|
486
536
|
const startTime = Date.now();
|
|
@@ -546,7 +596,7 @@ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: Gl
|
|
|
546
596
|
}
|
|
547
597
|
lines.push('');
|
|
548
598
|
lines.push(
|
|
549
|
-
`Next: review the extension migrations above, then run ${green_('prisma-next
|
|
599
|
+
`Next: review the extension migrations above, then run ${green_('prisma-next migrate')}.`,
|
|
550
600
|
);
|
|
551
601
|
}
|
|
552
602
|
|
|
@@ -617,11 +667,11 @@ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: Gl
|
|
|
617
667
|
|
|
618
668
|
lines.push('');
|
|
619
669
|
// The "Next:" hint always points at the canonical apply path
|
|
620
|
-
// (`prisma-next
|
|
621
|
-
//
|
|
622
|
-
//
|
|
670
|
+
// (`prisma-next migrate`) regardless of how many spaces were
|
|
671
|
+
// materialised — `db update` is a dev-time convenience, not the
|
|
672
|
+
// canonical replay step.
|
|
623
673
|
lines.push(
|
|
624
|
-
`Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next
|
|
674
|
+
`Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migrate')}.`,
|
|
625
675
|
);
|
|
626
676
|
|
|
627
677
|
if (result.preview && result.preview.statements.length > 0) {
|