@prisma-next/cli 0.11.0-dev.55 → 0.11.0-dev.57
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 +13 -9
- package/dist/{cli-errors-DFF1LlfU.mjs → cli-errors-DQY629C7.mjs} +16 -11
- package/dist/{cli-errors-DFF1LlfU.mjs.map → cli-errors-DQY629C7.mjs.map} +1 -1
- package/dist/cli.mjs +9 -9
- package/dist/{client-5uvDppD8.mjs → client-Ls2SAhrZ.mjs} +24 -6
- package/dist/{client-5uvDppD8.mjs.map → client-Ls2SAhrZ.mjs.map} +1 -1
- package/dist/{command-helpers-4UNsRRc4.mjs → command-helpers-DTpEJCgI.mjs} +2 -2
- package/dist/{command-helpers-4UNsRRc4.mjs.map → command-helpers-DTpEJCgI.mjs.map} +1 -1
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +5 -5
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-sign.mjs +4 -4
- package/dist/commands/db-update.mjs +5 -5
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +1 -1
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +34 -38
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.mjs +1 -1
- package/dist/commands/migration-graph.d.mts +1 -1
- package/dist/commands/migration-graph.mjs +3 -3
- package/dist/commands/migration-list.d.mts +2 -2
- package/dist/commands/migration-list.mjs +1 -1
- package/dist/commands/migration-log.mjs +3 -3
- package/dist/commands/migration-new.mjs +4 -4
- package/dist/commands/migration-plan.d.mts +1 -1
- 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.mjs +4 -4
- package/dist/commands/migration-status.d.mts +1 -1
- package/dist/commands/migration-status.mjs +6 -6
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.mjs +2 -2
- package/dist/contract-at-errors-B98TC1wK.mjs +42 -0
- package/dist/contract-at-errors-B98TC1wK.mjs.map +1 -0
- package/dist/{contract-emit-C-CFGZsI.mjs → contract-emit-BWLCn2PH.mjs} +3 -3
- package/dist/{contract-emit-C-CFGZsI.mjs.map → contract-emit-BWLCn2PH.mjs.map} +1 -1
- package/dist/{contract-emit-CuUzzM46.mjs → contract-emit-CS3vF-w9.mjs} +4 -4
- package/dist/{contract-emit-CuUzzM46.mjs.map → contract-emit-CS3vF-w9.mjs.map} +1 -1
- package/dist/{contract-infer-C98ZaRhp.mjs → contract-infer-BtefFYF-.mjs} +3 -3
- package/dist/{contract-infer-C98ZaRhp.mjs.map → contract-infer-BtefFYF-.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-CVHGuA35.mjs → contract-space-aggregate-loader-DX_1n2SA.mjs} +2 -2
- package/dist/{contract-space-aggregate-loader-CVHGuA35.mjs.map → contract-space-aggregate-loader-DX_1n2SA.mjs.map} +1 -1
- package/dist/{db-verify-BWl1Yxi-.mjs → db-verify-aHw2nzH2.mjs} +5 -5
- package/dist/{db-verify-BWl1Yxi-.mjs.map → db-verify-aHw2nzH2.mjs.map} +1 -1
- 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-DTcjouhS.mjs → framework-components-BwuEBcyk.mjs} +2 -2
- package/dist/{framework-components-DTcjouhS.mjs.map → framework-components-BwuEBcyk.mjs.map} +1 -1
- package/dist/{global-flags-DWsQ6SSI.d.mts → global-flags-Bo6nCRUS.d.mts} +1 -1
- package/dist/{global-flags-DWsQ6SSI.d.mts.map → global-flags-Bo6nCRUS.d.mts.map} +1 -1
- package/dist/{glyph-mode-CBB4emzO.d.mts → glyph-mode-VIjULGFF.d.mts} +1 -1
- package/dist/glyph-mode-VIjULGFF.d.mts.map +1 -0
- package/dist/{graph-render-D2FnLpuK.mjs → graph-render-eJDcLWny.mjs} +1 -1
- package/dist/{graph-render-D2FnLpuK.mjs.map → graph-render-eJDcLWny.mjs.map} +1 -1
- package/dist/{init-C7PvN163.mjs → init-DOE4Q9YK.mjs} +5 -5
- package/dist/{init-C7PvN163.mjs.map → init-DOE4Q9YK.mjs.map} +1 -1
- package/dist/{inspect-live-schema-BRCWQ-Sr.mjs → inspect-live-schema-IS8jWaJy.mjs} +4 -4
- package/dist/{inspect-live-schema-BRCWQ-Sr.mjs.map → inspect-live-schema-IS8jWaJy.mjs.map} +1 -1
- package/dist/{migration-check-DoskM1nB.mjs → migration-check-BFdael8w.mjs} +2 -2
- package/dist/{migration-check-DoskM1nB.mjs.map → migration-check-BFdael8w.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-CXLkoIJx.mjs → migration-command-scaffold-DojkenVv.mjs} +4 -4
- package/dist/{migration-command-scaffold-CXLkoIJx.mjs.map → migration-command-scaffold-DojkenVv.mjs.map} +1 -1
- package/dist/{migration-list-B2-iQ5Jd.mjs → migration-list-hj86sCtZ.mjs} +3 -3
- package/dist/{migration-list-B2-iQ5Jd.mjs.map → migration-list-hj86sCtZ.mjs.map} +1 -1
- package/dist/{migration-plan-BqmIKQpZ.mjs → migration-plan-Bt6wxUIv.mjs} +165 -178
- package/dist/migration-plan-Bt6wxUIv.mjs.map +1 -0
- package/dist/{migration-types-q64xAI_J.d.mts → migration-types-D2FW63pr.d.mts} +1 -1
- package/dist/{migration-types-q64xAI_J.d.mts.map → migration-types-D2FW63pr.d.mts.map} +1 -1
- package/dist/{migrations-BcVTutso.mjs → migrations-CVLh0Kv4.mjs} +2 -2
- package/dist/{migrations-BcVTutso.mjs.map → migrations-CVLh0Kv4.mjs.map} +1 -1
- package/dist/{output-B60Gw5fu.mjs → output-CF_hqzI-.mjs} +1 -1
- package/dist/{output-B60Gw5fu.mjs.map → output-CF_hqzI-.mjs.map} +1 -1
- package/dist/{types-CEtm6v6a.d.mts → types-BuatV9YW.d.mts} +1 -1
- package/dist/{types-CEtm6v6a.d.mts.map → types-BuatV9YW.d.mts.map} +1 -1
- package/dist/{verify-DOHbbrub.mjs → verify-BiWm4XwD.mjs} +2 -2
- package/dist/{verify-DOHbbrub.mjs.map → verify-BiWm4XwD.mjs.map} +1 -1
- package/package.json +18 -18
- package/src/commands/migrate.ts +37 -36
- package/src/commands/migration-plan.ts +104 -120
- package/src/control-api/operations/migration-apply.ts +25 -4
- package/src/utils/cli-errors.ts +22 -12
- package/src/utils/contract-at-errors.ts +96 -0
- package/src/utils/plan-resolution.ts +134 -133
- package/dist/glyph-mode-CBB4emzO.d.mts.map +0 -1
- package/dist/migration-plan-BqmIKQpZ.mjs.map +0 -1
|
@@ -2,7 +2,6 @@ import { mkdir, readFile, writeFile } 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,
|
|
6
5
|
createControlStack,
|
|
7
6
|
hasOperationPreview,
|
|
8
7
|
type MigrationPlanOperation,
|
|
@@ -36,20 +35,22 @@ import {
|
|
|
36
35
|
import {
|
|
37
36
|
addGlobalOptions,
|
|
38
37
|
getTargetMigrations,
|
|
39
|
-
loadMigrationPackages,
|
|
40
38
|
resolveContractPath,
|
|
41
39
|
resolveMigrationPaths,
|
|
42
40
|
setCommandDescriptions,
|
|
43
41
|
setCommandExamples,
|
|
44
42
|
} from '../utils/command-helpers';
|
|
45
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
buildContractSpaceAggregate,
|
|
45
|
+
loadContractSpaceAggregateForCli,
|
|
46
|
+
} from '../utils/contract-space-aggregate-loader';
|
|
46
47
|
import { runContractSpaceSeedPhase } from '../utils/contract-space-seed-phase';
|
|
47
48
|
import { toExtensionInputs } from '../utils/extension-pack-inputs';
|
|
48
49
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
49
50
|
import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
|
|
50
51
|
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
51
52
|
import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
|
|
52
|
-
import { resolveFromForPlan } from '../utils/plan-resolution';
|
|
53
|
+
import { resolveFromForPlan, resolveToForPlan } from '../utils/plan-resolution';
|
|
53
54
|
import { handleResult } from '../utils/result-handler';
|
|
54
55
|
import { createTerminalUI, type TerminalUI } from '../utils/terminal-ui';
|
|
55
56
|
|
|
@@ -57,55 +58,7 @@ interface MigrationPlanOptions extends CommonCommandOptions {
|
|
|
57
58
|
readonly config?: string;
|
|
58
59
|
readonly name?: string;
|
|
59
60
|
readonly from?: string;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Load a predecessor migration's destination contract from its sibling
|
|
64
|
-
* `end-contract.json` on disk and route it through the family's
|
|
65
|
-
* `ContractSerializer` (via `deserializeContract`) so the in-memory shape
|
|
66
|
-
* is the hydrated `Contract` every other caller sees. Bypassing this
|
|
67
|
-
* seam was the root cause of TML-2536: a raw `JSON.parse(...) as Contract`
|
|
68
|
-
* here let polymorphic `storage.types` entries reach the planner without
|
|
69
|
-
* the `kind` discriminator the planner dispatches on.
|
|
70
|
-
*
|
|
71
|
-
* Throws `CliStructuredError` with:
|
|
72
|
-
* - `errorFileNotFound` when the sibling file is missing — the user
|
|
73
|
-
* has likely deleted or never authored the snapshot, and the
|
|
74
|
-
* message names the file and points them at re-emitting from the
|
|
75
|
-
* source.
|
|
76
|
-
* - `errorContractValidationFailed` when the JSON parses but the
|
|
77
|
-
* family deserializer rejects it (legacy untagged shape, structural
|
|
78
|
-
* mismatch, etc.) — the message names the predecessor's path so
|
|
79
|
-
* the operator can locate the bad snapshot.
|
|
80
|
-
*/
|
|
81
|
-
async function readPredecessorEndContract(
|
|
82
|
-
migrationDir: string,
|
|
83
|
-
familyInstance: ControlFamilyInstance<string, unknown>,
|
|
84
|
-
): Promise<Contract> {
|
|
85
|
-
const path = join(migrationDir, 'end-contract.json');
|
|
86
|
-
let raw: string;
|
|
87
|
-
try {
|
|
88
|
-
raw = await readFile(path, 'utf-8');
|
|
89
|
-
} catch (error) {
|
|
90
|
-
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
91
|
-
throw errorFileNotFound(path, {
|
|
92
|
-
why: `Predecessor migration is missing its destination contract snapshot at ${path}`,
|
|
93
|
-
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.',
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
return familyInstance.deserializeContract(JSON.parse(raw) as unknown);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (CliStructuredError.is(error)) {
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
throw errorContractValidationFailed(
|
|
105
|
-
`Predecessor contract at ${path} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
|
|
106
|
-
{ where: { path } },
|
|
107
|
-
);
|
|
108
|
-
}
|
|
61
|
+
readonly to?: string;
|
|
109
62
|
}
|
|
110
63
|
|
|
111
64
|
async function writeSnapshotContractArtifacts(
|
|
@@ -282,6 +235,9 @@ async function executeMigrationPlanCommand(
|
|
|
282
235
|
if (options.from) {
|
|
283
236
|
details.push({ label: 'from', value: options.from });
|
|
284
237
|
}
|
|
238
|
+
if (options.to) {
|
|
239
|
+
details.push({ label: 'to', value: options.to });
|
|
240
|
+
}
|
|
285
241
|
if (options.name) {
|
|
286
242
|
details.push({ label: 'name', value: options.name });
|
|
287
243
|
}
|
|
@@ -315,8 +271,7 @@ async function executeMigrationPlanCommand(
|
|
|
315
271
|
);
|
|
316
272
|
}
|
|
317
273
|
|
|
318
|
-
// Construct the family instance up-front so on-disk reads
|
|
319
|
-
// contract here + every `readPredecessorEndContract` below) cross the
|
|
274
|
+
// Construct the family instance up-front so on-disk contract reads cross the
|
|
320
275
|
// serializer seam at the read site, not after the planner has already
|
|
321
276
|
// started dispatching on raw shapes. See TML-2536.
|
|
322
277
|
const stack = createControlStack(config);
|
|
@@ -342,9 +297,12 @@ async function executeMigrationPlanCommand(
|
|
|
342
297
|
}),
|
|
343
298
|
);
|
|
344
299
|
}
|
|
345
|
-
|
|
300
|
+
let toStorageHash: string = rawStorageHash;
|
|
346
301
|
|
|
347
|
-
|
|
302
|
+
// When `--to <ref>` resolves a non-default destination, these carry its raw
|
|
303
|
+
// artifacts so the planned package's `end-contract.*` is written from the
|
|
304
|
+
// resolved target rather than copied from the emitted `contract.json`.
|
|
305
|
+
let toArtifacts: { contractJson: unknown; contractDts: string } | null = null;
|
|
348
306
|
|
|
349
307
|
let fromContract: Contract | null = null;
|
|
350
308
|
let fromHash: string | null = null;
|
|
@@ -352,62 +310,71 @@ async function executeMigrationPlanCommand(
|
|
|
352
310
|
let snapshotStartContract: { contractJson: unknown; contractDts: string } | null = null;
|
|
353
311
|
let isAutoBaseline = false;
|
|
354
312
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
});
|
|
313
|
+
const tolerantAggregateResult = await loadContractSpaceAggregateForCli({
|
|
314
|
+
targetId: config.target.targetId,
|
|
315
|
+
migrationsDir,
|
|
316
|
+
appContract: toContract,
|
|
317
|
+
extensionPacks: config.extensionPacks ?? [],
|
|
318
|
+
deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
|
|
319
|
+
});
|
|
320
|
+
if (!tolerantAggregateResult.ok) {
|
|
321
|
+
return notOk(tolerantAggregateResult.failure);
|
|
322
|
+
}
|
|
323
|
+
const resolutionMember = tolerantAggregateResult.value.app;
|
|
367
324
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
325
|
+
const resolutionResult = await resolveFromForPlan({
|
|
326
|
+
optionsFrom: options.from,
|
|
327
|
+
member: resolutionMember,
|
|
328
|
+
});
|
|
371
329
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
330
|
+
if (!resolutionResult.ok) {
|
|
331
|
+
return notOk(resolutionResult.failure);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
switch (resolutionResult.value.kind) {
|
|
335
|
+
case 'greenfield':
|
|
336
|
+
break;
|
|
337
|
+
case 'graph-node':
|
|
338
|
+
fromHash = resolutionResult.value.fromHash;
|
|
339
|
+
fromContract = resolutionResult.value.fromContract;
|
|
340
|
+
fromContractSourceDir = resolutionResult.value.sourceDir;
|
|
341
|
+
break;
|
|
342
|
+
case 'snapshot':
|
|
343
|
+
fromHash = resolutionResult.value.fromHash;
|
|
344
|
+
fromContract = resolutionResult.value.fromContract;
|
|
345
|
+
snapshotStartContract = {
|
|
346
|
+
contractJson: resolutionResult.value.contractJson,
|
|
347
|
+
contractDts: resolutionResult.value.contractDts,
|
|
348
|
+
};
|
|
349
|
+
break;
|
|
350
|
+
case 'auto-baseline':
|
|
351
|
+
fromHash = resolutionResult.value.fromHash;
|
|
352
|
+
fromContract = resolutionResult.value.fromContract;
|
|
353
|
+
snapshotStartContract = {
|
|
354
|
+
contractJson: resolutionResult.value.contractJson,
|
|
355
|
+
contractDts: resolutionResult.value.contractDts,
|
|
356
|
+
};
|
|
357
|
+
isAutoBaseline = true;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// `--to <ref>` swaps the planner destination to an arbitrary resolved
|
|
362
|
+
// contract (e.g. an ancestor / rollback target). The from-side resolution
|
|
363
|
+
// above is untouched; only the destination + its emitted `end-contract.*`
|
|
364
|
+
// change.
|
|
365
|
+
if (options.to !== undefined) {
|
|
366
|
+
const toResolution = await resolveToForPlan(options.to, {
|
|
367
|
+
member: resolutionMember,
|
|
368
|
+
});
|
|
369
|
+
if (!toResolution.ok) {
|
|
370
|
+
return notOk(toResolution.failure);
|
|
404
371
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
372
|
+
toContract = toResolution.value.contract;
|
|
373
|
+
toStorageHash = toResolution.value.hash;
|
|
374
|
+
toArtifacts = {
|
|
375
|
+
contractJson: toResolution.value.contractJson,
|
|
376
|
+
contractDts: toResolution.value.contractDts,
|
|
377
|
+
};
|
|
411
378
|
}
|
|
412
379
|
|
|
413
380
|
// Phase 1 — seed: unconditionally re-emit per-space pinned artefacts
|
|
@@ -487,6 +454,26 @@ async function executeMigrationPlanCommand(
|
|
|
487
454
|
[config.target, config.adapter, ...(config.extensionPacks ?? [])],
|
|
488
455
|
);
|
|
489
456
|
|
|
457
|
+
// Write the planned package's destination `end-contract.*`. With `--to`, the
|
|
458
|
+
// resolved target's raw artifacts are written; otherwise the emitted
|
|
459
|
+
// `contract.json` / `contract.d.ts` are copied verbatim (today's behaviour).
|
|
460
|
+
async function writeDestinationEndContract(packageDir: string): Promise<void> {
|
|
461
|
+
if (toArtifacts !== null) {
|
|
462
|
+
await writeSnapshotContractArtifacts(
|
|
463
|
+
packageDir,
|
|
464
|
+
toArtifacts.contractJson,
|
|
465
|
+
toArtifacts.contractDts,
|
|
466
|
+
'end-contract',
|
|
467
|
+
);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
471
|
+
await copyFilesWithRename(packageDir, [
|
|
472
|
+
{ sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
|
|
473
|
+
{ sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
|
|
474
|
+
]);
|
|
475
|
+
}
|
|
476
|
+
|
|
490
477
|
try {
|
|
491
478
|
const planner = migrations.createPlanner(familyInstance);
|
|
492
479
|
|
|
@@ -591,11 +578,7 @@ async function executeMigrationPlanCommand(
|
|
|
591
578
|
deltaTimestamp,
|
|
592
579
|
deltaLeg.value,
|
|
593
580
|
);
|
|
594
|
-
|
|
595
|
-
await copyFilesWithRename(deltaPackageDir, [
|
|
596
|
-
{ sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
|
|
597
|
-
{ sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
|
|
598
|
-
]);
|
|
581
|
+
await writeDestinationEndContract(deltaPackageDir);
|
|
599
582
|
await writeSnapshotStartContract(
|
|
600
583
|
deltaPackageDir,
|
|
601
584
|
snapshotStartContract.contractJson,
|
|
@@ -668,11 +651,7 @@ async function executeMigrationPlanCommand(
|
|
|
668
651
|
timestamp,
|
|
669
652
|
deltaLeg.value,
|
|
670
653
|
);
|
|
671
|
-
|
|
672
|
-
await copyFilesWithRename(packageDir, [
|
|
673
|
-
{ sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
|
|
674
|
-
{ sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
|
|
675
|
-
]);
|
|
654
|
+
await writeDestinationEndContract(packageDir);
|
|
676
655
|
if (fromContractSourceDir !== null) {
|
|
677
656
|
const sourceArtifacts = getEmittedArtifactPaths(
|
|
678
657
|
join(fromContractSourceDir, 'end-contract.json'),
|
|
@@ -755,6 +734,7 @@ export function createMigrationPlanCommand(): Command {
|
|
|
755
734
|
setCommandExamples(command, [
|
|
756
735
|
'prisma-next migration plan',
|
|
757
736
|
'prisma-next migration plan --name add-users-table',
|
|
737
|
+
'prisma-next migration plan --to <migration-dir>^ --name rollback',
|
|
758
738
|
]);
|
|
759
739
|
addGlobalOptions(command)
|
|
760
740
|
.option('--config <path>', 'Path to prisma-next.config.ts')
|
|
@@ -763,6 +743,10 @@ export function createMigrationPlanCommand(): Command {
|
|
|
763
743
|
'--from <contract>',
|
|
764
744
|
'Starting contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path)',
|
|
765
745
|
)
|
|
746
|
+
.option(
|
|
747
|
+
'--to <contract>',
|
|
748
|
+
'Destination contract reference (hash, prefix, ref name, migration dir name, <dir>^, or ./path); defaults to the emitted contract',
|
|
749
|
+
)
|
|
766
750
|
.action(async (options: MigrationPlanOptions) => {
|
|
767
751
|
const flags = parseGlobalFlagsOrExit(options);
|
|
768
752
|
const startTime = Date.now();
|
|
@@ -450,16 +450,37 @@ function buildSuccess(args: BuildSuccessArgs): MigrationApplySuccess {
|
|
|
450
450
|
};
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Build the `neverPlanned` failure raised when a contract space has no on-disk
|
|
455
|
+
* migration graph but migrate was asked to reach a target hash. The `why`
|
|
456
|
+
* states only the condition; the recovery sequence is composed by
|
|
457
|
+
* `errorPathUnreachable`'s `fix`.
|
|
458
|
+
*
|
|
459
|
+
* @internal Exported for testing only.
|
|
460
|
+
*/
|
|
461
|
+
export function buildNeverPlannedFailure(
|
|
462
|
+
spaceId: string,
|
|
463
|
+
targetHash: string,
|
|
464
|
+
): MigrationApplyFailure {
|
|
454
465
|
return {
|
|
455
466
|
code: 'MIGRATION_PATH_NOT_FOUND',
|
|
456
467
|
summary: `No on-disk migrations for contract space "${spaceId}"`,
|
|
457
|
-
why: `migrate is replay-only: every contract space must have an authored migration graph on disk. Space "${spaceId}" has no migrations under \`migrations/${spaceId}/\` but its head ref targets "${targetHash}"
|
|
468
|
+
why: `migrate is replay-only: every contract space must have an authored migration graph on disk. Space "${spaceId}" has no migrations under \`migrations/${spaceId}/\` but its head ref targets "${targetHash}".`,
|
|
458
469
|
meta: { spaceId, target: targetHash, kind: 'neverPlanned' },
|
|
459
470
|
};
|
|
460
471
|
}
|
|
461
472
|
|
|
462
|
-
|
|
473
|
+
/**
|
|
474
|
+
* Build the `pathUnreachable` failure raised when an emitted contract has no
|
|
475
|
+
* on-disk migration edge from the current marker to the target. The `why`
|
|
476
|
+
* states only the condition (no edge between the two named states, and migrate
|
|
477
|
+
* replays edges rather than inventing them); the recovery sequence — plan the
|
|
478
|
+
* edge, then re-apply — is composed by `errorPathUnreachable`'s `fix`, so the
|
|
479
|
+
* two read as one non-redundant plan-then-apply story.
|
|
480
|
+
*
|
|
481
|
+
* @internal Exported for testing only.
|
|
482
|
+
*/
|
|
483
|
+
export function buildPathNotFoundFailure(
|
|
463
484
|
spaceId: string,
|
|
464
485
|
marker: ContractMarkerRecordLike | null,
|
|
465
486
|
targetHash: string,
|
|
@@ -477,7 +498,7 @@ function buildPathNotFoundFailure(
|
|
|
477
498
|
return {
|
|
478
499
|
code: 'MIGRATION_PATH_NOT_FOUND',
|
|
479
500
|
summary,
|
|
480
|
-
why: `
|
|
501
|
+
why: `No migration edge connects the current state "${fromHash}" to the target "${targetHash}" in contract space "${spaceId}". The on-disk migration graph does not join the two, and migrate replays existing edges — it never invents one.`,
|
|
481
502
|
meta: { spaceId, fromHash, targetHash, kind: 'pathUnreachable' },
|
|
482
503
|
};
|
|
483
504
|
}
|
package/src/utils/cli-errors.ts
CHANGED
|
@@ -252,7 +252,9 @@ export function errorMarkerMismatch(
|
|
|
252
252
|
|
|
253
253
|
export function errorPathUnreachable(failure: MigrationApplyFailure): CliStructuredError {
|
|
254
254
|
const meta = failure.meta ?? {};
|
|
255
|
-
const
|
|
255
|
+
const fromHashMeta = typeof meta['fromHash'] === 'string' ? meta['fromHash'] : null;
|
|
256
|
+
// `buildPathNotFoundFailure` uses this sentinel in meta when the live marker is null.
|
|
257
|
+
const planFromHash = fromHashMeta === '<empty>' ? null : fromHashMeta;
|
|
256
258
|
const targetHash =
|
|
257
259
|
typeof meta['targetHash'] === 'string'
|
|
258
260
|
? meta['targetHash']
|
|
@@ -264,26 +266,34 @@ export function errorPathUnreachable(failure: MigrationApplyFailure): CliStructu
|
|
|
264
266
|
Array.isArray(deadEnds) && deadEnds.length > 0
|
|
265
267
|
? ` Dead-ends: ${deadEnds.map(String).join(', ')}.`
|
|
266
268
|
: '';
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
269
|
+
// Plan-then-apply recovery. The planner destination is the missing edge's
|
|
270
|
+
// target; `migration plan --to` (built for arbitrary targets) makes this a
|
|
271
|
+
// real command, so the diagnostic that sends you here is now honest.
|
|
272
|
+
const planCommand = (() => {
|
|
273
|
+
if (planFromHash !== null && targetHash !== null) {
|
|
274
|
+
return `prisma-next migration plan --from ${planFromHash} --to ${targetHash} --name <slug>`;
|
|
270
275
|
}
|
|
271
276
|
if (targetHash !== null) {
|
|
272
|
-
return `
|
|
277
|
+
return `prisma-next migration plan --to ${targetHash} --name <slug>`;
|
|
273
278
|
}
|
|
274
|
-
if (
|
|
275
|
-
return `
|
|
279
|
+
if (planFromHash !== null) {
|
|
280
|
+
return `prisma-next migration plan --from ${planFromHash} --name <slug>`;
|
|
276
281
|
}
|
|
277
|
-
return '
|
|
282
|
+
return 'prisma-next migration plan';
|
|
278
283
|
})();
|
|
284
|
+
const applyCommand =
|
|
285
|
+
targetHash !== null ? `prisma-next migrate --to ${targetHash}` : 'prisma-next migrate';
|
|
279
286
|
return errorRuntime(failure.summary, {
|
|
280
287
|
why:
|
|
281
288
|
failure.why ??
|
|
282
|
-
`Cannot reach target "${targetHash ?? '<unknown>'}" from current marker "${
|
|
289
|
+
`Cannot reach target "${targetHash ?? '<unknown>'}" from current marker "${fromHashMeta ?? '<unknown>'}".${deadEndsSuffix}`,
|
|
283
290
|
fix: [
|
|
284
|
-
'
|
|
285
|
-
|
|
286
|
-
|
|
291
|
+
'Plan the missing edge, then apply it:',
|
|
292
|
+
` 1. ${planCommand}`,
|
|
293
|
+
` 2. ${applyCommand}`,
|
|
294
|
+
'A rollback (reverse) plan is expected to contain destructive (DROP) operations — review them before applying.',
|
|
295
|
+
'Narrower cases (rename inference, re-adding a NOT NULL column without a safe default, or a type change that needs data) may additionally need a hint in the planned migration.',
|
|
296
|
+
'Inspect the on-disk graph with `prisma-next migration list`, or `prisma-next migration show <bundle>` for any bundle in the path you expected.',
|
|
287
297
|
].join('\n'),
|
|
288
298
|
meta: {
|
|
289
299
|
...meta,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
2
|
+
import { notOk, type Result } from '@prisma-next/utils/result';
|
|
3
|
+
import { join } from 'pathe';
|
|
4
|
+
import {
|
|
5
|
+
CliStructuredError,
|
|
6
|
+
errorContractValidationFailed,
|
|
7
|
+
errorFileNotFound,
|
|
8
|
+
errorSnapshotMissing,
|
|
9
|
+
errorUnexpected,
|
|
10
|
+
mapMigrationToolsError,
|
|
11
|
+
} from './cli-errors';
|
|
12
|
+
|
|
13
|
+
export function mapContractAtError(
|
|
14
|
+
error: unknown,
|
|
15
|
+
options?: { readonly artifactRole?: 'from' | 'to' },
|
|
16
|
+
): Result<never, CliStructuredError> {
|
|
17
|
+
if (MigrationToolsError.is(error)) {
|
|
18
|
+
switch (error.code) {
|
|
19
|
+
case 'MIGRATION.SNAPSHOT_MISSING': {
|
|
20
|
+
const refName =
|
|
21
|
+
typeof error.details?.['refName'] === 'string'
|
|
22
|
+
? error.details['refName']
|
|
23
|
+
: typeof error.details?.['identifier'] === 'string'
|
|
24
|
+
? error.details['identifier']
|
|
25
|
+
: 'unknown';
|
|
26
|
+
return notOk(errorSnapshotMissing(refName));
|
|
27
|
+
}
|
|
28
|
+
case 'MIGRATION.CONTRACT_DESERIALIZATION_FAILED': {
|
|
29
|
+
const filePath =
|
|
30
|
+
typeof error.details?.['filePath'] === 'string'
|
|
31
|
+
? error.details['filePath']
|
|
32
|
+
: 'ref-snapshot';
|
|
33
|
+
const message =
|
|
34
|
+
typeof error.details?.['message'] === 'string' ? error.details['message'] : error.message;
|
|
35
|
+
const isRefSnapshot = filePath.endsWith('.contract.json');
|
|
36
|
+
return notOk(
|
|
37
|
+
errorContractValidationFailed(
|
|
38
|
+
isRefSnapshot
|
|
39
|
+
? `Ref snapshot contract failed to deserialize: ${message}`
|
|
40
|
+
: `Predecessor contract at ${filePath} failed to deserialize: ${message}`,
|
|
41
|
+
{ where: { path: isRefSnapshot ? 'ref-snapshot' : filePath } },
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
case 'MIGRATION.INVALID_JSON': {
|
|
46
|
+
const filePath =
|
|
47
|
+
typeof error.details?.['filePath'] === 'string' ? error.details['filePath'] : 'unknown';
|
|
48
|
+
const message =
|
|
49
|
+
typeof error.details?.['parseError'] === 'string'
|
|
50
|
+
? error.details['parseError']
|
|
51
|
+
: error.message;
|
|
52
|
+
const role = options?.artifactRole ?? 'from';
|
|
53
|
+
return notOk(
|
|
54
|
+
errorContractValidationFailed(
|
|
55
|
+
role === 'to'
|
|
56
|
+
? `Target contract at ${filePath} failed to deserialize: ${message}`
|
|
57
|
+
: `Predecessor contract at ${filePath} failed to deserialize: ${message}`,
|
|
58
|
+
{ where: { path: filePath } },
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
case 'MIGRATION.BUNDLE_NOT_FOUND_FOR_GRAPH_NODE':
|
|
63
|
+
return notOk(
|
|
64
|
+
errorUnexpected(error.message, {
|
|
65
|
+
why: error.why,
|
|
66
|
+
fix: error.fix,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
case 'MIGRATION.FILE_MISSING': {
|
|
70
|
+
const file =
|
|
71
|
+
typeof error.details?.['file'] === 'string' ? error.details['file'] : 'end-contract.json';
|
|
72
|
+
const dir = typeof error.details?.['dir'] === 'string' ? error.details['dir'] : '';
|
|
73
|
+
const jsonPath = dir ? join(dir, 'end-contract.json') : file;
|
|
74
|
+
const role = options?.artifactRole ?? 'from';
|
|
75
|
+
return notOk(
|
|
76
|
+
errorFileNotFound(jsonPath, {
|
|
77
|
+
why:
|
|
78
|
+
role === 'to'
|
|
79
|
+
? `Target migration is missing its destination contract snapshot at ${jsonPath}`
|
|
80
|
+
: `Predecessor migration is missing its destination contract snapshot at ${jsonPath}`,
|
|
81
|
+
fix:
|
|
82
|
+
role === 'to'
|
|
83
|
+
? 'Re-emit the target migration so its sibling `end-contract.json` / `end-contract.d.ts` are restored, then re-run this command.'
|
|
84
|
+
: '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.',
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
default:
|
|
89
|
+
return notOk(mapMigrationToolsError(error));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (CliStructuredError.is(error)) {
|
|
93
|
+
return notOk(error);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|