prisma-next 0.5.0-dev.9 → 0.5.0
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/dist/cli-errors-B9OBbled.d.mts +3 -0
- package/dist/cli-errors-D3_sMh2K.mjs +33 -0
- package/dist/cli-errors-D3_sMh2K.mjs.map +1 -0
- package/dist/cli.mjs +16 -78
- package/dist/cli.mjs.map +1 -1
- package/dist/client-qVH-rEgd.mjs +1595 -0
- package/dist/client-qVH-rEgd.mjs.map +1 -0
- package/dist/{result-handler-Ba3zWQsI.mjs → command-helpers-BeZHkxV8.mjs} +70 -47
- package/dist/command-helpers-BeZHkxV8.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +2 -4
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +2 -4
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +16 -13
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.d.mts.map +1 -1
- package/dist/commands/db-schema.mjs +6 -7
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.d.mts.map +1 -1
- package/dist/commands/db-sign.mjs +9 -9
- 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 +15 -13
- 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 -321
- package/dist/commands/migration-apply.d.mts +28 -13
- package/dist/commands/migration-apply.d.mts.map +1 -1
- package/dist/commands/migration-apply.mjs +55 -151
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts +0 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +34 -40
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +33 -6
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +2 -348
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.d.mts.map +1 -1
- package/dist/commands/migration-ref.mjs +8 -12
- package/dist/commands/migration-ref.mjs.map +1 -1
- package/dist/commands/migration-show.d.mts +13 -7
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +35 -36
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +126 -5
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +2 -4
- package/dist/{config-loader-C25b63rJ.mjs → config-loader-B6sJjXTv.mjs} +3 -5
- package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
- package/dist/config-loader.d.mts +0 -1
- package/dist/config-loader.d.mts.map +1 -1
- package/dist/config-loader.mjs +2 -3
- package/dist/contract-emit-9DBda5Ou.mjs +150 -0
- package/dist/contract-emit-9DBda5Ou.mjs.map +1 -0
- package/dist/contract-emit-B77TsJqf.mjs +327 -0
- package/dist/contract-emit-B77TsJqf.mjs.map +1 -0
- package/dist/{contract-enrichment-CAOELa-H.mjs → contract-enrichment-Dani0mMW.mjs} +4 -6
- package/dist/contract-enrichment-Dani0mMW.mjs.map +1 -0
- package/dist/{contract-infer-D9cC3rJm.mjs → contract-infer-BK9YFGEG.mjs} +13 -22
- package/dist/contract-infer-BK9YFGEG.mjs.map +1 -0
- package/dist/db-verify-C0y1PCO2.mjs +404 -0
- package/dist/db-verify-C0y1PCO2.mjs.map +1 -0
- package/dist/exports/config-types.mjs +1 -2
- package/dist/exports/control-api.d.mts +101 -586
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +4 -6
- package/dist/exports/index.d.mts.map +1 -1
- package/dist/exports/index.mjs +28 -30
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.d.mts +2 -4
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/exports/init-output.mjs +2 -3
- package/dist/extension-pack-inputs-C7xgE-vv.mjs +74 -0
- package/dist/extension-pack-inputs-C7xgE-vv.mjs.map +1 -0
- package/dist/{framework-components-Cr--XBKy.mjs → framework-components-ChqVUxR-.mjs} +3 -4
- package/dist/{framework-components-Cr--XBKy.mjs.map → framework-components-ChqVUxR-.mjs.map} +1 -1
- package/dist/global-flags-Icqpxk23.d.mts +12 -0
- package/dist/global-flags-Icqpxk23.d.mts.map +1 -0
- package/dist/helpers-eqdN8tH6.mjs +25 -0
- package/dist/helpers-eqdN8tH6.mjs.map +1 -0
- package/dist/{init-C5220SY9.mjs → init-DETSgw3h.mjs} +40 -49
- package/dist/init-DETSgw3h.mjs.map +1 -0
- package/dist/{inspect-live-schema-yrHAvG71.mjs → inspect-live-schema-CWYxGKlb.mjs} +10 -11
- package/dist/inspect-live-schema-CWYxGKlb.mjs.map +1 -0
- package/dist/migration-cli.d.mts +41 -12
- package/dist/migration-cli.d.mts.map +1 -1
- package/dist/migration-cli.mjs +309 -86
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-B3B09et6.mjs → migration-command-scaffold-B5dORFEv.mjs} +8 -9
- package/dist/migration-command-scaffold-B5dORFEv.mjs.map +1 -0
- package/dist/migration-plan-C6lVaHsO.mjs +554 -0
- package/dist/migration-plan-C6lVaHsO.mjs.map +1 -0
- package/dist/{migration-status-DUMiH8_G.mjs → migration-status-CZ-D5k7k.mjs} +272 -65
- package/dist/migration-status-CZ-D5k7k.mjs.map +1 -0
- package/dist/migrations-D_UJnpuW.mjs +216 -0
- package/dist/migrations-D_UJnpuW.mjs.map +1 -0
- package/dist/{output-BpcQrnnq.mjs → output-B16Kefzx.mjs} +9 -3
- package/dist/output-B16Kefzx.mjs.map +1 -0
- package/dist/{progress-adapter-DvQWB1nK.mjs → progress-adapter-DFfvZcYL.mjs} +2 -2
- package/dist/{progress-adapter-DvQWB1nK.mjs.map → progress-adapter-DFfvZcYL.mjs.map} +1 -1
- package/dist/result-handler-rmPVKIP2.mjs +25 -0
- package/dist/result-handler-rmPVKIP2.mjs.map +1 -0
- package/dist/rolldown-runtime-twds-ZHy.mjs +14 -0
- package/dist/{terminal-ui-C3ZLwQxK.mjs → terminal-ui-C_hFNbAn.mjs} +4 -28
- package/dist/terminal-ui-C_hFNbAn.mjs.map +1 -0
- package/dist/types-D7x-IFLO.d.mts +858 -0
- package/dist/types-D7x-IFLO.d.mts.map +1 -0
- package/dist/{verify-Bkycc-Tf.mjs → verify-CiwNWM9N.mjs} +3 -4
- package/dist/verify-CiwNWM9N.mjs.map +1 -0
- package/package.json +19 -17
- package/dist/cli-errors-BFYgBH3L.d.mts +0 -4
- package/dist/cli-errors-Cd79vmTH.mjs +0 -5
- package/dist/client-CrsnY58k.mjs +0 -997
- package/dist/client-CrsnY58k.mjs.map +0 -1
- package/dist/commands/db-verify.mjs.map +0 -1
- package/dist/commands/migration-plan.mjs.map +0 -1
- package/dist/config-loader-C25b63rJ.mjs.map +0 -1
- package/dist/contract-emit--feXyNd7.mjs +0 -4
- package/dist/contract-emit-NJ01hiiv.mjs +0 -195
- package/dist/contract-emit-NJ01hiiv.mjs.map +0 -1
- package/dist/contract-emit-V5SSitUT.mjs +0 -122
- package/dist/contract-emit-V5SSitUT.mjs.map +0 -1
- package/dist/contract-enrichment-CAOELa-H.mjs.map +0 -1
- package/dist/contract-infer-D9cC3rJm.mjs.map +0 -1
- package/dist/extract-operation-statements-DsFfxXVZ.mjs +0 -13
- package/dist/extract-operation-statements-DsFfxXVZ.mjs.map +0 -1
- package/dist/extract-sql-ddl-D9UbZDyz.mjs +0 -26
- package/dist/extract-sql-ddl-D9UbZDyz.mjs.map +0 -1
- package/dist/init-C5220SY9.mjs.map +0 -1
- package/dist/inspect-live-schema-yrHAvG71.mjs.map +0 -1
- package/dist/migration-command-scaffold-B3B09et6.mjs.map +0 -1
- package/dist/migration-status-DUMiH8_G.mjs.map +0 -1
- package/dist/migrations-Bo5WtTla.mjs +0 -153
- package/dist/migrations-Bo5WtTla.mjs.map +0 -1
- package/dist/output-BpcQrnnq.mjs.map +0 -1
- package/dist/result-handler-Ba3zWQsI.mjs.map +0 -1
- package/dist/terminal-ui-C3ZLwQxK.mjs.map +0 -1
- package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
- package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
- package/dist/verify-Bkycc-Tf.mjs.map +0 -1
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { t as loadConfig } from "./config-loader-B6sJjXTv.mjs";
|
|
2
|
+
import { _ as errorUnexpected, a as errorContractValidationFailed, f as errorMigrationPlanningFailed, h as errorTargetMigrationNotSupported, l as errorFileNotFound, m as errorRuntime, t as CliStructuredError, v as mapMigrationToolsError } from "./cli-errors-D3_sMh2K.mjs";
|
|
3
|
+
import { t as assertFrameworkComponentsCompatible } from "./framework-components-ChqVUxR-.mjs";
|
|
4
|
+
import { a as loadMigrationPackages, c as resolveContractPath, d as setCommandDescriptions, f as setCommandExamples, g as parseGlobalFlags, i as getTargetMigrations, l as resolveMigrationPaths, t as addGlobalOptions, y as formatStyledHeader } from "./command-helpers-BeZHkxV8.mjs";
|
|
5
|
+
import { t as TerminalUI } from "./terminal-ui-C_hFNbAn.mjs";
|
|
6
|
+
import { t as handleResult } from "./result-handler-rmPVKIP2.mjs";
|
|
7
|
+
import { i as toMigratePassInputs, n as toExtensionInputs, r as toExtensionMigrationsInputs } from "./extension-pack-inputs-C7xgE-vv.mjs";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { getEmittedArtifactPaths } from "@prisma-next/emitter";
|
|
10
|
+
import { notOk, ok } from "@prisma-next/utils/result";
|
|
11
|
+
import { join, relative } from "pathe";
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { APP_SPACE_ID, createControlStack, hasOperationPreview } from "@prisma-next/framework-components/control";
|
|
14
|
+
import { copyFilesWithRename, formatMigrationDirName, materialiseExtensionMigrationPackageIfMissing, writeMigrationPackage } from "@prisma-next/migration-tools/io";
|
|
15
|
+
import { findLatestMigration } from "@prisma-next/migration-tools/migration-graph";
|
|
16
|
+
import { detectSpaceContractDrift, emitContractSpaceArtefacts, planAllSpaces, readContractSpaceHeadRef, spaceMigrationDirectory } from "@prisma-next/migration-tools/spaces";
|
|
17
|
+
import { MigrationToolsError } from "@prisma-next/migration-tools/errors";
|
|
18
|
+
import { computeMigrationHash } from "@prisma-next/migration-tools/hash";
|
|
19
|
+
import { writeMigrationTs } from "@prisma-next/migration-tools/migration-ts";
|
|
20
|
+
import { deriveProvidedInvariants } from "@prisma-next/migration-tools/invariants";
|
|
21
|
+
//#region src/utils/contract-space-extension-migrations-pass.ts
|
|
22
|
+
/**
|
|
23
|
+
* Materialise an extension's pre-built migration packages onto disk
|
|
24
|
+
* under `migrations/<spaceId>/<dirName>/` for every package that does
|
|
25
|
+
* not yet exist there.
|
|
26
|
+
*
|
|
27
|
+
* Helper-location pattern — the per-space "planner" for extension
|
|
28
|
+
* spaces is a no-op that just returns the descriptor's `migrations`
|
|
29
|
+
* verbatim; the value `planAllSpaces` brings to this consumer site is
|
|
30
|
+
* **deterministic ordering** (alphabetical by spaceId) and
|
|
31
|
+
* **duplicate-spaceId detection**. The actual write is performed via
|
|
32
|
+
* `materialiseMigrationPackage` per package.
|
|
33
|
+
*
|
|
34
|
+
* Idempotent: an existing `migrations/<spaceId>/<dirName>/` is left
|
|
35
|
+
* untouched and reported in `result.skipped` — the helper never
|
|
36
|
+
* overwrites authored migration content, ensuring re-running
|
|
37
|
+
* `migrate` does not corrupt or churn extension migration packages.
|
|
38
|
+
*
|
|
39
|
+
* Per-space artefacts (`contract.json`, `contract.d.ts`,
|
|
40
|
+
* `refs/head.json`) are emitted by
|
|
41
|
+
* {@link import('./contract-space-migrate-pass').runContractSpaceMigratePass}
|
|
42
|
+
* separately — they cover the head-pointer side of the ledger. This
|
|
43
|
+
* helper covers the migration-graph side.
|
|
44
|
+
*/
|
|
45
|
+
async function runContractSpaceExtensionMigrationsPass(inputs) {
|
|
46
|
+
const planned = planAllSpaces(inputs.extensionPacks.filter((pack) => pack.contractSpace !== void 0).map((pack) => ({
|
|
47
|
+
spaceId: pack.id,
|
|
48
|
+
priorContract: null,
|
|
49
|
+
newContract: pack.contractSpace.contractJson,
|
|
50
|
+
__migrations: pack.contractSpace.migrations
|
|
51
|
+
})), (input) => input.__migrations);
|
|
52
|
+
const emitted = [];
|
|
53
|
+
const skipped = [];
|
|
54
|
+
for (const space of planned) {
|
|
55
|
+
const spaceDir = spaceMigrationDirectory(inputs.migrationsDir, space.spaceId);
|
|
56
|
+
for (const pkg of space.migrationPackages) {
|
|
57
|
+
const { written } = await materialiseExtensionMigrationPackageIfMissing(spaceDir, pkg);
|
|
58
|
+
if (written) emitted.push({
|
|
59
|
+
spaceId: space.spaceId,
|
|
60
|
+
dirName: pkg.dirName
|
|
61
|
+
});
|
|
62
|
+
else skipped.push({
|
|
63
|
+
spaceId: space.spaceId,
|
|
64
|
+
dirName: pkg.dirName
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
emitted,
|
|
70
|
+
skipped
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/utils/contract-space-migrate-pass.ts
|
|
75
|
+
/**
|
|
76
|
+
* Run drift detection + on-disk artefact emission for every loaded
|
|
77
|
+
* extension space at `migrate` time.
|
|
78
|
+
*
|
|
79
|
+
* Per sub-spec § 3:
|
|
80
|
+
*
|
|
81
|
+
* - For each declared extension that exposes a `contractSpace`:
|
|
82
|
+
* - Read the on-disk head hash from `migrations/<spaceId>/refs/head.json`
|
|
83
|
+
* (returns `null` on first emit).
|
|
84
|
+
* - Compare against the descriptor's `headRef.hash` via
|
|
85
|
+
* `detectSpaceContractDrift`. The `kind` discriminant decides whether
|
|
86
|
+
* the user sees a warning (`drift`), a no-op silent emit (`firstEmit`,
|
|
87
|
+
* `noDrift`), or nothing at all.
|
|
88
|
+
* - Always re-emit the on-disk artefacts (`contract.json`, `contract.d.ts`,
|
|
89
|
+
* `refs/head.json`). The framework owns these files and the helper is
|
|
90
|
+
* idempotent.
|
|
91
|
+
*
|
|
92
|
+
* Drift warnings are returned to the caller for formatting (TerminalUI,
|
|
93
|
+
* structured-output envelope, etc.) — the helper does not print directly,
|
|
94
|
+
* keeping it framework-neutral and unit-testable.
|
|
95
|
+
*
|
|
96
|
+
* Extension migration packages (the descriptor's pre-canned `migrations`
|
|
97
|
+
* array → `migrations/<spaceId>/<dirName>/`) are intentionally not
|
|
98
|
+
* materialised here — that interaction will be wired in a follow-on round
|
|
99
|
+
* once the runner-side single-tx slice (sub-spec § 6) is in place.
|
|
100
|
+
* On-disk artefacts are sufficient to lock the drift-warning behaviour
|
|
101
|
+
* and the always-on re-emit AC for R2.
|
|
102
|
+
*
|
|
103
|
+
* @see specs/framework-mechanism.spec.md § 3 — Drift detection (T1.9).
|
|
104
|
+
*/
|
|
105
|
+
async function runContractSpaceMigratePass(inputs) {
|
|
106
|
+
const drifts = [];
|
|
107
|
+
const emittedSpaceIds = [];
|
|
108
|
+
for (const pack of inputs.extensionPacks) {
|
|
109
|
+
if (pack.contractSpace === void 0) continue;
|
|
110
|
+
const { contractJson, headRef } = pack.contractSpace;
|
|
111
|
+
const onDiskHeadRef = await readContractSpaceHeadRef(inputs.migrationsDir, pack.id);
|
|
112
|
+
const drift = detectSpaceContractDrift(pack.id, {
|
|
113
|
+
descriptorHash: headRef.hash,
|
|
114
|
+
priorHeadHash: onDiskHeadRef?.hash ?? null
|
|
115
|
+
});
|
|
116
|
+
drifts.push(drift);
|
|
117
|
+
await emitContractSpaceArtefacts(inputs.migrationsDir, pack.id, {
|
|
118
|
+
contract: contractJson,
|
|
119
|
+
contractDts: buildPlaceholderContractDts(pack.id),
|
|
120
|
+
headRef: {
|
|
121
|
+
hash: headRef.hash,
|
|
122
|
+
invariants: headRef.invariants
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
emittedSpaceIds.push(pack.id);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
drifts,
|
|
129
|
+
emittedSpaceIds
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Format the user-facing drift warning for a single space. Callers
|
|
134
|
+
* funnel this through their preferred output channel (TerminalUI line,
|
|
135
|
+
* structured-output envelope `warnings[]`, etc.).
|
|
136
|
+
*
|
|
137
|
+
* Locks AM7 — drift warning surfaces the extension name and the diff
|
|
138
|
+
* direction (descriptor → on-disk head).
|
|
139
|
+
*/
|
|
140
|
+
function formatContractSpaceDriftWarning(drift) {
|
|
141
|
+
if (drift.kind !== "drift") throw new Error(`formatContractSpaceDriftWarning called with non-drift result: ${drift.kind}`);
|
|
142
|
+
return `Contract-space drift detected for "${drift.spaceId}": descriptor hash ${drift.descriptorHash} differs from on-disk head hash ${drift.priorHeadHash ?? "<none>"}. The on-disk artefacts under migrations/${drift.spaceId}/ will be refreshed to match the descriptor.`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Placeholder `.d.ts` content for an extension space's on-disk mirror.
|
|
146
|
+
*
|
|
147
|
+
* Rendering a fully-typed `.d.ts` for an extension contract requires the
|
|
148
|
+
* SQL-family renderer with the codec / typemap registry threaded
|
|
149
|
+
* through; that integration is tracked under sub-spec Open Question 3
|
|
150
|
+
* (see `projects/extension-contract-spaces/specs/framework-mechanism.spec.md`).
|
|
151
|
+
*
|
|
152
|
+
* Until that ships, the on-disk `.d.ts` is a `@ts-nocheck` stub. The
|
|
153
|
+
* spec gap closing alongside the typed renderer is **AC2 / AC14**
|
|
154
|
+
* (byte-equivalence of per-space artefacts under `migrate`):
|
|
155
|
+
* a placeholder cannot be byte-equal to a fully-rendered `.d.ts` from
|
|
156
|
+
* the same descriptor, so AC2 / AC14 are PARTIAL today and become
|
|
157
|
+
* fully-PASS once OQ3 closes.
|
|
158
|
+
*
|
|
159
|
+
* Scheduled to close in **M3** (cipherstash editor tooling) — that's
|
|
160
|
+
* the milestone where the typed renderer gets its first real
|
|
161
|
+
* extension-space consumer and the byte-equivalence guarantee is
|
|
162
|
+
* practically required.
|
|
163
|
+
*/
|
|
164
|
+
function buildPlaceholderContractDts(spaceId) {
|
|
165
|
+
return [
|
|
166
|
+
"// @ts-nocheck",
|
|
167
|
+
"/**",
|
|
168
|
+
` * Placeholder \`.d.ts\` for extension space "${spaceId}".`,
|
|
169
|
+
" *",
|
|
170
|
+
" * The framework re-emits this file on every `migrate` run alongside",
|
|
171
|
+
" * `contract.json` and `refs/head.json`. A typed `.d.ts` rendering",
|
|
172
|
+
" * pass for extension contracts is tracked under the project's open",
|
|
173
|
+
" * questions; until that ships, consumers should import",
|
|
174
|
+
" * `contract.json` directly with `validateContract<…>(…)`.",
|
|
175
|
+
" */",
|
|
176
|
+
"export {};",
|
|
177
|
+
""
|
|
178
|
+
].join("\n");
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/commands/migration-plan.ts
|
|
182
|
+
async function executeMigrationPlanCommand(options, flags, ui, startTime) {
|
|
183
|
+
const config = await loadConfig(options.config);
|
|
184
|
+
const { configPath, migrationsDir, appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(options.config, config);
|
|
185
|
+
const contractPathAbsolute = resolveContractPath(config);
|
|
186
|
+
const contractPath = relative(process.cwd(), contractPathAbsolute);
|
|
187
|
+
if (!flags.json && !flags.quiet) {
|
|
188
|
+
const details = [
|
|
189
|
+
{
|
|
190
|
+
label: "config",
|
|
191
|
+
value: configPath
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
label: "contract",
|
|
195
|
+
value: contractPath
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
label: "migrations",
|
|
199
|
+
value: appMigrationsRelative
|
|
200
|
+
}
|
|
201
|
+
];
|
|
202
|
+
if (options.from) details.push({
|
|
203
|
+
label: "from",
|
|
204
|
+
value: options.from
|
|
205
|
+
});
|
|
206
|
+
if (options.name) details.push({
|
|
207
|
+
label: "name",
|
|
208
|
+
value: options.name
|
|
209
|
+
});
|
|
210
|
+
const header = formatStyledHeader({
|
|
211
|
+
command: "migration plan",
|
|
212
|
+
description: "Plan a migration from contract changes",
|
|
213
|
+
url: "https://pris.ly/migration-plan",
|
|
214
|
+
details,
|
|
215
|
+
flags
|
|
216
|
+
});
|
|
217
|
+
ui.stderr(header);
|
|
218
|
+
}
|
|
219
|
+
let contractJsonContent;
|
|
220
|
+
try {
|
|
221
|
+
contractJsonContent = await readFile(contractPathAbsolute, "utf-8");
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error instanceof Error && error.code === "ENOENT") return notOk(errorFileNotFound(contractPathAbsolute, {
|
|
224
|
+
why: `Contract file not found at ${contractPathAbsolute}`,
|
|
225
|
+
fix: `Run \`prisma-next contract emit\` to generate ${contractPath}, or update \`config.contract.output\` in ${configPath}`
|
|
226
|
+
}));
|
|
227
|
+
return notOk(errorUnexpected(error instanceof Error ? error.message : String(error), { why: `Failed to read contract file: ${error instanceof Error ? error.message : String(error)}` }));
|
|
228
|
+
}
|
|
229
|
+
let toContractJson;
|
|
230
|
+
try {
|
|
231
|
+
toContractJson = JSON.parse(contractJsonContent);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return notOk(errorContractValidationFailed(`Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`, { where: { path: contractPathAbsolute } }));
|
|
234
|
+
}
|
|
235
|
+
const rawStorageHash = toContractJson.storage?.storageHash;
|
|
236
|
+
if (typeof rawStorageHash !== "string") return notOk(errorContractValidationFailed("Contract is missing storageHash", { where: { path: contractPathAbsolute } }));
|
|
237
|
+
const toStorageHash = rawStorageHash;
|
|
238
|
+
let fromContract = null;
|
|
239
|
+
let fromHash = null;
|
|
240
|
+
let fromContractSourceDir = null;
|
|
241
|
+
try {
|
|
242
|
+
const { bundles, graph } = await loadMigrationPackages(appMigrationsDir);
|
|
243
|
+
if (options.from) {
|
|
244
|
+
const resolved = resolveBundleByPrefix(bundles, options.from);
|
|
245
|
+
if (!resolved.ok) {
|
|
246
|
+
const f = resolved.failure;
|
|
247
|
+
return notOk(f.reason === "ambiguous" ? errorRuntime("Multiple matching migrations found", {
|
|
248
|
+
why: `Prefix "${options.from}" matches ${f.count} migrations in ${appMigrationsRelative}`,
|
|
249
|
+
fix: "Provide a longer prefix to disambiguate, or omit --from to use the latest migration target."
|
|
250
|
+
}) : errorRuntime("Starting contract not found", {
|
|
251
|
+
why: `No migration with to hash matching "${options.from}" exists in ${appMigrationsRelative}`,
|
|
252
|
+
fix: "Check that the --from hash matches a known migration target hash, or omit --from to use the latest migration target."
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
fromHash = resolved.value.metadata.to;
|
|
256
|
+
fromContract = resolved.value.metadata.toContract;
|
|
257
|
+
fromContractSourceDir = resolved.value.dirPath;
|
|
258
|
+
} else {
|
|
259
|
+
const latestMigration = findLatestMigration(graph);
|
|
260
|
+
if (latestMigration) {
|
|
261
|
+
fromHash = latestMigration.to;
|
|
262
|
+
const leafPkg = bundles.find((p) => p.metadata.migrationHash === latestMigration.migrationHash);
|
|
263
|
+
if (leafPkg) {
|
|
264
|
+
fromContract = leafPkg.metadata.toContract;
|
|
265
|
+
fromContractSourceDir = leafPkg.dirPath;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
|
|
271
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
272
|
+
return notOk(errorUnexpected(message, { why: `Unexpected error while loading migrations: ${message}` }));
|
|
273
|
+
}
|
|
274
|
+
const canonicalExtensionInputs = toExtensionInputs(config.extensionPacks ?? []);
|
|
275
|
+
const migratePass = await runContractSpaceMigratePass({
|
|
276
|
+
migrationsDir,
|
|
277
|
+
extensionPacks: toMigratePassInputs(canonicalExtensionInputs)
|
|
278
|
+
});
|
|
279
|
+
if (!flags.json && !flags.quiet) {
|
|
280
|
+
for (const drift of migratePass.drifts) if (drift.kind === "drift") ui.stderr(formatContractSpaceDriftWarning(drift));
|
|
281
|
+
}
|
|
282
|
+
const extensionMigrationsResult = await runContractSpaceExtensionMigrationsPass({
|
|
283
|
+
migrationsDir,
|
|
284
|
+
extensionPacks: toExtensionMigrationsInputs(canonicalExtensionInputs)
|
|
285
|
+
});
|
|
286
|
+
if (!flags.json && !flags.quiet) for (const entry of extensionMigrationsResult.emitted) ui.step(`Emitted ${entry.spaceId}/${entry.dirName}`);
|
|
287
|
+
if (fromHash === toStorageHash) return ok({
|
|
288
|
+
ok: true,
|
|
289
|
+
noOp: true,
|
|
290
|
+
from: fromHash,
|
|
291
|
+
to: toStorageHash,
|
|
292
|
+
operations: [],
|
|
293
|
+
emittedExtensionDirs: extensionMigrationsResult.emitted,
|
|
294
|
+
summary: "No changes detected between contracts",
|
|
295
|
+
timings: { total: Date.now() - startTime }
|
|
296
|
+
});
|
|
297
|
+
const migrations = getTargetMigrations(config.target);
|
|
298
|
+
if (!migrations) return notOk(errorTargetMigrationNotSupported({ why: `Target "${config.target.id}" does not support migrations` }));
|
|
299
|
+
const frameworkComponents = assertFrameworkComponentsCompatible(config.family.familyId, config.target.targetId, [
|
|
300
|
+
config.target,
|
|
301
|
+
config.adapter,
|
|
302
|
+
...config.extensionPacks ?? []
|
|
303
|
+
]);
|
|
304
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
305
|
+
const packageDir = join(appMigrationsDir, formatMigrationDirName(timestamp, options.name ?? "migration"));
|
|
306
|
+
const baseMetadata = {
|
|
307
|
+
from: fromHash,
|
|
308
|
+
to: toStorageHash,
|
|
309
|
+
fromContract,
|
|
310
|
+
toContract: toContractJson,
|
|
311
|
+
hints: {
|
|
312
|
+
used: [],
|
|
313
|
+
applied: [],
|
|
314
|
+
plannerVersion: "2.0.0"
|
|
315
|
+
},
|
|
316
|
+
labels: [],
|
|
317
|
+
createdAt: timestamp.toISOString()
|
|
318
|
+
};
|
|
319
|
+
try {
|
|
320
|
+
const stack = createControlStack(config);
|
|
321
|
+
const familyInstance = config.family.create(stack);
|
|
322
|
+
const planner = migrations.createPlanner(familyInstance);
|
|
323
|
+
const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
|
|
324
|
+
const plannerResult = planner.plan({
|
|
325
|
+
contract: toContractJson,
|
|
326
|
+
schema: fromSchema,
|
|
327
|
+
policy: { allowedOperationClasses: [
|
|
328
|
+
"additive",
|
|
329
|
+
"widening",
|
|
330
|
+
"destructive",
|
|
331
|
+
"data"
|
|
332
|
+
] },
|
|
333
|
+
fromContract,
|
|
334
|
+
frameworkComponents,
|
|
335
|
+
spaceId: APP_SPACE_ID
|
|
336
|
+
});
|
|
337
|
+
if (plannerResult.kind === "failure") return notOk(errorMigrationPlanningFailed({ conflicts: plannerResult.conflicts }));
|
|
338
|
+
let plannedOps = [];
|
|
339
|
+
let hasPlaceholders = false;
|
|
340
|
+
try {
|
|
341
|
+
plannedOps = plannerResult.plan.operations;
|
|
342
|
+
if (plannedOps.length === 0) return notOk(errorMigrationPlanningFailed({ conflicts: [{
|
|
343
|
+
kind: "unsupportedChange",
|
|
344
|
+
summary: "Contract changed but planner produced no operations. This indicates unsupported or ignored changes."
|
|
345
|
+
}] }));
|
|
346
|
+
} catch (e) {
|
|
347
|
+
if (CliStructuredError.is(e) && e.domain === "MIG" && e.code === "2001") hasPlaceholders = true;
|
|
348
|
+
else throw e;
|
|
349
|
+
}
|
|
350
|
+
const migrationTsContent = plannerResult.plan.renderTypeScript();
|
|
351
|
+
const opsForWrite = hasPlaceholders ? [] : plannedOps;
|
|
352
|
+
const metadataWithInvariants = {
|
|
353
|
+
...baseMetadata,
|
|
354
|
+
providedInvariants: deriveProvidedInvariants(opsForWrite)
|
|
355
|
+
};
|
|
356
|
+
await writeMigrationPackage(packageDir, {
|
|
357
|
+
...metadataWithInvariants,
|
|
358
|
+
migrationHash: computeMigrationHash(metadataWithInvariants, opsForWrite)
|
|
359
|
+
}, opsForWrite);
|
|
360
|
+
const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
|
|
361
|
+
await copyFilesWithRename(packageDir, [{
|
|
362
|
+
sourcePath: destinationArtifacts.jsonPath,
|
|
363
|
+
destName: "end-contract.json"
|
|
364
|
+
}, {
|
|
365
|
+
sourcePath: destinationArtifacts.dtsPath,
|
|
366
|
+
destName: "end-contract.d.ts"
|
|
367
|
+
}]);
|
|
368
|
+
if (fromContractSourceDir !== null) {
|
|
369
|
+
const sourceArtifacts = getEmittedArtifactPaths(join(fromContractSourceDir, "end-contract.json"));
|
|
370
|
+
await copyFilesWithRename(packageDir, [{
|
|
371
|
+
sourcePath: sourceArtifacts.jsonPath,
|
|
372
|
+
destName: "start-contract.json"
|
|
373
|
+
}, {
|
|
374
|
+
sourcePath: sourceArtifacts.dtsPath,
|
|
375
|
+
destName: "start-contract.d.ts"
|
|
376
|
+
}]);
|
|
377
|
+
}
|
|
378
|
+
await writeMigrationTs(packageDir, migrationTsContent);
|
|
379
|
+
if (hasPlaceholders) return ok({
|
|
380
|
+
ok: true,
|
|
381
|
+
noOp: false,
|
|
382
|
+
from: fromHash,
|
|
383
|
+
to: toStorageHash,
|
|
384
|
+
dir: relative(process.cwd(), packageDir),
|
|
385
|
+
operations: [],
|
|
386
|
+
emittedExtensionDirs: extensionMigrationsResult.emitted,
|
|
387
|
+
pendingPlaceholders: true,
|
|
388
|
+
summary: "Planned migration with placeholder(s) — edit migration.ts then run `node migration.ts` to self-emit",
|
|
389
|
+
timings: { total: Date.now() - startTime }
|
|
390
|
+
});
|
|
391
|
+
const preview = hasOperationPreview(familyInstance) ? familyInstance.toOperationPreview(plannedOps) : void 0;
|
|
392
|
+
return ok({
|
|
393
|
+
ok: true,
|
|
394
|
+
noOp: false,
|
|
395
|
+
from: fromHash,
|
|
396
|
+
to: toStorageHash,
|
|
397
|
+
dir: relative(process.cwd(), packageDir),
|
|
398
|
+
operations: plannedOps.map((op) => ({
|
|
399
|
+
id: op.id,
|
|
400
|
+
label: op.label,
|
|
401
|
+
operationClass: op.operationClass
|
|
402
|
+
})),
|
|
403
|
+
emittedExtensionDirs: extensionMigrationsResult.emitted,
|
|
404
|
+
...preview !== void 0 ? { preview } : {},
|
|
405
|
+
summary: buildPlanSummary(plannedOps.length, extensionMigrationsResult.emitted.length),
|
|
406
|
+
timings: { total: Date.now() - startTime }
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (CliStructuredError.is(error)) return notOk(error);
|
|
410
|
+
if (MigrationToolsError.is(error)) return notOk(mapMigrationToolsError(error));
|
|
411
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
412
|
+
return notOk(errorUnexpected(message, { why: `Unexpected error during migration plan: ${message}` }));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function createMigrationPlanCommand() {
|
|
416
|
+
const command = new Command("plan");
|
|
417
|
+
setCommandDescriptions(command, "Plan a migration from contract changes", "Compares the emitted contract against the latest on-disk migration state and\nproduces a new migration package with the required operations. No database\nconnection is needed — this is a fully offline operation.");
|
|
418
|
+
setCommandExamples(command, ["prisma-next migration plan", "prisma-next migration plan --name add-users-table"]);
|
|
419
|
+
addGlobalOptions(command).option("--config <path>", "Path to prisma-next.config.ts").option("--name <slug>", "Name slug for the migration directory", "migration").option("--from <hash>", "Explicit starting contract hash (overrides latest migration target)").action(async (options) => {
|
|
420
|
+
const flags = parseGlobalFlags(options);
|
|
421
|
+
const startTime = Date.now();
|
|
422
|
+
const ui = new TerminalUI({
|
|
423
|
+
color: flags.color,
|
|
424
|
+
interactive: flags.interactive
|
|
425
|
+
});
|
|
426
|
+
const exitCode = handleResult(await executeMigrationPlanCommand(options, flags, ui, startTime), flags, ui, (planResult) => {
|
|
427
|
+
if (flags.json) ui.output(JSON.stringify(planResult, null, 2));
|
|
428
|
+
else if (!flags.quiet) ui.log(formatMigrationPlanOutput(planResult, flags));
|
|
429
|
+
});
|
|
430
|
+
process.exit(exitCode);
|
|
431
|
+
});
|
|
432
|
+
return command;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Compose the success-line summary so the cross-space side effect
|
|
436
|
+
* (extension-space migration packages materialised on disk during
|
|
437
|
+
* this `plan` run) is visible in the top line — not just in the
|
|
438
|
+
* step log above it.
|
|
439
|
+
*
|
|
440
|
+
* Example outputs:
|
|
441
|
+
* - `Planned 3 operation(s)` (app-space-only project)
|
|
442
|
+
* - `Planned 3 operation(s); materialised 1 extension-space migration` (one extension)
|
|
443
|
+
* - `Planned 3 operation(s); materialised 2 extension-space migrations` (two extensions)
|
|
444
|
+
*
|
|
445
|
+
* Locks AC3 at the summary-line level: a reader of the success line
|
|
446
|
+
* can tell that something happened beyond the app space.
|
|
447
|
+
*/
|
|
448
|
+
function buildPlanSummary(plannedOpsCount, emittedExtensionDirsCount) {
|
|
449
|
+
const base = `Planned ${plannedOpsCount} operation(s)`;
|
|
450
|
+
if (emittedExtensionDirsCount === 0) return base;
|
|
451
|
+
return `${base}; materialised ${emittedExtensionDirsCount} ${emittedExtensionDirsCount === 1 ? "extension-space migration" : "extension-space migrations"}`;
|
|
452
|
+
}
|
|
453
|
+
function formatMigrationPlanOutput(result, flags) {
|
|
454
|
+
const lines = [];
|
|
455
|
+
const useColor = flags.color !== false;
|
|
456
|
+
const green_ = useColor ? (s) => `\x1b[32m${s}\x1b[0m` : (s) => s;
|
|
457
|
+
const yellow_ = useColor ? (s) => `\x1b[33m${s}\x1b[0m` : (s) => s;
|
|
458
|
+
const dim_ = useColor ? (s) => `\x1b[2m${s}\x1b[0m` : (s) => s;
|
|
459
|
+
function appendEmittedExtensions() {
|
|
460
|
+
if (result.emittedExtensionDirs.length === 0) return;
|
|
461
|
+
lines.push("");
|
|
462
|
+
lines.push(dim_("Emitted extension migrations:"));
|
|
463
|
+
for (const entry of result.emittedExtensionDirs) lines.push(dim_(` ${entry.spaceId} → migrations/${entry.spaceId}/${entry.dirName}`));
|
|
464
|
+
lines.push("");
|
|
465
|
+
lines.push(`Next: review the extension migrations above, then run ${green_("prisma-next migration apply")}.`);
|
|
466
|
+
}
|
|
467
|
+
if (result.noOp) {
|
|
468
|
+
lines.push(`${green_("✔")} No changes detected`);
|
|
469
|
+
lines.push(dim_(` from: ${result.from}`));
|
|
470
|
+
lines.push(dim_(` to: ${result.to}`));
|
|
471
|
+
appendEmittedExtensions();
|
|
472
|
+
return lines.join("\n");
|
|
473
|
+
}
|
|
474
|
+
if (result.pendingPlaceholders) {
|
|
475
|
+
lines.push(`${yellow_("⚠")} ${result.summary}`);
|
|
476
|
+
lines.push("");
|
|
477
|
+
lines.push(dim_(`from: ${result.from}`));
|
|
478
|
+
lines.push(dim_(`to: ${result.to}`));
|
|
479
|
+
if (result.dir) lines.push(dim_(`dir: ${result.dir}`));
|
|
480
|
+
lines.push("");
|
|
481
|
+
lines.push("Open migration.ts and replace each `placeholder(...)` call with your actual query.");
|
|
482
|
+
lines.push(`Then run: ${green_(`node ${result.dir ?? "<dir>"}/migration.ts`)}`);
|
|
483
|
+
appendEmittedExtensions();
|
|
484
|
+
return lines.join("\n");
|
|
485
|
+
}
|
|
486
|
+
lines.push(`${green_("✔")} ${result.summary}`);
|
|
487
|
+
lines.push("");
|
|
488
|
+
if (result.operations.length > 0) {
|
|
489
|
+
lines.push(dim_("│"));
|
|
490
|
+
for (let i = 0; i < result.operations.length; i++) {
|
|
491
|
+
const op = result.operations[i];
|
|
492
|
+
const treeChar = i === result.operations.length - 1 ? "└" : "├";
|
|
493
|
+
const destructiveMarker = op.operationClass === "destructive" ? ` ${yellow_("(destructive)")}` : "";
|
|
494
|
+
lines.push(`${dim_(treeChar)}─ ${op.label}${destructiveMarker}`);
|
|
495
|
+
}
|
|
496
|
+
if (result.operations.some((op) => op.operationClass === "destructive")) {
|
|
497
|
+
lines.push("");
|
|
498
|
+
lines.push(`${yellow_("⚠")} This migration contains destructive operations that may cause data loss.`);
|
|
499
|
+
}
|
|
500
|
+
lines.push("");
|
|
501
|
+
}
|
|
502
|
+
lines.push(dim_(`from: ${result.from}`));
|
|
503
|
+
lines.push(dim_(`to: ${result.to}`));
|
|
504
|
+
if (result.dir) lines.push(dim_(`App space → ${result.dir}`));
|
|
505
|
+
for (const entry of result.emittedExtensionDirs) lines.push(dim_(`Extension space ${entry.spaceId} → migrations/${entry.spaceId}/${entry.dirName}`));
|
|
506
|
+
lines.push("");
|
|
507
|
+
lines.push(`Next: review ${green_(result.dir ?? "<dir>")} if needed, then run ${green_("prisma-next migration apply")}.`);
|
|
508
|
+
if (result.preview && result.preview.statements.length > 0) {
|
|
509
|
+
const allSql = result.preview.statements.every((s) => s.language === "sql");
|
|
510
|
+
lines.push("");
|
|
511
|
+
lines.push(dim_(allSql ? "DDL preview" : "Operation preview"));
|
|
512
|
+
lines.push("");
|
|
513
|
+
for (const statement of result.preview.statements) {
|
|
514
|
+
const trimmed = statement.text.trim();
|
|
515
|
+
if (!trimmed) continue;
|
|
516
|
+
const line = statement.language === "sql" && !trimmed.endsWith(";") ? `${trimmed};` : trimmed;
|
|
517
|
+
lines.push(line);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (flags.verbose && result.timings) {
|
|
521
|
+
lines.push("");
|
|
522
|
+
lines.push(dim_(`Total time: ${result.timings.total}ms`));
|
|
523
|
+
}
|
|
524
|
+
return lines.join("\n");
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Resolve a migration package by **target contract hash** (`metadata.to`)
|
|
528
|
+
* using exact match or prefix match.
|
|
529
|
+
*
|
|
530
|
+
* Note: matches `metadata.to` (the contract hash this migration produces),
|
|
531
|
+
* not `metadata.migrationHash` (the package's content-addressed identity).
|
|
532
|
+
* Tries exact match first, then prefix match (auto-prepending `sha256:` when
|
|
533
|
+
* the needle omits the scheme). Returns the matched package on success, or a
|
|
534
|
+
* discriminated failure indicating whether the prefix was ambiguous or simply
|
|
535
|
+
* not found.
|
|
536
|
+
*
|
|
537
|
+
* @internal Exported for testing only.
|
|
538
|
+
*/
|
|
539
|
+
function resolveBundleByPrefix(bundles, needle) {
|
|
540
|
+
const exact = bundles.find((p) => p.metadata.to === needle);
|
|
541
|
+
if (exact) return ok(exact);
|
|
542
|
+
const prefixWithScheme = needle.startsWith("sha256:") ? needle : `sha256:${needle}`;
|
|
543
|
+
const candidates = bundles.filter((p) => p.metadata.to.startsWith(prefixWithScheme));
|
|
544
|
+
if (candidates.length === 1) return ok(candidates[0]);
|
|
545
|
+
if (candidates.length > 1) return notOk({
|
|
546
|
+
reason: "ambiguous",
|
|
547
|
+
count: candidates.length
|
|
548
|
+
});
|
|
549
|
+
return notOk({ reason: "not-found" });
|
|
550
|
+
}
|
|
551
|
+
//#endregion
|
|
552
|
+
export { formatMigrationPlanOutput as n, resolveBundleByPrefix as r, createMigrationPlanCommand as t };
|
|
553
|
+
|
|
554
|
+
//# sourceMappingURL=migration-plan-C6lVaHsO.mjs.map
|