@prisma-next/cli 0.8.0-dev.7 → 0.8.0-dev.9
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 +66 -18
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-4d26awB-.mjs → client-XkUw4xD0.mjs} +10 -9
- 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.mjs +5 -5
- 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-DLc5GYbr.mjs → contract-emit-CgoFk9AU.mjs} +3 -3
- package/dist/{contract-emit-DLc5GYbr.mjs.map → contract-emit-CgoFk9AU.mjs.map} +1 -1
- package/dist/{contract-emit-BhKR-D9Y.mjs → contract-emit-GpxW5RLe.mjs} +6 -6
- package/dist/{contract-emit-BhKR-D9Y.mjs.map → contract-emit-GpxW5RLe.mjs.map} +1 -1
- package/dist/{contract-infer-Bnla2kuK.mjs → contract-infer-D8edZOCi.mjs} +5 -5
- package/dist/{contract-infer-Bnla2kuK.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-DitNxDiE.mjs → db-verify-DtRB9iHJ.mjs} +7 -7
- package/dist/{db-verify-DitNxDiE.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.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-Do4Ei0i_.mjs → graph-render-eJDcLWny.mjs} +3 -692
- package/dist/graph-render-eJDcLWny.mjs.map +1 -0
- package/dist/{init-CcDCJMLk.mjs → init-BU2G31T8.mjs} +7 -6
- package/dist/{init-CcDCJMLk.mjs.map → init-BU2G31T8.mjs.map} +1 -1
- package/dist/{inspect-live-schema-CyzAzPzF.mjs → inspect-live-schema-CPPqCips.mjs} +4 -4
- package/dist/{inspect-live-schema-CyzAzPzF.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-Jp1rosw8.mjs → migration-command-scaffold-B_ezTTwX.mjs} +4 -4
- package/dist/{migration-command-scaffold-Jp1rosw8.mjs.map → migration-command-scaffold-B_ezTTwX.mjs.map} +1 -1
- package/dist/{migration-plan-DTwYi61q.mjs → migration-plan-DWB-NTxH.mjs} +26 -24
- 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-DEg3SSnJ.mjs → output-B60Gw5fu.mjs} +1 -1
- package/dist/{output-DEg3SSnJ.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 +39 -23
- 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/{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-plan.ts +29 -22
- 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/operations/apply-aggregate.ts +4 -4
- package/src/control-api/operations/db-apply-aggregate.ts +3 -2
- package/src/control-api/operations/migration-apply.ts +4 -3
- 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 +1 -1
- package/src/utils/formatters/help.ts +12 -2
- package/src/utils/formatters/migrations.ts +2 -2
- package/dist/cli-errors-D3_sMh2K.mjs.map +0 -1
- package/dist/client-4d26awB-.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/migration-plan-DTwYi61q.mjs.map +0 -1
- package/dist/migration-status-Do4Ei0i_.mjs.map +0 -1
- package/dist/migrations-CTsyBXCA.mjs.map +0 -1
- package/dist/terminal-ui-C_hFNbAn.mjs.map +0 -1
- /package/dist/{cli-errors-B9OBbled.d.mts → cli-errors-DdcjVLJV.d.mts} +0 -0
package/src/commands/db-sign.ts
CHANGED
|
@@ -3,9 +3,12 @@ import type {
|
|
|
3
3
|
SignDatabaseResult,
|
|
4
4
|
VerifyDatabaseSchemaResult,
|
|
5
5
|
} from '@prisma-next/framework-components/control';
|
|
6
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
7
|
+
import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
|
|
8
|
+
import { readRefs } from '@prisma-next/migration-tools/refs';
|
|
6
9
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
7
10
|
import { Command } from 'commander';
|
|
8
|
-
import { relative, resolve } from 'pathe';
|
|
11
|
+
import { join, relative, resolve } from 'pathe';
|
|
9
12
|
import { loadConfig } from '../config-loader';
|
|
10
13
|
import { createControlClient } from '../control-api/client';
|
|
11
14
|
import { ContractValidationError } from '../control-api/errors';
|
|
@@ -15,12 +18,17 @@ import {
|
|
|
15
18
|
errorDatabaseConnectionRequired,
|
|
16
19
|
errorDriverRequired,
|
|
17
20
|
errorFileNotFound,
|
|
21
|
+
errorRuntime,
|
|
18
22
|
errorUnexpected,
|
|
23
|
+
mapMigrationToolsError,
|
|
24
|
+
mapRefResolutionError,
|
|
19
25
|
} from '../utils/cli-errors';
|
|
20
26
|
import {
|
|
21
27
|
addGlobalOptions,
|
|
28
|
+
loadMigrationPackages,
|
|
22
29
|
maskConnectionUrl,
|
|
23
30
|
resolveContractPath,
|
|
31
|
+
resolveMigrationPaths,
|
|
24
32
|
setCommandDescriptions,
|
|
25
33
|
setCommandExamples,
|
|
26
34
|
} from '../utils/command-helpers';
|
|
@@ -40,6 +48,7 @@ import { TerminalUI } from '../utils/terminal-ui';
|
|
|
40
48
|
interface DbSignOptions extends CommonCommandOptions {
|
|
41
49
|
readonly db?: string;
|
|
42
50
|
readonly config?: string;
|
|
51
|
+
readonly contract?: string;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
/**
|
|
@@ -54,11 +63,11 @@ type DbSignFailure = CliStructuredError | VerifyDatabaseSchemaResult;
|
|
|
54
63
|
* Failure: CliStructuredError (infra error) or VerifyDatabaseSchemaResult (schema mismatch)
|
|
55
64
|
*/
|
|
56
65
|
async function executeDbSignCommand(
|
|
66
|
+
contractArg: string | undefined,
|
|
57
67
|
options: DbSignOptions,
|
|
58
68
|
flags: GlobalFlags,
|
|
59
69
|
ui: TerminalUI,
|
|
60
70
|
): Promise<Result<SignDatabaseResult, DbSignFailure>> {
|
|
61
|
-
// Load config
|
|
62
71
|
const config = await loadConfig(options.config);
|
|
63
72
|
const configPath = options.config
|
|
64
73
|
? relative(process.cwd(), resolve(options.config))
|
|
@@ -66,11 +75,12 @@ async function executeDbSignCommand(
|
|
|
66
75
|
const contractPathAbsolute = resolveContractPath(config);
|
|
67
76
|
const contractPath = relative(process.cwd(), contractPathAbsolute);
|
|
68
77
|
|
|
69
|
-
|
|
78
|
+
const effectiveContractArg = contractArg ?? options.contract;
|
|
79
|
+
|
|
70
80
|
if (!flags.json && !flags.quiet) {
|
|
71
81
|
const details: Array<{ label: string; value: string }> = [
|
|
72
82
|
{ label: 'config', value: configPath },
|
|
73
|
-
{ label: 'contract', value: contractPath },
|
|
83
|
+
{ label: 'contract', value: effectiveContractArg ?? contractPath },
|
|
74
84
|
];
|
|
75
85
|
if (options.db) {
|
|
76
86
|
details.push({ label: 'database', value: maskConnectionUrl(options.db) });
|
|
@@ -85,36 +95,79 @@ async function executeDbSignCommand(
|
|
|
85
95
|
ui.stderr(header);
|
|
86
96
|
}
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
let contractJson: Record<string, unknown>;
|
|
99
|
+
|
|
100
|
+
if (effectiveContractArg) {
|
|
101
|
+
try {
|
|
102
|
+
const { appMigrationsDir, refsDir } = resolveMigrationPaths(options.config, config);
|
|
103
|
+
const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
|
|
104
|
+
const refs = await readRefs(refsDir);
|
|
105
|
+
const refResult = parseContractRef(effectiveContractArg, { graph, refs });
|
|
106
|
+
if (!refResult.ok) {
|
|
107
|
+
return notOk(mapRefResolutionError(refResult.failure));
|
|
108
|
+
}
|
|
109
|
+
const targetHash = refResult.value.hash;
|
|
110
|
+
const matchingBundle = bundles.find((p) => p.metadata.to === targetHash);
|
|
111
|
+
if (matchingBundle) {
|
|
112
|
+
const endContractPath = join(matchingBundle.dirPath, 'end-contract.json');
|
|
113
|
+
const raw = await readFile(endContractPath, 'utf-8');
|
|
114
|
+
contractJson = JSON.parse(raw) as Record<string, unknown>;
|
|
115
|
+
} else {
|
|
116
|
+
const defaultRaw = await readFile(contractPathAbsolute, 'utf-8');
|
|
117
|
+
const defaultContract = JSON.parse(defaultRaw) as Record<string, unknown>;
|
|
118
|
+
const storageHash = (defaultContract['storage'] as Record<string, unknown> | undefined)?.[
|
|
119
|
+
'storageHash'
|
|
120
|
+
];
|
|
121
|
+
if (storageHash === targetHash) {
|
|
122
|
+
contractJson = defaultContract;
|
|
123
|
+
} else {
|
|
124
|
+
return notOk(
|
|
125
|
+
errorRuntime(`No contract file found for hash "${targetHash}"`, {
|
|
126
|
+
why: `Resolved contract reference "${effectiveContractArg}" to hash "${targetHash}" but no migration produces that hash and the emitted contract does not match.`,
|
|
127
|
+
fix: 'Ensure the target contract exists on disk — either as a migration endpoint or as the emitted contract.json.',
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
|
|
134
|
+
if (error instanceof CliStructuredError) return notOk(error);
|
|
94
135
|
return notOk(
|
|
95
|
-
|
|
96
|
-
why: `
|
|
97
|
-
|
|
136
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
137
|
+
why: `Failed to resolve contract reference: ${error instanceof Error ? error.message : String(error)}`,
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
let contractJsonContent: string;
|
|
143
|
+
try {
|
|
144
|
+
contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
147
|
+
return notOk(
|
|
148
|
+
errorFileNotFound(contractPathAbsolute, {
|
|
149
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
150
|
+
fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`,
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return notOk(
|
|
155
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
156
|
+
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,
|
|
98
157
|
}),
|
|
99
158
|
);
|
|
100
159
|
}
|
|
101
|
-
return notOk(
|
|
102
|
-
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
103
|
-
why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}`,
|
|
104
|
-
}),
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
160
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
161
|
+
try {
|
|
162
|
+
contractJson = JSON.parse(contractJsonContent) as Record<string, unknown>;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return notOk(
|
|
165
|
+
errorContractValidationFailed(
|
|
166
|
+
`Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
167
|
+
{ where: { path: contractPathAbsolute } },
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
118
171
|
}
|
|
119
172
|
|
|
120
173
|
// Resolve database connection (--db flag or config.db.connection)
|
|
@@ -203,16 +256,33 @@ export function createDbSignCommand(): Command {
|
|
|
203
256
|
'in CI/deployment pipelines. The signature records that this database instance is aligned\n' +
|
|
204
257
|
'with a specific contract version.',
|
|
205
258
|
);
|
|
206
|
-
setCommandExamples(command, [
|
|
259
|
+
setCommandExamples(command, [
|
|
260
|
+
'prisma-next db sign --db $DATABASE_URL',
|
|
261
|
+
'prisma-next db sign production --db $DATABASE_URL',
|
|
262
|
+
'prisma-next db sign --contract production --db $DATABASE_URL',
|
|
263
|
+
]);
|
|
207
264
|
addGlobalOptions(command)
|
|
265
|
+
.argument('[contract]', 'Contract reference (hash, prefix, ref name, or migration dir name)')
|
|
208
266
|
.option('--db <url>', 'Database connection string')
|
|
209
267
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
210
|
-
.
|
|
268
|
+
.option(
|
|
269
|
+
'--contract <contract>',
|
|
270
|
+
'Contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
|
|
271
|
+
)
|
|
272
|
+
.action(async (positionalContract: string | undefined, options: DbSignOptions) => {
|
|
211
273
|
const flags = parseGlobalFlags(options);
|
|
212
274
|
|
|
275
|
+
if (positionalContract && options.contract) {
|
|
276
|
+
process.stderr.write(
|
|
277
|
+
'Cannot specify both a positional contract argument and --contract flag.\n',
|
|
278
|
+
);
|
|
279
|
+
process.exit(2);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
213
283
|
const ui = new TerminalUI({ color: flags.color, interactive: flags.interactive });
|
|
214
284
|
|
|
215
|
-
const result = await executeDbSignCommand(options, flags, ui);
|
|
285
|
+
const result = await executeDbSignCommand(positionalContract, options, flags, ui);
|
|
216
286
|
|
|
217
287
|
if (result.ok) {
|
|
218
288
|
// Success - format sign output
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
3
|
+
import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
|
|
4
|
+
import { readRefs } from '@prisma-next/migration-tools/refs';
|
|
1
5
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
6
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
3
7
|
import { Command } from 'commander';
|
|
8
|
+
import { join } from 'pathe';
|
|
4
9
|
import { ContractValidationError } from '../control-api/errors';
|
|
5
10
|
import type { DbUpdateFailure } from '../control-api/types';
|
|
6
11
|
import {
|
|
@@ -11,9 +16,12 @@ import {
|
|
|
11
16
|
errorMigrationPlanningFailed,
|
|
12
17
|
errorRunnerFailed,
|
|
13
18
|
errorUnexpected,
|
|
19
|
+
mapMigrationToolsError,
|
|
20
|
+
mapRefResolutionError,
|
|
14
21
|
} from '../utils/cli-errors';
|
|
15
22
|
import type { MigrationCommandOptions } from '../utils/command-helpers';
|
|
16
23
|
import {
|
|
24
|
+
loadMigrationPackages,
|
|
17
25
|
resolveMigrationPaths,
|
|
18
26
|
sanitizeErrorMessage,
|
|
19
27
|
setCommandDescriptions,
|
|
@@ -33,7 +41,9 @@ import {
|
|
|
33
41
|
import { handleResult } from '../utils/result-handler';
|
|
34
42
|
import { TerminalUI } from '../utils/terminal-ui';
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
interface DbUpdateOptions extends MigrationCommandOptions {
|
|
45
|
+
readonly to?: string;
|
|
46
|
+
}
|
|
37
47
|
|
|
38
48
|
/**
|
|
39
49
|
* Maps a DbUpdateFailure to a CliStructuredError for consistent error handling.
|
|
@@ -81,9 +91,47 @@ async function executeDbUpdateCommand(
|
|
|
81
91
|
if (!ctxResult.ok) {
|
|
82
92
|
return ctxResult;
|
|
83
93
|
}
|
|
84
|
-
const { client, config,
|
|
85
|
-
|
|
86
|
-
const { migrationsDir } = resolveMigrationPaths(
|
|
94
|
+
const { client, config, dbConnection, onProgress, contractPathAbsolute } = ctxResult.value;
|
|
95
|
+
let { contractJson } = ctxResult.value;
|
|
96
|
+
const { migrationsDir, appMigrationsDir, refsDir } = resolveMigrationPaths(
|
|
97
|
+
options.config,
|
|
98
|
+
config,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (options.to) {
|
|
102
|
+
try {
|
|
103
|
+
const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
|
|
104
|
+
const refs = await readRefs(refsDir);
|
|
105
|
+
const refResult = parseContractRef(options.to, { graph, refs });
|
|
106
|
+
if (!refResult.ok) {
|
|
107
|
+
return notOk(mapRefResolutionError(refResult.failure));
|
|
108
|
+
}
|
|
109
|
+
const targetHash = refResult.value.hash;
|
|
110
|
+
const matchingBundle = bundles.find((p) => p.metadata.to === targetHash);
|
|
111
|
+
if (!matchingBundle) {
|
|
112
|
+
return notOk(
|
|
113
|
+
errorUnexpected(
|
|
114
|
+
`No migration bundle found for --to "${options.to}" (resolved hash: ${targetHash})`,
|
|
115
|
+
{
|
|
116
|
+
why: `The ref resolved successfully but no on-disk migration package has an end-contract hash matching ${targetHash}.`,
|
|
117
|
+
fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const endContractPath = join(matchingBundle.dirPath, 'end-contract.json');
|
|
123
|
+
const raw = await readFile(endContractPath, 'utf-8');
|
|
124
|
+
contractJson = JSON.parse(raw) as Record<string, unknown>;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (MigrationToolsError.is(error)) {
|
|
127
|
+
return notOk(mapMigrationToolsError(error));
|
|
128
|
+
}
|
|
129
|
+
if (CliStructuredError.is(error)) {
|
|
130
|
+
return notOk(error);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
87
135
|
|
|
88
136
|
try {
|
|
89
137
|
await client.connect(dbConnection);
|
|
@@ -185,6 +233,10 @@ export function createDbUpdateCommand(): Command {
|
|
|
185
233
|
'prisma-next db update --db $DATABASE_URL --dry-run',
|
|
186
234
|
]);
|
|
187
235
|
addMigrationCommandOptions(command);
|
|
236
|
+
command.option(
|
|
237
|
+
'--to <contract>',
|
|
238
|
+
'Target contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
|
|
239
|
+
);
|
|
188
240
|
command.action(async (options: DbUpdateOptions) => {
|
|
189
241
|
const flags = parseGlobalFlags(options);
|
|
190
242
|
const startTime = Date.now();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
3
|
import { errorUnknownInvariant, MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
4
|
+
import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
|
|
4
5
|
import type { RefEntry } from '@prisma-next/migration-tools/refs';
|
|
5
|
-
import { readRefs
|
|
6
|
+
import { readRefs } from '@prisma-next/migration-tools/refs';
|
|
6
7
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
7
8
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
8
9
|
import { Command } from 'commander';
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
errorTargetMigrationNotSupported,
|
|
26
27
|
errorUnexpected,
|
|
27
28
|
mapMigrationToolsError,
|
|
29
|
+
mapRefResolutionError,
|
|
28
30
|
} from '../utils/cli-errors';
|
|
29
31
|
import {
|
|
30
32
|
addGlobalOptions,
|
|
@@ -44,29 +46,16 @@ import { type GlobalFlags, parseGlobalFlags } from '../utils/global-flags';
|
|
|
44
46
|
import { handleResult } from '../utils/result-handler';
|
|
45
47
|
import { TerminalUI } from '../utils/terminal-ui';
|
|
46
48
|
|
|
47
|
-
interface
|
|
49
|
+
interface MigrateCommandOptions extends CommonCommandOptions {
|
|
48
50
|
readonly db?: string;
|
|
49
51
|
readonly config?: string;
|
|
50
|
-
readonly
|
|
52
|
+
readonly to?: string;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
* Per-space breakdown of an apply run. The CLI command surfaces these
|
|
55
|
-
* for both the JSON shape (`appliedSpaces[]`) and the human-readable
|
|
56
|
-
* formatter (per-space block — same shape `db init` / `db update`
|
|
57
|
-
* use, M6 sub-spec § Output shape contract).
|
|
58
|
-
*/
|
|
59
|
-
export interface MigrationApplyResult {
|
|
55
|
+
export interface MigrateResult {
|
|
60
56
|
readonly ok: boolean;
|
|
61
|
-
/** Number of contract spaces that had non-zero pending operations applied. */
|
|
62
57
|
readonly migrationsApplied: number;
|
|
63
|
-
/** Total contract spaces visible in the aggregate (pending + already-up-to-date). */
|
|
64
58
|
readonly migrationsTotal: number;
|
|
65
|
-
/**
|
|
66
|
-
* Marker hash for the **app member** post-apply. Surfaced for
|
|
67
|
-
* back-compat with single-space callers; per-space markers live on
|
|
68
|
-
* `perSpace[].marker.storageHash`.
|
|
69
|
-
*/
|
|
70
59
|
readonly markerHash: string;
|
|
71
60
|
readonly applied: readonly {
|
|
72
61
|
readonly spaceId: string;
|
|
@@ -77,17 +66,7 @@ export interface MigrationApplyResult {
|
|
|
77
66
|
readonly operationsExecuted: number;
|
|
78
67
|
}[];
|
|
79
68
|
readonly summary: string;
|
|
80
|
-
/**
|
|
81
|
-
* Per-space breakdown in canonical schedule order (extensions
|
|
82
|
-
* alphabetically, then app). Always present for the aggregate-walking
|
|
83
|
-
* apply path.
|
|
84
|
-
*/
|
|
85
69
|
readonly perSpace: readonly AggregatePerSpaceExecutionEntry[];
|
|
86
|
-
/**
|
|
87
|
-
* Path-decision data for the app member. Surfaced for back-compat
|
|
88
|
-
* with single-space callers (cli-journeys invariant tests).
|
|
89
|
-
* Absent for no-op applies where the app had nothing to do.
|
|
90
|
-
*/
|
|
91
70
|
readonly pathDecision?: MigrationApplyPathDecision;
|
|
92
71
|
readonly timings: {
|
|
93
72
|
readonly total: number;
|
|
@@ -97,17 +76,17 @@ export interface MigrationApplyResult {
|
|
|
97
76
|
function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType {
|
|
98
77
|
return errorRuntime(failure.summary, {
|
|
99
78
|
why: failure.why ?? 'Migration runner failed',
|
|
100
|
-
fix: 'Fix the issue and re-run `prisma-next
|
|
79
|
+
fix: 'Fix the issue and re-run `prisma-next migrate --to <contract>` — previously applied migrations are preserved.',
|
|
101
80
|
meta: failure.meta ?? {},
|
|
102
81
|
});
|
|
103
82
|
}
|
|
104
83
|
|
|
105
|
-
async function
|
|
106
|
-
options:
|
|
84
|
+
async function executeMigrateCommand(
|
|
85
|
+
options: MigrateCommandOptions,
|
|
107
86
|
flags: GlobalFlags,
|
|
108
87
|
ui: TerminalUI,
|
|
109
88
|
startTime: number,
|
|
110
|
-
): Promise<Result<
|
|
89
|
+
): Promise<Result<MigrateResult, CliStructuredErrorType>> {
|
|
111
90
|
const config = await loadConfig(options.config);
|
|
112
91
|
const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative, refsDir } =
|
|
113
92
|
resolveMigrationPaths(options.config, config);
|
|
@@ -116,8 +95,8 @@ async function executeMigrationApplyCommand(
|
|
|
116
95
|
if (!dbConnection) {
|
|
117
96
|
return notOk(
|
|
118
97
|
errorDatabaseConnectionRequired({
|
|
119
|
-
why: `Database connection is required for
|
|
120
|
-
commandName: '
|
|
98
|
+
why: `Database connection is required for migrate (set db.connection in ${configPath}, or pass --db <url>)`,
|
|
99
|
+
commandName: 'migrate',
|
|
121
100
|
}),
|
|
122
101
|
);
|
|
123
102
|
}
|
|
@@ -125,7 +104,7 @@ async function executeMigrationApplyCommand(
|
|
|
125
104
|
if (!config.driver) {
|
|
126
105
|
return notOk(
|
|
127
106
|
errorDriverRequired({
|
|
128
|
-
why: 'Config.driver is required for
|
|
107
|
+
why: 'Config.driver is required for migrate',
|
|
129
108
|
}),
|
|
130
109
|
);
|
|
131
110
|
}
|
|
@@ -139,12 +118,22 @@ async function executeMigrationApplyCommand(
|
|
|
139
118
|
}
|
|
140
119
|
|
|
141
120
|
let refEntry: RefEntry | undefined;
|
|
142
|
-
const
|
|
121
|
+
const toArg = options.to;
|
|
143
122
|
|
|
144
|
-
if (
|
|
123
|
+
if (toArg) {
|
|
145
124
|
try {
|
|
146
125
|
const refs = await readRefs(refsDir);
|
|
147
|
-
|
|
126
|
+
const { graph } = await loadMigrationPackages(appMigrationsDir);
|
|
127
|
+
const refResult = parseContractRef(toArg, { graph, refs });
|
|
128
|
+
if (!refResult.ok) {
|
|
129
|
+
return notOk(mapRefResolutionError(refResult.failure));
|
|
130
|
+
}
|
|
131
|
+
if (refResult.value.provenance.kind === 'ref') {
|
|
132
|
+
const resolved = refs[refResult.value.provenance.refName];
|
|
133
|
+
if (resolved) refEntry = resolved;
|
|
134
|
+
} else {
|
|
135
|
+
refEntry = { hash: refResult.value.hash, invariants: [] };
|
|
136
|
+
}
|
|
148
137
|
} catch (error) {
|
|
149
138
|
if (MigrationToolsError.is(error)) {
|
|
150
139
|
return notOk(mapMigrationToolsError(error));
|
|
@@ -153,8 +142,6 @@ async function executeMigrationApplyCommand(
|
|
|
153
142
|
}
|
|
154
143
|
}
|
|
155
144
|
|
|
156
|
-
// Resolve and parse the contract envelope. The aggregate-walking
|
|
157
|
-
// operation needs the validated app contract to load the aggregate.
|
|
158
145
|
const contractPathAbsolute = resolveContractPath(config);
|
|
159
146
|
let contractRaw: Contract;
|
|
160
147
|
try {
|
|
@@ -165,7 +152,7 @@ async function executeMigrationApplyCommand(
|
|
|
165
152
|
return notOk(
|
|
166
153
|
errorFileNotFound(contractPathAbsolute, {
|
|
167
154
|
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
168
|
-
fix: 'Run `prisma-next contract emit` to generate a valid contract.json, then retry
|
|
155
|
+
fix: 'Run `prisma-next contract emit` to generate a valid contract.json, then retry.',
|
|
169
156
|
}),
|
|
170
157
|
);
|
|
171
158
|
}
|
|
@@ -188,21 +175,19 @@ async function executeMigrationApplyCommand(
|
|
|
188
175
|
value: maskConnectionUrl(dbConnection),
|
|
189
176
|
});
|
|
190
177
|
}
|
|
191
|
-
if (
|
|
192
|
-
details.push({ label: '
|
|
178
|
+
if (toArg) {
|
|
179
|
+
details.push({ label: 'to', value: toArg });
|
|
193
180
|
}
|
|
194
181
|
const header = formatStyledHeader({
|
|
195
|
-
command: '
|
|
196
|
-
description: 'Apply planned migrations to the database',
|
|
197
|
-
url: 'https://pris.ly/
|
|
182
|
+
command: 'migrate',
|
|
183
|
+
description: 'Apply planned migrations to advance the database',
|
|
184
|
+
url: 'https://pris.ly/migrate',
|
|
198
185
|
details,
|
|
199
186
|
flags,
|
|
200
187
|
});
|
|
201
188
|
ui.stderr(header);
|
|
202
189
|
}
|
|
203
190
|
|
|
204
|
-
// Load app-space migration packages — the aggregate operation
|
|
205
|
-
// needs them to hydrate the app member's graph for graph-walk.
|
|
206
191
|
let appPackages: Awaited<ReturnType<typeof loadMigrationPackages>>;
|
|
207
192
|
try {
|
|
208
193
|
appPackages = await loadMigrationPackages(appMigrationsDir);
|
|
@@ -224,13 +209,6 @@ async function executeMigrationApplyCommand(
|
|
|
224
209
|
try {
|
|
225
210
|
await client.connect(dbConnection);
|
|
226
211
|
|
|
227
|
-
// Pre-check unknown invariants against `(declared by app graph) ∪
|
|
228
|
-
// (already on the app marker)`. The marker side of the union
|
|
229
|
-
// catches the case where the ref carries an invariant whose
|
|
230
|
-
// declaring migration was retired (history rewritten) but whose
|
|
231
|
-
// id is recorded on the marker — surfacing UNKNOWN_INVARIANT
|
|
232
|
-
// there would be misleading because the database has already
|
|
233
|
-
// satisfied the requirement.
|
|
234
212
|
if (refEntry && refEntry.invariants.length > 0) {
|
|
235
213
|
const allMarkers = await client.readAllMarkers();
|
|
236
214
|
const appMarker = allMarkers.get('app') ?? null;
|
|
@@ -242,7 +220,7 @@ async function executeMigrationApplyCommand(
|
|
|
242
220
|
return notOk(
|
|
243
221
|
mapMigrationToolsError(
|
|
244
222
|
errorUnknownInvariant({
|
|
245
|
-
...ifDefined('refName',
|
|
223
|
+
...ifDefined('refName', toArg),
|
|
246
224
|
unknown,
|
|
247
225
|
declared: [...declared].sort(),
|
|
248
226
|
}),
|
|
@@ -261,7 +239,7 @@ async function executeMigrationApplyCommand(
|
|
|
261
239
|
appMigrationPackages: appPackages.bundles,
|
|
262
240
|
...ifDefined('refHash', refEntry?.hash),
|
|
263
241
|
...(refEntry?.invariants ? { refInvariants: refEntry.invariants } : {}),
|
|
264
|
-
...(refEntry !== undefined ? ifDefined('refName',
|
|
242
|
+
...(refEntry !== undefined ? ifDefined('refName', toArg) : {}),
|
|
265
243
|
});
|
|
266
244
|
|
|
267
245
|
if (!applyResult.ok) {
|
|
@@ -290,7 +268,7 @@ async function executeMigrationApplyCommand(
|
|
|
290
268
|
}
|
|
291
269
|
return notOk(
|
|
292
270
|
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
293
|
-
why: `Unexpected error during
|
|
271
|
+
why: `Unexpected error during migrate: ${error instanceof Error ? error.message : String(error)}`,
|
|
294
272
|
}),
|
|
295
273
|
);
|
|
296
274
|
} finally {
|
|
@@ -298,23 +276,29 @@ async function executeMigrationApplyCommand(
|
|
|
298
276
|
}
|
|
299
277
|
}
|
|
300
278
|
|
|
301
|
-
export function
|
|
302
|
-
const command = new Command('
|
|
279
|
+
export function createMigrateCommand(): Command {
|
|
280
|
+
const command = new Command('migrate');
|
|
303
281
|
setCommandDescriptions(
|
|
304
282
|
command,
|
|
305
|
-
'Apply planned migrations to the database',
|
|
283
|
+
'Apply planned migrations to advance the database',
|
|
306
284
|
'Walks every contract space (app + extensions) and applies pending\n' +
|
|
307
285
|
'on-disk migrations in canonical order (extensions alphabetically,\n' +
|
|
308
|
-
'then app). Graph-walks the on-disk migration graph for every space
|
|
309
|
-
|
|
310
|
-
"transaction; per-space failure rolls back every space's writes.",
|
|
286
|
+
'then app). Graph-walks the on-disk migration graph for every space.\n' +
|
|
287
|
+
'Use --to to target a specific contract (hash, ref name, migration dir).',
|
|
311
288
|
);
|
|
312
|
-
setCommandExamples(command, [
|
|
289
|
+
setCommandExamples(command, [
|
|
290
|
+
'prisma-next migrate --db $DATABASE_URL',
|
|
291
|
+
'prisma-next migrate --to production --db $DATABASE_URL',
|
|
292
|
+
'prisma-next migrate --to sha256:abc123 --db $DATABASE_URL',
|
|
293
|
+
]);
|
|
313
294
|
addGlobalOptions(command)
|
|
314
295
|
.option('--db <url>', 'Database connection string')
|
|
315
296
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
316
|
-
.option(
|
|
317
|
-
|
|
297
|
+
.option(
|
|
298
|
+
'--to <contract>',
|
|
299
|
+
'Target contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
|
|
300
|
+
)
|
|
301
|
+
.action(async (options: MigrateCommandOptions) => {
|
|
318
302
|
const flags = parseGlobalFlags(options);
|
|
319
303
|
const startTime = Date.now();
|
|
320
304
|
|
|
@@ -323,13 +307,13 @@ export function createMigrationApplyCommand(): Command {
|
|
|
323
307
|
interactive: flags.interactive,
|
|
324
308
|
});
|
|
325
309
|
|
|
326
|
-
const result = await
|
|
310
|
+
const result = await executeMigrateCommand(options, flags, ui, startTime);
|
|
327
311
|
|
|
328
|
-
const exitCode = handleResult(result, flags, ui, (
|
|
312
|
+
const exitCode = handleResult(result, flags, ui, (migrateResult) => {
|
|
329
313
|
if (flags.json) {
|
|
330
|
-
ui.output(JSON.stringify(
|
|
314
|
+
ui.output(JSON.stringify(migrateResult, null, 2));
|
|
331
315
|
} else if (!flags.quiet) {
|
|
332
|
-
ui.log(formatMigrationApplyCommandOutput(
|
|
316
|
+
ui.log(formatMigrationApplyCommandOutput(migrateResult, flags));
|
|
333
317
|
}
|
|
334
318
|
});
|
|
335
319
|
|