@prisma-next/cli 0.8.0 → 0.9.0-dev.2
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 +8 -9
- 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 +67 -19
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-BCnP7cHo.mjs → client-Brv4qlfB.mjs} +28 -30
- package/dist/client-Brv4qlfB.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.d.mts.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} +65 -39
- 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 +30 -29
- 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 +90 -52
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +5 -17
- 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-9DBda5Ou.mjs → contract-emit-C3STUIBg.mjs} +6 -6
- package/dist/{contract-emit-9DBda5Ou.mjs.map → contract-emit-C3STUIBg.mjs.map} +1 -1
- package/dist/{contract-emit-B77TsJqf.mjs → contract-emit-iynA3BCA.mjs} +9 -5
- package/dist/contract-emit-iynA3BCA.mjs.map +1 -0
- package/dist/{contract-infer-ByxhPjpW.mjs → contract-infer-Cnj8G1E2.mjs} +5 -5
- package/dist/{contract-infer-ByxhPjpW.mjs.map → contract-infer-Cnj8G1E2.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-BrwKK6Q6.mjs → contract-space-aggregate-loader-pAc8CDfY.mjs} +4 -4
- package/dist/{contract-space-aggregate-loader-BrwKK6Q6.mjs.map → contract-space-aggregate-loader-pAc8CDfY.mjs.map} +1 -1
- package/dist/{db-verify-Czm5T-J4.mjs → db-verify-D7cyH_zz.mjs} +12 -9
- package/dist/db-verify-D7cyH_zz.mjs.map +1 -0
- 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/index.mjs.map +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-B-k3a1Qw.mjs → init-DEe-IimY.mjs} +133 -61
- package/dist/init-DEe-IimY.mjs.map +1 -0
- package/dist/{inspect-live-schema-DxdBd4Er.mjs → inspect-live-schema-CWLK_lgs.mjs} +4 -4
- package/dist/{inspect-live-schema-DxdBd4Er.mjs.map → inspect-live-schema-CWLK_lgs.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-CmXXC1UZ.mjs} +4 -4
- package/dist/{migration-command-scaffold-BdV8JYXV.mjs.map → migration-command-scaffold-CmXXC1UZ.mjs.map} +1 -1
- package/dist/{migration-plan-mRu5K81L.mjs → migration-plan-CHyUlBV0.mjs} +76 -37
- package/dist/migration-plan-CHyUlBV0.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-BVj6a971.mjs → output-B60Gw5fu.mjs} +12 -11
- package/dist/{output-BVj6a971.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-0aS865QN.d.mts} +14 -8
- package/dist/types-0aS865QN.d.mts.map +1 -0
- 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/db-verify.ts +19 -3
- package/src/commands/init/agent-skill-install.ts +145 -43
- package/src/commands/init/errors.ts +2 -2
- package/src/commands/init/exit-codes.ts +2 -2
- package/src/commands/init/index.ts +1 -1
- package/src/commands/init/init.ts +15 -6
- package/src/commands/init/inputs.ts +1 -1
- package/src/commands/init/output.ts +22 -17
- package/src/commands/{migration-apply.ts → migrate.ts} +77 -73
- 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 +30 -22
- package/src/commands/migration-plan.ts +104 -35
- package/src/commands/migration-show.ts +141 -65
- package/src/commands/migration-status.ts +82 -69
- package/src/commands/{migration-ref.ts → ref.ts} +32 -86
- package/src/control-api/client.ts +30 -21
- package/src/control-api/operations/apply-aggregate.ts +4 -4
- package/src/control-api/operations/contract-emit.ts +26 -3
- package/src/control-api/operations/db-apply-aggregate.ts +4 -3
- package/src/control-api/operations/db-verify.ts +2 -2
- package/src/control-api/operations/migration-apply.ts +5 -4
- package/src/control-api/types.ts +12 -7
- package/src/load-ts-contract.ts +9 -1
- 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 +4 -4
- 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/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/db-verify-Czm5T-J4.mjs.map +0 -1
- package/dist/init-B-k3a1Qw.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/dist/types-LItU7E4l.d.mts.map +0 -1
- /package/dist/{cli-errors-B9OBbled.d.mts → cli-errors-DdcjVLJV.d.mts} +0 -0
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
3
|
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
4
4
|
import {
|
|
5
|
+
type ControlFamilyInstance,
|
|
5
6
|
createControlStack,
|
|
6
7
|
hasOperationPreview,
|
|
7
8
|
type MigrationPlanOperation,
|
|
@@ -18,6 +19,8 @@ import {
|
|
|
18
19
|
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
19
20
|
import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
|
|
20
21
|
import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
|
|
22
|
+
import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
|
|
23
|
+
import { readRefs } from '@prisma-next/migration-tools/refs';
|
|
21
24
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
22
25
|
import { Command } from 'commander';
|
|
23
26
|
import { join, relative } from 'pathe';
|
|
@@ -28,10 +31,10 @@ import {
|
|
|
28
31
|
errorContractValidationFailed,
|
|
29
32
|
errorFileNotFound,
|
|
30
33
|
errorMigrationPlanningFailed,
|
|
31
|
-
errorRuntime,
|
|
32
34
|
errorTargetMigrationNotSupported,
|
|
33
35
|
errorUnexpected,
|
|
34
36
|
mapMigrationToolsError,
|
|
37
|
+
mapRefResolutionError,
|
|
35
38
|
} from '../utils/cli-errors';
|
|
36
39
|
import {
|
|
37
40
|
addGlobalOptions,
|
|
@@ -58,6 +61,55 @@ interface MigrationPlanOptions extends CommonCommandOptions {
|
|
|
58
61
|
readonly from?: string;
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Load a predecessor migration's destination contract from its sibling
|
|
66
|
+
* `end-contract.json` on disk and route it through the family's
|
|
67
|
+
* `ContractSerializer` (via `deserializeContract`) so the in-memory shape
|
|
68
|
+
* is the hydrated `Contract` every other caller sees. Bypassing this
|
|
69
|
+
* seam was the root cause of TML-2536: a raw `JSON.parse(...) as Contract`
|
|
70
|
+
* here let polymorphic `storage.types` entries reach the planner without
|
|
71
|
+
* the `kind` discriminator the planner dispatches on.
|
|
72
|
+
*
|
|
73
|
+
* Throws `CliStructuredError` with:
|
|
74
|
+
* - `errorFileNotFound` when the sibling file is missing — the user
|
|
75
|
+
* has likely deleted or never authored the snapshot, and the
|
|
76
|
+
* message names the file and points them at re-emitting from the
|
|
77
|
+
* source.
|
|
78
|
+
* - `errorContractValidationFailed` when the JSON parses but the
|
|
79
|
+
* family deserializer rejects it (legacy untagged shape, structural
|
|
80
|
+
* mismatch, etc.) — the message names the predecessor's path so
|
|
81
|
+
* the operator can locate the bad snapshot.
|
|
82
|
+
*/
|
|
83
|
+
async function readPredecessorEndContract(
|
|
84
|
+
migrationDir: string,
|
|
85
|
+
familyInstance: ControlFamilyInstance<string, unknown>,
|
|
86
|
+
): Promise<Contract> {
|
|
87
|
+
const path = join(migrationDir, 'end-contract.json');
|
|
88
|
+
let raw: string;
|
|
89
|
+
try {
|
|
90
|
+
raw = await readFile(path, 'utf-8');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
93
|
+
throw errorFileNotFound(path, {
|
|
94
|
+
why: `Predecessor migration is missing its destination contract snapshot at ${path}`,
|
|
95
|
+
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.',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return familyInstance.deserializeContract(JSON.parse(raw) as unknown);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (CliStructuredError.is(error)) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
throw errorContractValidationFailed(
|
|
107
|
+
`Predecessor contract at ${path} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
|
|
108
|
+
{ where: { path } },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
61
113
|
export interface MigrationPlanResult {
|
|
62
114
|
readonly ok: boolean;
|
|
63
115
|
readonly noOp: boolean;
|
|
@@ -74,7 +126,7 @@ export interface MigrationPlanResult {
|
|
|
74
126
|
* Surfacing these in the result (rather than only via `ui.step` log
|
|
75
127
|
* lines) makes the cross-space side effect explicit to JSON consumers
|
|
76
128
|
* and the success-summary renderer — the same multi-space side effect
|
|
77
|
-
* that `
|
|
129
|
+
* that `migrate` will replay.
|
|
78
130
|
*/
|
|
79
131
|
readonly emittedExtensionDirs: readonly { readonly spaceId: string; readonly dirName: string }[];
|
|
80
132
|
readonly operations: readonly {
|
|
@@ -155,19 +207,26 @@ async function executeMigrationPlanCommand(
|
|
|
155
207
|
);
|
|
156
208
|
}
|
|
157
209
|
|
|
158
|
-
|
|
210
|
+
// Construct the family instance up-front so on-disk reads (the app
|
|
211
|
+
// contract here + every `readPredecessorEndContract` below) cross the
|
|
212
|
+
// serializer seam at the read site, not after the planner has already
|
|
213
|
+
// started dispatching on raw shapes. See TML-2536.
|
|
214
|
+
const stack = createControlStack(config);
|
|
215
|
+
const familyInstance = config.family.create(stack);
|
|
216
|
+
|
|
217
|
+
let toContract: Contract;
|
|
159
218
|
try {
|
|
160
|
-
|
|
219
|
+
toContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
|
|
161
220
|
} catch (error) {
|
|
162
221
|
return notOk(
|
|
163
222
|
errorContractValidationFailed(
|
|
164
|
-
`Contract
|
|
223
|
+
`Contract at ${contractPathAbsolute} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
|
|
165
224
|
{ where: { path: contractPathAbsolute } },
|
|
166
225
|
),
|
|
167
226
|
);
|
|
168
227
|
}
|
|
169
228
|
|
|
170
|
-
const rawStorageHash =
|
|
229
|
+
const rawStorageHash = toContract.storage?.storageHash;
|
|
171
230
|
if (typeof rawStorageHash !== 'string') {
|
|
172
231
|
return notOk(
|
|
173
232
|
errorContractValidationFailed('Contract is missing storageHash', {
|
|
@@ -186,24 +245,26 @@ async function executeMigrationPlanCommand(
|
|
|
186
245
|
const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
|
|
187
246
|
|
|
188
247
|
if (options.from) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
248
|
+
const refs = await readRefs(resolveMigrationPaths(options.config, config).refsDir);
|
|
249
|
+
const refResult = parseContractRef(options.from, { graph, refs });
|
|
250
|
+
if (!refResult.ok) {
|
|
251
|
+
return notOk(mapRefResolutionError(refResult.failure));
|
|
252
|
+
}
|
|
253
|
+
fromHash = refResult.value.hash;
|
|
254
|
+
const matchingBundle = bundles.find((p) => p.metadata.to === fromHash);
|
|
255
|
+
if (!matchingBundle) {
|
|
192
256
|
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
|
-
}),
|
|
257
|
+
errorUnexpected(
|
|
258
|
+
`No migration bundle found for --from "${options.from}" (resolved hash: ${fromHash})`,
|
|
259
|
+
{
|
|
260
|
+
why: `The ref resolved successfully but no on-disk migration package has an end-contract hash matching ${fromHash}.`,
|
|
261
|
+
fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',
|
|
262
|
+
},
|
|
263
|
+
),
|
|
202
264
|
);
|
|
203
265
|
}
|
|
204
|
-
|
|
205
|
-
fromContract =
|
|
206
|
-
fromContractSourceDir = resolved.value.dirPath;
|
|
266
|
+
fromContractSourceDir = matchingBundle.dirPath;
|
|
267
|
+
fromContract = await readPredecessorEndContract(fromContractSourceDir, familyInstance);
|
|
207
268
|
} else {
|
|
208
269
|
const latestMigration = findLatestMigration(graph);
|
|
209
270
|
if (latestMigration) {
|
|
@@ -212,8 +273,8 @@ async function executeMigrationPlanCommand(
|
|
|
212
273
|
(p) => p.metadata.migrationHash === latestMigration.migrationHash,
|
|
213
274
|
);
|
|
214
275
|
if (leafPkg) {
|
|
215
|
-
fromContract = leafPkg.metadata.toContract;
|
|
216
276
|
fromContractSourceDir = leafPkg.dirPath;
|
|
277
|
+
fromContract = await readPredecessorEndContract(fromContractSourceDir, familyInstance);
|
|
217
278
|
}
|
|
218
279
|
}
|
|
219
280
|
}
|
|
@@ -221,6 +282,12 @@ async function executeMigrationPlanCommand(
|
|
|
221
282
|
if (MigrationToolsError.is(error)) {
|
|
222
283
|
return notOk(mapMigrationToolsError(error));
|
|
223
284
|
}
|
|
285
|
+
// `readPredecessorEndContract` raises a `CliStructuredError` directly
|
|
286
|
+
// for the missing-snapshot case so the operator gets a precise
|
|
287
|
+
// why/fix; pass it through unchanged rather than re-wrapping.
|
|
288
|
+
if (CliStructuredError.is(error)) {
|
|
289
|
+
return notOk(error);
|
|
290
|
+
}
|
|
224
291
|
// Wrap unexpected (non-MigrationToolsError) failures from the migration
|
|
225
292
|
// load phase in a structured CLI envelope. Letting them throw would
|
|
226
293
|
// bypass `handleResult()` and crash the command — see CLI structured-
|
|
@@ -286,15 +353,16 @@ async function executeMigrationPlanCommand(
|
|
|
286
353
|
// Phase 2 — load: build the aggregate against the now-consistent disk
|
|
287
354
|
// state that phase 1 just seeded. The seed phase guarantees every
|
|
288
355
|
// declared extension has its head ref pinned, so the loader's
|
|
289
|
-
// declaredButUnmigrated precheck always passes here.
|
|
290
|
-
|
|
291
|
-
|
|
356
|
+
// declaredButUnmigrated precheck always passes here. The app contract
|
|
357
|
+
// was already routed through `familyInstance.deserializeContract` at the
|
|
358
|
+
// read site above (see TML-2536), so it's the hydrated `Contract`
|
|
359
|
+
// here — no second validation pass needed.
|
|
292
360
|
const aggregateResult = await buildContractSpaceAggregate({
|
|
293
361
|
targetId: config.target.targetId,
|
|
294
362
|
migrationsDir,
|
|
295
|
-
appContract:
|
|
363
|
+
appContract: toContract,
|
|
296
364
|
extensionPacks: config.extensionPacks ?? [],
|
|
297
|
-
|
|
365
|
+
deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
|
|
298
366
|
});
|
|
299
367
|
if (!aggregateResult.ok) {
|
|
300
368
|
return notOk(aggregateResult.failure);
|
|
@@ -316,8 +384,6 @@ async function executeMigrationPlanCommand(
|
|
|
316
384
|
const baseMetadata: Omit<MigrationMetadata, 'migrationHash' | 'providedInvariants'> = {
|
|
317
385
|
from: fromHash,
|
|
318
386
|
to: toStorageHash,
|
|
319
|
-
fromContract,
|
|
320
|
-
toContract: toContractJson,
|
|
321
387
|
hints: {
|
|
322
388
|
used: [],
|
|
323
389
|
applied: [],
|
|
@@ -480,7 +546,10 @@ export function createMigrationPlanCommand(): Command {
|
|
|
480
546
|
addGlobalOptions(command)
|
|
481
547
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
482
548
|
.option('--name <slug>', 'Name slug for the migration directory', 'migration')
|
|
483
|
-
.option(
|
|
549
|
+
.option(
|
|
550
|
+
'--from <contract>',
|
|
551
|
+
'Starting contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
|
|
552
|
+
)
|
|
484
553
|
.action(async (options: MigrationPlanOptions) => {
|
|
485
554
|
const flags = parseGlobalFlags(options);
|
|
486
555
|
const startTime = Date.now();
|
|
@@ -546,7 +615,7 @@ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: Gl
|
|
|
546
615
|
}
|
|
547
616
|
lines.push('');
|
|
548
617
|
lines.push(
|
|
549
|
-
`Next: review the extension migrations above, then run ${green_('prisma-next
|
|
618
|
+
`Next: review the extension migrations above, then run ${green_('prisma-next migrate')}.`,
|
|
550
619
|
);
|
|
551
620
|
}
|
|
552
621
|
|
|
@@ -617,11 +686,11 @@ export function formatMigrationPlanOutput(result: MigrationPlanResult, flags: Gl
|
|
|
617
686
|
|
|
618
687
|
lines.push('');
|
|
619
688
|
// The "Next:" hint always points at the canonical apply path
|
|
620
|
-
// (`prisma-next
|
|
621
|
-
//
|
|
622
|
-
//
|
|
689
|
+
// (`prisma-next migrate`) regardless of how many spaces were
|
|
690
|
+
// materialised — `db update` is a dev-time convenience, not the
|
|
691
|
+
// canonical replay step.
|
|
623
692
|
lines.push(
|
|
624
|
-
`Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next
|
|
693
|
+
`Next: review ${green_(result.dir ?? '<dir>')} if needed, then run ${green_('prisma-next migrate')}.`,
|
|
625
694
|
);
|
|
626
695
|
|
|
627
696
|
if (result.preview && result.preview.statements.length > 0) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
3
|
import {
|
|
4
|
+
APP_SPACE_ID,
|
|
4
5
|
createControlStack,
|
|
5
6
|
type MigrationPlanOperation,
|
|
6
7
|
type OperationPreview,
|
|
@@ -12,6 +13,8 @@ import {
|
|
|
12
13
|
reconstructGraph,
|
|
13
14
|
} from '@prisma-next/migration-tools/migration-graph';
|
|
14
15
|
import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
|
|
16
|
+
import { parseMigrationRef } from '@prisma-next/migration-tools/ref-resolution';
|
|
17
|
+
import { readRefs } from '@prisma-next/migration-tools/refs';
|
|
15
18
|
import { spaceMigrationDirectory } from '@prisma-next/migration-tools/spaces';
|
|
16
19
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
17
20
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
@@ -26,6 +29,7 @@ import {
|
|
|
26
29
|
errorRuntime,
|
|
27
30
|
errorUnexpected,
|
|
28
31
|
mapMigrationToolsError,
|
|
32
|
+
mapRefResolutionError,
|
|
29
33
|
} from '../utils/cli-errors';
|
|
30
34
|
import {
|
|
31
35
|
addGlobalOptions,
|
|
@@ -33,6 +37,7 @@ import {
|
|
|
33
37
|
resolveMigrationPaths,
|
|
34
38
|
setCommandDescriptions,
|
|
35
39
|
setCommandExamples,
|
|
40
|
+
setCommandSeeAlso,
|
|
36
41
|
} from '../utils/command-helpers';
|
|
37
42
|
import { buildContractSpaceAggregate } from '../utils/contract-space-aggregate-loader';
|
|
38
43
|
import { formatMigrationShowOutput } from '../utils/formatters/migrations';
|
|
@@ -237,7 +242,7 @@ async function executeMigrationShowCommand(
|
|
|
237
242
|
ui: TerminalUI,
|
|
238
243
|
): Promise<Result<MigrationShowResult, CliStructuredError>> {
|
|
239
244
|
const config = await loadConfig(options.config);
|
|
240
|
-
const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } =
|
|
245
|
+
const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative, refsDir } =
|
|
241
246
|
resolveMigrationPaths(options.config, config);
|
|
242
247
|
|
|
243
248
|
const contractPathAbsolute = resolveContractPath(config);
|
|
@@ -261,7 +266,91 @@ async function executeMigrationShowCommand(
|
|
|
261
266
|
ui.stderr(header);
|
|
262
267
|
}
|
|
263
268
|
|
|
264
|
-
//
|
|
269
|
+
// `migration show` is an offline command; the control client is constructed
|
|
270
|
+
// purely to dispatch the family-specific `toOperationPreview` capability and
|
|
271
|
+
// is not connected to a database.
|
|
272
|
+
const client = createControlClient({
|
|
273
|
+
family: config.family,
|
|
274
|
+
target: config.target,
|
|
275
|
+
adapter: config.adapter,
|
|
276
|
+
...ifDefined('driver', config.driver),
|
|
277
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Explicit-target path. Read the app-space migrations directory directly
|
|
281
|
+
// and resolve `target` against the app graph. We deliberately skip
|
|
282
|
+
// `buildContractSpaceAggregate` here for two reasons:
|
|
283
|
+
//
|
|
284
|
+
// 1. Functional: the user asked about ONE specific migration. They don't
|
|
285
|
+
// need extension-space enumeration; resolving + rendering the named
|
|
286
|
+
// package is enough.
|
|
287
|
+
// 2. UX: the aggregate's layout-integrity check (PN-MIG-5001) fires when
|
|
288
|
+
// an extension is declared but its migrations directory hasn't been
|
|
289
|
+
// materialised. Gating an offline read-only inspect command on that
|
|
290
|
+
// check forces users to run `migrate` against a database before they
|
|
291
|
+
// can see what a migration contains — which contradicts what an
|
|
292
|
+
// offline read-only verb should require.
|
|
293
|
+
//
|
|
294
|
+
// Same pattern as `migration list`, `migration graph`, `migration check`:
|
|
295
|
+
// those verbs read `appMigrationsDir` directly without ever consulting
|
|
296
|
+
// the aggregate.
|
|
297
|
+
if (target) {
|
|
298
|
+
try {
|
|
299
|
+
let appPkg: OnDiskMigrationPackage;
|
|
300
|
+
if (looksLikePath(target)) {
|
|
301
|
+
const resolved = resolveAppTargetPath(target, appMigrationsDir, appMigrationsRelative);
|
|
302
|
+
if (!resolved.ok) return resolved;
|
|
303
|
+
appPkg = await readMigrationPackage(resolved.value);
|
|
304
|
+
} else {
|
|
305
|
+
const allPackages = await readMigrationsDir(appMigrationsDir);
|
|
306
|
+
if (allPackages.length === 0) {
|
|
307
|
+
return notOk(
|
|
308
|
+
errorRuntime('No migrations found', {
|
|
309
|
+
why: `No migration packages found in ${appMigrationsRelative}`,
|
|
310
|
+
fix: 'Run `prisma-next migration plan` to create a migration first.',
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const graph = reconstructGraph(allPackages);
|
|
315
|
+
const refs = await readRefs(refsDir);
|
|
316
|
+
const migResult = parseMigrationRef(target, { graph, refs });
|
|
317
|
+
if (!migResult.ok) {
|
|
318
|
+
return notOk(mapRefResolutionError(migResult.failure));
|
|
319
|
+
}
|
|
320
|
+
const matchedPkg = allPackages.find(
|
|
321
|
+
(p) => p.metadata.migrationHash === migResult.value.migrationHash,
|
|
322
|
+
);
|
|
323
|
+
if (!matchedPkg) {
|
|
324
|
+
return notOk(
|
|
325
|
+
errorRuntime('Migration package not found', {
|
|
326
|
+
why: `Resolved migration "${migResult.value.dirName}" but the package was not loaded`,
|
|
327
|
+
fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
appPkg = matchedPkg;
|
|
332
|
+
}
|
|
333
|
+
return ok({
|
|
334
|
+
ok: true,
|
|
335
|
+
spaces: [pkgToSpaceResult(APP_SPACE_ID, appPkg, client)],
|
|
336
|
+
});
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (MigrationToolsError.is(error)) {
|
|
339
|
+
return notOk(mapMigrationToolsError(error));
|
|
340
|
+
}
|
|
341
|
+
return notOk(
|
|
342
|
+
errorUnexpected(error instanceof Error ? error.message : String(error), {
|
|
343
|
+
why: `Failed to read app-space migration: ${error instanceof Error ? error.message : String(error)}`,
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// No-target path. Enumerate the latest migration per space (app +
|
|
350
|
+
// extensions). The aggregate-loader is needed here because we need to
|
|
351
|
+
// know which extension spaces are declared; its layout-integrity check
|
|
352
|
+
// is appropriate at this entry point because the user is asking the
|
|
353
|
+
// system to report on every loaded space.
|
|
265
354
|
let contractJsonContent: string;
|
|
266
355
|
try {
|
|
267
356
|
contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
|
|
@@ -281,93 +370,71 @@ async function executeMigrationShowCommand(
|
|
|
281
370
|
);
|
|
282
371
|
}
|
|
283
372
|
|
|
373
|
+
// Construct the family instance up-front so the on-disk app contract
|
|
374
|
+
// read crosses the serializer seam (`familyInstance.deserializeContract`)
|
|
375
|
+
// at the read site. See TML-2536.
|
|
376
|
+
const stack = createControlStack(config);
|
|
377
|
+
const familyInstance = config.family.create(stack);
|
|
378
|
+
|
|
284
379
|
let appContract: Contract;
|
|
285
380
|
try {
|
|
286
|
-
appContract = JSON.parse(contractJsonContent) as
|
|
381
|
+
appContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
|
|
287
382
|
} catch (error) {
|
|
288
383
|
return notOk(
|
|
289
384
|
errorContractValidationFailed(
|
|
290
|
-
`Contract
|
|
385
|
+
`Contract at ${contractPathAbsolute} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
|
|
291
386
|
{ where: { path: contractPathAbsolute } },
|
|
292
387
|
),
|
|
293
388
|
);
|
|
294
389
|
}
|
|
295
390
|
|
|
296
|
-
// Build the aggregate against current disk state to enumerate all spaces.
|
|
297
|
-
const stack = createControlStack(config);
|
|
298
|
-
const familyInstance = config.family.create(stack);
|
|
299
391
|
const aggregateResult = await buildContractSpaceAggregate({
|
|
300
392
|
targetId: config.target.targetId,
|
|
301
393
|
migrationsDir,
|
|
302
394
|
appContract,
|
|
303
395
|
extensionPacks: config.extensionPacks ?? [],
|
|
304
|
-
|
|
396
|
+
deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
|
|
305
397
|
});
|
|
306
398
|
if (!aggregateResult.ok) {
|
|
307
399
|
return notOk(aggregateResult.failure);
|
|
308
400
|
}
|
|
309
401
|
const aggregate = aggregateResult.value;
|
|
310
402
|
|
|
311
|
-
// `migration show` is an offline command; the control client is constructed
|
|
312
|
-
// purely to dispatch the family-specific `toOperationPreview` capability and
|
|
313
|
-
// is not connected to a database.
|
|
314
|
-
const client = createControlClient({
|
|
315
|
-
family: config.family,
|
|
316
|
-
target: config.target,
|
|
317
|
-
adapter: config.adapter,
|
|
318
|
-
...ifDefined('driver', config.driver),
|
|
319
|
-
extensionPacks: config.extensionPacks ?? [],
|
|
320
|
-
});
|
|
321
|
-
|
|
322
403
|
const spaces: MigrationShowSpaceResult[] = [];
|
|
323
404
|
|
|
324
|
-
// App space:
|
|
405
|
+
// App space: latest leaf.
|
|
325
406
|
try {
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
appPkg = resolved.value;
|
|
345
|
-
} else {
|
|
346
|
-
const graph = reconstructGraph(allPackages);
|
|
347
|
-
const latestMigration = findLatestMigration(graph);
|
|
348
|
-
if (!latestMigration) {
|
|
349
|
-
return notOk(
|
|
350
|
-
errorRuntime('Could not resolve latest migration', {
|
|
351
|
-
why: 'No latest migration found in the migration history',
|
|
352
|
-
fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
|
|
353
|
-
}),
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
const leafPkg = allPackages.find(
|
|
357
|
-
(p) => p.metadata.migrationHash === latestMigration.migrationHash,
|
|
358
|
-
);
|
|
359
|
-
if (!leafPkg) {
|
|
360
|
-
return notOk(
|
|
361
|
-
errorRuntime('Could not resolve latest migration', {
|
|
362
|
-
why: `Latest migration ${latestMigration.dirName} does not match any package`,
|
|
363
|
-
fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
|
|
364
|
-
}),
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
appPkg = leafPkg;
|
|
368
|
-
}
|
|
407
|
+
const allPackages = await readMigrationsDir(appMigrationsDir);
|
|
408
|
+
if (allPackages.length === 0) {
|
|
409
|
+
return notOk(
|
|
410
|
+
errorRuntime('No migrations found', {
|
|
411
|
+
why: `No migration packages found in ${appMigrationsRelative}`,
|
|
412
|
+
fix: 'Run `prisma-next migration plan` to create a migration first.',
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const graph = reconstructGraph(allPackages);
|
|
417
|
+
const latestMigration = findLatestMigration(graph);
|
|
418
|
+
if (!latestMigration) {
|
|
419
|
+
return notOk(
|
|
420
|
+
errorRuntime('Could not resolve latest migration', {
|
|
421
|
+
why: 'No latest migration found in the migration history',
|
|
422
|
+
fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
369
425
|
}
|
|
370
|
-
|
|
426
|
+
const leafPkg = allPackages.find(
|
|
427
|
+
(p) => p.metadata.migrationHash === latestMigration.migrationHash,
|
|
428
|
+
);
|
|
429
|
+
if (!leafPkg) {
|
|
430
|
+
return notOk(
|
|
431
|
+
errorRuntime('Could not resolve latest migration', {
|
|
432
|
+
why: `Latest migration ${latestMigration.dirName} does not match any package`,
|
|
433
|
+
fix: 'The migrations directory may be corrupted. Inspect the migration.json files.',
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
spaces.push(pkgToSpaceResult(aggregate.app.spaceId, leafPkg, client));
|
|
371
438
|
} catch (error) {
|
|
372
439
|
if (MigrationToolsError.is(error)) {
|
|
373
440
|
return notOk(mapMigrationToolsError(error));
|
|
@@ -415,8 +482,17 @@ export function createMigrationShowCommand(): Command {
|
|
|
415
482
|
'prisma-next migration show',
|
|
416
483
|
'prisma-next migration show sha256:a1b2c3',
|
|
417
484
|
]);
|
|
485
|
+
setCommandSeeAlso(command, [
|
|
486
|
+
{ verb: 'migration status', oneLiner: 'Show migration path and pending status' },
|
|
487
|
+
{ verb: 'migration log', oneLiner: 'Show executed migration history' },
|
|
488
|
+
{ verb: 'migration list', oneLiner: 'List on-disk migrations' },
|
|
489
|
+
{ verb: 'migration graph', oneLiner: 'Show the migration graph topology' },
|
|
490
|
+
]);
|
|
418
491
|
addGlobalOptions(command)
|
|
419
|
-
.argument(
|
|
492
|
+
.argument(
|
|
493
|
+
'[target]',
|
|
494
|
+
'Migration reference: directory name, hash/prefix, or path (defaults to latest)',
|
|
495
|
+
)
|
|
420
496
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
421
497
|
.action(async (target: string | undefined, options: MigrationShowOptions) => {
|
|
422
498
|
const flags = parseGlobalFlags(options);
|