@prisma-next/cli 0.5.0-dev.7 → 0.5.0-dev.70

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.
Files changed (171) hide show
  1. package/README.md +56 -21
  2. package/dist/cli-errors-D3_sMh2K.mjs +33 -0
  3. package/dist/cli-errors-D3_sMh2K.mjs.map +1 -0
  4. package/dist/cli-errors-QH8kf-C2.d.mts +3 -0
  5. package/dist/cli.mjs +16 -78
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/client-0ZX24FXF.mjs +1398 -0
  8. package/dist/client-0ZX24FXF.mjs.map +1 -0
  9. package/dist/commands/contract-emit.d.mts.map +1 -1
  10. package/dist/commands/contract-emit.mjs +2 -4
  11. package/dist/commands/contract-infer.d.mts.map +1 -1
  12. package/dist/commands/contract-infer.mjs +2 -4
  13. package/dist/commands/db-init.d.mts.map +1 -1
  14. package/dist/commands/db-init.mjs +14 -13
  15. package/dist/commands/db-init.mjs.map +1 -1
  16. package/dist/commands/db-schema.d.mts.map +1 -1
  17. package/dist/commands/db-schema.mjs +5 -7
  18. package/dist/commands/db-schema.mjs.map +1 -1
  19. package/dist/commands/db-sign.d.mts.map +1 -1
  20. package/dist/commands/db-sign.mjs +8 -9
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.d.mts.map +1 -1
  23. package/dist/commands/db-update.mjs +13 -13
  24. package/dist/commands/db-update.mjs.map +1 -1
  25. package/dist/commands/db-verify.d.mts.map +1 -1
  26. package/dist/commands/db-verify.mjs +1 -321
  27. package/dist/commands/migration-apply.d.mts +5 -2
  28. package/dist/commands/migration-apply.d.mts.map +1 -1
  29. package/dist/commands/migration-apply.mjs +64 -66
  30. package/dist/commands/migration-apply.mjs.map +1 -1
  31. package/dist/commands/migration-new.d.mts +0 -1
  32. package/dist/commands/migration-new.d.mts.map +1 -1
  33. package/dist/commands/migration-new.mjs +33 -40
  34. package/dist/commands/migration-new.mjs.map +1 -1
  35. package/dist/commands/migration-plan.d.mts +14 -5
  36. package/dist/commands/migration-plan.d.mts.map +1 -1
  37. package/dist/commands/migration-plan.mjs +1 -347
  38. package/dist/commands/migration-ref.d.mts +1 -1
  39. package/dist/commands/migration-ref.d.mts.map +1 -1
  40. package/dist/commands/migration-ref.mjs +7 -12
  41. package/dist/commands/migration-ref.mjs.map +1 -1
  42. package/dist/commands/migration-show.d.mts +13 -7
  43. package/dist/commands/migration-show.d.mts.map +1 -1
  44. package/dist/commands/migration-show.mjs +34 -36
  45. package/dist/commands/migration-show.mjs.map +1 -1
  46. package/dist/commands/migration-status.d.mts +23 -5
  47. package/dist/commands/migration-status.d.mts.map +1 -1
  48. package/dist/commands/migration-status.mjs +2 -4
  49. package/dist/{config-loader-C25b63rJ.mjs → config-loader-B6sJjXTv.mjs} +3 -5
  50. package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
  51. package/dist/config-loader.d.mts +0 -1
  52. package/dist/config-loader.d.mts.map +1 -1
  53. package/dist/config-loader.mjs +2 -3
  54. package/dist/contract-emit-B3ChISB_.mjs +338 -0
  55. package/dist/contract-emit-B3ChISB_.mjs.map +1 -0
  56. package/dist/contract-emit-DkMqO7f2.mjs +148 -0
  57. package/dist/contract-emit-DkMqO7f2.mjs.map +1 -0
  58. package/dist/{contract-enrichment-CAOELa-H.mjs → contract-enrichment-CF6ogEJ_.mjs} +4 -6
  59. package/dist/contract-enrichment-CF6ogEJ_.mjs.map +1 -0
  60. package/dist/{contract-infer-D9cC3rJm.mjs → contract-infer-BDKAE0B0.mjs} +12 -22
  61. package/dist/contract-infer-BDKAE0B0.mjs.map +1 -0
  62. package/dist/db-verify-B4TdDKOI.mjs +403 -0
  63. package/dist/db-verify-B4TdDKOI.mjs.map +1 -0
  64. package/dist/exports/config-types.mjs +1 -2
  65. package/dist/exports/control-api.d.mts +287 -29
  66. package/dist/exports/control-api.d.mts.map +1 -1
  67. package/dist/exports/control-api.mjs +4 -6
  68. package/dist/exports/index.d.mts.map +1 -1
  69. package/dist/exports/index.mjs +28 -30
  70. package/dist/exports/index.mjs.map +1 -1
  71. package/dist/exports/init-output.d.mts +2 -4
  72. package/dist/exports/init-output.d.mts.map +1 -1
  73. package/dist/exports/init-output.mjs +2 -3
  74. package/dist/{framework-components-Cr--XBKy.mjs → framework-components-gwAHl7ml.mjs} +3 -4
  75. package/dist/{framework-components-Cr--XBKy.mjs.map → framework-components-gwAHl7ml.mjs.map} +1 -1
  76. package/dist/{init-C5220SY9.mjs → init-Deo7U8_U.mjs} +26 -35
  77. package/dist/init-Deo7U8_U.mjs.map +1 -0
  78. package/dist/{inspect-live-schema-yrHAvG71.mjs → inspect-live-schema-BAgQMYpD.mjs} +10 -11
  79. package/dist/inspect-live-schema-BAgQMYpD.mjs.map +1 -0
  80. package/dist/migration-cli.d.mts +41 -12
  81. package/dist/migration-cli.d.mts.map +1 -1
  82. package/dist/migration-cli.mjs +309 -86
  83. package/dist/migration-cli.mjs.map +1 -1
  84. package/dist/{migration-command-scaffold-B3B09et6.mjs → migration-command-scaffold-B8J702Uh.mjs} +8 -9
  85. package/dist/migration-command-scaffold-B8J702Uh.mjs.map +1 -0
  86. package/dist/migration-plan-BcKNnTM7.mjs +530 -0
  87. package/dist/migration-plan-BcKNnTM7.mjs.map +1 -0
  88. package/dist/{migration-status-DUMiH8_G.mjs → migration-status-CjwB2of-.mjs} +117 -64
  89. package/dist/migration-status-CjwB2of-.mjs.map +1 -0
  90. package/dist/{migrations-Bo5WtTla.mjs → migrations-CIK94AJf.mjs} +43 -23
  91. package/dist/migrations-CIK94AJf.mjs.map +1 -0
  92. package/dist/{output-BpcQrnnq.mjs → output-DnjfCC_u.mjs} +9 -3
  93. package/dist/output-DnjfCC_u.mjs.map +1 -0
  94. package/dist/{progress-adapter-DvQWB1nK.mjs → progress-adapter-xASh41wr.mjs} +2 -2
  95. package/dist/{progress-adapter-DvQWB1nK.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
  96. package/dist/{result-handler-Ba3zWQsI.mjs → result-handler-DWb1rFS-.mjs} +52 -27
  97. package/dist/result-handler-DWb1rFS-.mjs.map +1 -0
  98. package/dist/{terminal-ui-C3ZLwQxK.mjs → terminal-ui-zaRDhJnP.mjs} +2 -6
  99. package/dist/{terminal-ui-C3ZLwQxK.mjs.map → terminal-ui-zaRDhJnP.mjs.map} +1 -1
  100. package/dist/{verify-Bkycc-Tf.mjs → verify-BEIa9638.mjs} +3 -4
  101. package/dist/verify-BEIa9638.mjs.map +1 -0
  102. package/package.json +28 -26
  103. package/src/cli.ts +32 -6
  104. package/src/commands/contract-emit.ts +67 -163
  105. package/src/commands/contract-infer.ts +7 -20
  106. package/src/commands/db-init.ts +14 -3
  107. package/src/commands/db-update.ts +8 -4
  108. package/src/commands/db-verify.ts +47 -15
  109. package/src/commands/init/index.ts +1 -1
  110. package/src/commands/init/init.ts +2 -2
  111. package/src/commands/init/templates/code-templates.ts +12 -4
  112. package/src/commands/inspect-live-schema.ts +10 -5
  113. package/src/commands/migration-apply.ts +92 -71
  114. package/src/commands/migration-new.ts +42 -45
  115. package/src/commands/migration-plan.ts +147 -64
  116. package/src/commands/migration-ref.ts +8 -7
  117. package/src/commands/migration-show.ts +60 -41
  118. package/src/commands/migration-status.ts +196 -60
  119. package/src/config-path-validation.ts +0 -1
  120. package/src/control-api/client.ts +69 -1
  121. package/src/control-api/contract-enrichment.ts +6 -4
  122. package/src/control-api/operations/contract-emit.ts +198 -115
  123. package/src/control-api/operations/db-apply-aggregate.ts +446 -0
  124. package/src/control-api/operations/db-init.ts +51 -253
  125. package/src/control-api/operations/db-update.ts +66 -183
  126. package/src/control-api/operations/db-verify.ts +342 -0
  127. package/src/control-api/operations/migration-apply.ts +37 -9
  128. package/src/control-api/types.ts +125 -7
  129. package/src/exports/control-api.ts +15 -3
  130. package/src/load-ts-contract.ts +28 -26
  131. package/src/migration-cli.ts +445 -122
  132. package/src/utils/cli-errors.ts +49 -2
  133. package/src/utils/combine-schema-results.ts +84 -0
  134. package/src/utils/command-helpers.ts +69 -25
  135. package/src/utils/contract-space-aggregate-loader.ts +236 -0
  136. package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
  137. package/src/utils/contract-space-migrate-pass.ts +156 -0
  138. package/src/utils/emit-queue.ts +26 -0
  139. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  140. package/src/utils/formatters/migrations.ts +62 -26
  141. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  142. package/dist/cli-errors-BFYgBH3L.d.mts +0 -4
  143. package/dist/cli-errors-Cd79vmTH.mjs +0 -5
  144. package/dist/client-CrsnY58k.mjs +0 -997
  145. package/dist/client-CrsnY58k.mjs.map +0 -1
  146. package/dist/commands/db-verify.mjs.map +0 -1
  147. package/dist/commands/migration-plan.mjs.map +0 -1
  148. package/dist/config-loader-C25b63rJ.mjs.map +0 -1
  149. package/dist/contract-emit--feXyNd7.mjs +0 -4
  150. package/dist/contract-emit-NJ01hiiv.mjs +0 -195
  151. package/dist/contract-emit-NJ01hiiv.mjs.map +0 -1
  152. package/dist/contract-emit-V5SSitUT.mjs +0 -122
  153. package/dist/contract-emit-V5SSitUT.mjs.map +0 -1
  154. package/dist/contract-enrichment-CAOELa-H.mjs.map +0 -1
  155. package/dist/contract-infer-D9cC3rJm.mjs.map +0 -1
  156. package/dist/extract-operation-statements-DsFfxXVZ.mjs +0 -13
  157. package/dist/extract-operation-statements-DsFfxXVZ.mjs.map +0 -1
  158. package/dist/extract-sql-ddl-D9UbZDyz.mjs +0 -26
  159. package/dist/extract-sql-ddl-D9UbZDyz.mjs.map +0 -1
  160. package/dist/init-C5220SY9.mjs.map +0 -1
  161. package/dist/inspect-live-schema-yrHAvG71.mjs.map +0 -1
  162. package/dist/migration-command-scaffold-B3B09et6.mjs.map +0 -1
  163. package/dist/migration-status-DUMiH8_G.mjs.map +0 -1
  164. package/dist/migrations-Bo5WtTla.mjs.map +0 -1
  165. package/dist/output-BpcQrnnq.mjs.map +0 -1
  166. package/dist/result-handler-Ba3zWQsI.mjs.map +0 -1
  167. package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
  168. package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
  169. package/dist/verify-Bkycc-Tf.mjs.map +0 -1
  170. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  171. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -0,0 +1,156 @@
1
+ import {
2
+ detectSpaceContractDrift,
3
+ emitContractSpaceArtefacts,
4
+ readContractSpaceHeadRef,
5
+ type SpaceContractDriftResult,
6
+ } from '@prisma-next/migration-tools/spaces';
7
+
8
+ /**
9
+ * Minimal descriptor view consumed by the migrate-time per-space pass.
10
+ *
11
+ * The CLI receives descriptors typed against the SQL family (or any other
12
+ * family in the future); this helper only needs the structural shape of
13
+ * `contractSpace`, so it accepts an `unknown`-typed `contractJson` and
14
+ * a structurally-typed `headRef`. SQL-family callers pass the same
15
+ * `Contract<SqlStorage>` value through unchanged — `emitContractSpaceArtefacts`
16
+ * already serialises through `canonicalizeJson` and is framework-neutral.
17
+ *
18
+ * @see specs/framework-mechanism.spec.md § 3 — Per-space helper location.
19
+ */
20
+ export interface MigrateExtensionInput {
21
+ readonly id: string;
22
+ readonly contractSpace?: {
23
+ readonly contractJson: unknown;
24
+ readonly headRef: { readonly hash: string; readonly invariants: readonly string[] };
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Inputs needed to compose the migrate-time per-space pass at the CLI
30
+ * surface — typically called once after the app-space migration package
31
+ * has been written, regardless of whether the app-space had structural
32
+ * changes (an extension bump alone should still re-pin its artefacts).
33
+ */
34
+ export interface ContractSpaceMigratePassInputs {
35
+ readonly migrationsDir: string;
36
+ readonly extensionPacks: ReadonlyArray<MigrateExtensionInput>;
37
+ }
38
+
39
+ export interface ContractSpaceMigratePassResult {
40
+ readonly drifts: readonly SpaceContractDriftResult[];
41
+ readonly emittedSpaceIds: readonly string[];
42
+ }
43
+
44
+ /**
45
+ * Run drift detection + on-disk artefact emission for every loaded
46
+ * extension space at `migrate` time.
47
+ *
48
+ * Per sub-spec § 3:
49
+ *
50
+ * - For each declared extension that exposes a `contractSpace`:
51
+ * - Read the on-disk head hash from `migrations/<spaceId>/refs/head.json`
52
+ * (returns `null` on first emit).
53
+ * - Compare against the descriptor's `headRef.hash` via
54
+ * `detectSpaceContractDrift`. The `kind` discriminant decides whether
55
+ * the user sees a warning (`drift`), a no-op silent emit (`firstEmit`,
56
+ * `noDrift`), or nothing at all.
57
+ * - Always re-emit the on-disk artefacts (`contract.json`, `contract.d.ts`,
58
+ * `refs/head.json`). The framework owns these files and the helper is
59
+ * idempotent.
60
+ *
61
+ * Drift warnings are returned to the caller for formatting (TerminalUI,
62
+ * structured-output envelope, etc.) — the helper does not print directly,
63
+ * keeping it framework-neutral and unit-testable.
64
+ *
65
+ * Extension migration packages (the descriptor's pre-canned `migrations`
66
+ * array → `migrations/<spaceId>/<dirName>/`) are intentionally not
67
+ * materialised here — that interaction will be wired in a follow-on round
68
+ * once the runner-side single-tx slice (sub-spec § 6) is in place.
69
+ * On-disk artefacts are sufficient to lock the drift-warning behaviour
70
+ * and the always-on re-emit AC for R2.
71
+ *
72
+ * @see specs/framework-mechanism.spec.md § 3 — Drift detection (T1.9).
73
+ */
74
+ export async function runContractSpaceMigratePass(
75
+ inputs: ContractSpaceMigratePassInputs,
76
+ ): Promise<ContractSpaceMigratePassResult> {
77
+ const drifts: SpaceContractDriftResult[] = [];
78
+ const emittedSpaceIds: string[] = [];
79
+
80
+ for (const pack of inputs.extensionPacks) {
81
+ if (pack.contractSpace === undefined) continue;
82
+ const { contractJson, headRef } = pack.contractSpace;
83
+
84
+ const onDiskHeadRef = await readContractSpaceHeadRef(inputs.migrationsDir, pack.id);
85
+ const drift = detectSpaceContractDrift(pack.id, {
86
+ descriptorHash: headRef.hash,
87
+ priorHeadHash: onDiskHeadRef?.hash ?? null,
88
+ });
89
+ drifts.push(drift);
90
+
91
+ await emitContractSpaceArtefacts(inputs.migrationsDir, pack.id, {
92
+ contract: contractJson,
93
+ contractDts: buildPlaceholderContractDts(pack.id),
94
+ headRef: { hash: headRef.hash, invariants: headRef.invariants },
95
+ });
96
+ emittedSpaceIds.push(pack.id);
97
+ }
98
+
99
+ return { drifts, emittedSpaceIds };
100
+ }
101
+
102
+ /**
103
+ * Format the user-facing drift warning for a single space. Callers
104
+ * funnel this through their preferred output channel (TerminalUI line,
105
+ * structured-output envelope `warnings[]`, etc.).
106
+ *
107
+ * Locks AM7 — drift warning surfaces the extension name and the diff
108
+ * direction (descriptor → on-disk head).
109
+ */
110
+ export function formatContractSpaceDriftWarning(drift: SpaceContractDriftResult): string {
111
+ if (drift.kind !== 'drift') {
112
+ throw new Error(`formatContractSpaceDriftWarning called with non-drift result: ${drift.kind}`);
113
+ }
114
+ return (
115
+ `Contract-space drift detected for "${drift.spaceId}": descriptor hash ` +
116
+ `${drift.descriptorHash} differs from on-disk head hash ${drift.priorHeadHash ?? '<none>'}. ` +
117
+ `The on-disk artefacts under migrations/${drift.spaceId}/ will be refreshed to match the descriptor.`
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Placeholder `.d.ts` content for an extension space's on-disk mirror.
123
+ *
124
+ * Rendering a fully-typed `.d.ts` for an extension contract requires the
125
+ * SQL-family renderer with the codec / typemap registry threaded
126
+ * through; that integration is tracked under sub-spec Open Question 3
127
+ * (see `projects/extension-contract-spaces/specs/framework-mechanism.spec.md`).
128
+ *
129
+ * Until that ships, the on-disk `.d.ts` is a `@ts-nocheck` stub. The
130
+ * spec gap closing alongside the typed renderer is **AC2 / AC14**
131
+ * (byte-equivalence of per-space artefacts under `migrate`):
132
+ * a placeholder cannot be byte-equal to a fully-rendered `.d.ts` from
133
+ * the same descriptor, so AC2 / AC14 are PARTIAL today and become
134
+ * fully-PASS once OQ3 closes.
135
+ *
136
+ * Scheduled to close in **M3** (cipherstash editor tooling) — that's
137
+ * the milestone where the typed renderer gets its first real
138
+ * extension-space consumer and the byte-equivalence guarantee is
139
+ * practically required.
140
+ */
141
+ function buildPlaceholderContractDts(spaceId: string): string {
142
+ return [
143
+ '// @ts-nocheck',
144
+ '/**',
145
+ ` * Placeholder \`.d.ts\` for extension space "${spaceId}".`,
146
+ ' *',
147
+ ' * The framework re-emits this file on every `migrate` run alongside',
148
+ ' * `contract.json` and `refs/head.json`. A typed `.d.ts` rendering',
149
+ " * pass for extension contracts is tracked under the project's open",
150
+ ' * questions; until that ships, consumers should import',
151
+ ' * `contract.json` directly with `validateContract<…>(…)`.',
152
+ ' */',
153
+ 'export {};',
154
+ '',
155
+ ].join('\n');
156
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Per-output FIFO queue for `executeContractEmit`.
3
+ *
4
+ * Ensures that at most one emit (load → resolve source → emit bytes → publish)
5
+ * runs per output JSON path at a time. Concurrent calls for the same path
6
+ * line up behind the in-flight one and run in submission order; the user-visible
7
+ * outcome is "last submission wins on disk" without any supersession bookkeeping.
8
+ *
9
+ * Long-lived hosts (Vite dev server, watch CLIs) must call `disposeEmitQueue`
10
+ * when they stop publishing to a path, otherwise the module-global `Map`
11
+ * accumulates one entry per unique output path for the lifetime of the process.
12
+ */
13
+ const emitQueues = new Map<string, Promise<unknown>>();
14
+
15
+ export function queueEmitByOutput<T>(outputJsonPath: string, action: () => Promise<T>): Promise<T> {
16
+ const previous = emitQueues.get(outputJsonPath) ?? Promise.resolve();
17
+ // Continue regardless of the previous task's outcome — a failed emit must not
18
+ // block subsequent ones. The current task's outcome propagates via `next`.
19
+ const next = previous.then(action, action);
20
+ emitQueues.set(outputJsonPath, next);
21
+ return next;
22
+ }
23
+
24
+ export function disposeEmitQueue(outputJsonPath: string): void {
25
+ emitQueues.delete(outputJsonPath);
26
+ }
@@ -2,8 +2,8 @@
2
2
  * Maps MigrationGraph + status info to the generic graph renderer types.
3
3
  */
4
4
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
5
- import { findPath } from '@prisma-next/migration-tools/dag';
6
- import type { MigrationGraph } from '@prisma-next/migration-tools/types';
5
+ import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
6
+ import { findPath } from '@prisma-next/migration-tools/migration-graph';
7
7
  import { ifDefined } from '@prisma-next/utils/defined';
8
8
 
9
9
  import type { StatusRef } from '../migration-types';
@@ -106,7 +106,11 @@ export function migrationGraphToRenderInput(input: MigrationGraphInput): Migrati
106
106
  for (const entry of entries) {
107
107
  const status = statusByDirName.get(entry.dirName);
108
108
  const icon = status ? STATUS_ICON[status] : '';
109
- const label = `${entry.dirName}${icon}`;
109
+ const invariantsSuffix =
110
+ entry.invariants.length > 0
111
+ ? ` provides [${entry.invariants.map((id) => JSON.stringify(id)).join(', ')}]`
112
+ : '';
113
+ const label = `${entry.dirName}${icon}${invariantsSuffix}`;
110
114
 
111
115
  edgeList.push({
112
116
  from: toShortId(entry.from),
@@ -1,8 +1,41 @@
1
+ import type { OperationPreview } from '@prisma-next/framework-components/control';
1
2
  import { green, yellow } from 'colorette';
2
3
 
3
4
  import type { GlobalFlags } from '../global-flags';
4
5
  import { createColorFormatter, formatDim, isVerbose } from './helpers';
5
6
 
7
+ /**
8
+ * Render a single statement of an `OperationPreview` for the human-readable
9
+ * preview block. SQL statements get a trailing `;` if missing — matches the
10
+ * legacy `string[]`-based renderer byte-for-byte (per spec OQ-4). Other
11
+ * languages (`'mongodb-shell'`) render verbatim.
12
+ */
13
+ function renderPreviewStatement(text: string, language: string): string | undefined {
14
+ const trimmed = text.trim();
15
+ if (!trimmed) return undefined;
16
+ if (language === 'sql') {
17
+ return trimmed.endsWith(';') ? trimmed : `${trimmed};`;
18
+ }
19
+ return trimmed;
20
+ }
21
+
22
+ /**
23
+ * Choose the header label for a preview block. SQL-only previews keep the
24
+ * legacy `DDL preview` label (preserves CLI byte-identity for SQL targets per
25
+ * spec OQ-4); previews from any other family — or a mix that includes any
26
+ * non-SQL language — use the family-agnostic `Operation preview` label.
27
+ *
28
+ * An empty `statements` array deliberately renders as `Operation preview`
29
+ * rather than `DDL preview`: `Array.prototype.every` is vacuously true for
30
+ * empty arrays, but we have no evidence the preview is SQL-only when no
31
+ * statements are present, so the family-agnostic label is the safer default.
32
+ */
33
+ export function previewBlockHeader(preview: OperationPreview): string {
34
+ const allSql =
35
+ preview.statements.length > 0 && preview.statements.every((s) => s.language === 'sql');
36
+ return allSql ? 'DDL preview' : 'Operation preview';
37
+ }
38
+
6
39
  // ============================================================================
7
40
  // Migration Command Output Formatters (shared by db init and db update)
8
41
  // ============================================================================
@@ -24,7 +57,12 @@ export interface MigrationCommandResult {
24
57
  readonly label: string;
25
58
  readonly operationClass: string;
26
59
  }[];
27
- readonly sql?: readonly string[];
60
+ /**
61
+ * Family-agnostic textual preview of the planned operations. Replaces the
62
+ * previous `sql?: readonly string[]`. Consumers should read
63
+ * `plan.preview?.statements`.
64
+ */
65
+ readonly preview?: OperationPreview;
28
66
  };
29
67
  readonly execution?: {
30
68
  readonly operationsPlanned: number;
@@ -92,20 +130,20 @@ export function formatMigrationPlanOutput(
92
130
  lines.push(`${formatDimText(`Destination hash: ${result.plan.destination.storageHash}`)}`);
93
131
  }
94
132
 
95
- // SQL DDL preview (SQL family only)
96
- const planSql = result.plan?.sql;
97
- if (planSql) {
133
+ // Statement preview (any family that implements OperationPreviewCapable)
134
+ const preview = result.plan?.preview;
135
+ if (preview) {
98
136
  lines.push('');
99
- lines.push(`${formatDimText('DDL preview')}`);
100
- if (planSql.length === 0) {
101
- lines.push(`${formatDimText('No DDL operations.')}`);
137
+ lines.push(`${formatDimText(previewBlockHeader(preview))}`);
138
+ if (preview.statements.length === 0) {
139
+ lines.push(`${formatDimText('No operations.')}`);
102
140
  } else {
103
141
  lines.push('');
104
- for (const statement of planSql) {
105
- const trimmed = statement.trim();
106
- if (!trimmed) continue;
107
- const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
108
- lines.push(`${line}`);
142
+ for (const statement of preview.statements) {
143
+ const rendered = renderPreviewStatement(statement.text, statement.language);
144
+ if (rendered) {
145
+ lines.push(rendered);
146
+ }
109
147
  }
110
148
  }
111
149
  }
@@ -181,17 +219,16 @@ export function formatMigrationApplyCommandOutput(
181
219
  interface MigrationShowResult {
182
220
  readonly dirName: string;
183
221
  readonly dirPath: string;
184
- readonly from: string;
222
+ readonly from: string | null;
185
223
  readonly to: string;
186
- readonly migrationId: string;
187
- readonly kind: string;
224
+ readonly migrationHash: string;
188
225
  readonly createdAt: string;
189
226
  readonly operations: readonly {
190
227
  readonly id: string;
191
228
  readonly label: string;
192
229
  readonly operationClass: string;
193
230
  }[];
194
- readonly sql: readonly string[];
231
+ readonly preview: OperationPreview;
195
232
  readonly summary: string;
196
233
  }
197
234
 
@@ -208,10 +245,9 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
208
245
  const formatDimText = (text: string) => formatDim(useColor, text);
209
246
 
210
247
  lines.push(`${formatGreen('✔')} ${result.dirName}`);
211
- lines.push(`${formatDimText(` kind: ${result.kind}`)}`);
212
- lines.push(`${formatDimText(` from: ${result.from}`)}`);
248
+ lines.push(`${formatDimText(` from: ${result.from ?? '(baseline)'}`)}`);
213
249
  lines.push(`${formatDimText(` to: ${result.to}`)}`);
214
- lines.push(`${formatDimText(` migrationId: ${result.migrationId}`)}`);
250
+ lines.push(`${formatDimText(` migrationHash: ${result.migrationHash}`)}`);
215
251
  lines.push(`${formatDimText(` created: ${result.createdAt}`)}`);
216
252
 
217
253
  lines.push('');
@@ -239,15 +275,15 @@ export function formatMigrationShowOutput(result: MigrationShowResult, flags: Gl
239
275
  }
240
276
  }
241
277
 
242
- if (result.sql.length > 0) {
278
+ if (result.preview.statements.length > 0) {
243
279
  lines.push('');
244
- lines.push(`${formatDimText('DDL preview')}`);
280
+ lines.push(`${formatDimText(previewBlockHeader(result.preview))}`);
245
281
  lines.push('');
246
- for (const statement of result.sql) {
247
- const trimmed = statement.trim();
248
- if (!trimmed) continue;
249
- const line = trimmed.endsWith(';') ? trimmed : `${trimmed};`;
250
- lines.push(`${line}`);
282
+ for (const statement of result.preview.statements) {
283
+ const rendered = renderPreviewStatement(statement.text, statement.language);
284
+ if (rendered) {
285
+ lines.push(rendered);
286
+ }
251
287
  }
252
288
  }
253
289
 
@@ -0,0 +1,134 @@
1
+ import { readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { basename, dirname, join } from 'pathe';
3
+
4
+ function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === 'object' && value !== null;
6
+ }
7
+
8
+ function createTempArtifactPath(path: string, publicationToken: string, phase: string): string {
9
+ return join(dirname(path), `.${basename(path)}.${process.pid}.${publicationToken}.${phase}.tmp`);
10
+ }
11
+
12
+ type PreviousArtifact = { readonly content: string } | 'remove';
13
+
14
+ async function readExistingArtifact(path: string): Promise<PreviousArtifact> {
15
+ try {
16
+ return { content: await readFile(path, 'utf-8') };
17
+ } catch (error) {
18
+ if (isRecord(error) && error['code'] === 'ENOENT') {
19
+ return 'remove';
20
+ }
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ async function restoreArtifact(
26
+ path: string,
27
+ previous: PreviousArtifact,
28
+ publicationToken: string,
29
+ ): Promise<void> {
30
+ if (previous === 'remove') {
31
+ await rm(path, { force: true });
32
+ return;
33
+ }
34
+
35
+ const restorePath = createTempArtifactPath(path, publicationToken, 'rollback');
36
+ await writeFile(restorePath, previous.content, 'utf-8');
37
+ try {
38
+ await rename(restorePath, path);
39
+ } finally {
40
+ await rm(restorePath, { force: true });
41
+ }
42
+ }
43
+
44
+ interface PublishEntry {
45
+ readonly tempPath: string;
46
+ readonly outputPath: string;
47
+ readonly previous: PreviousArtifact;
48
+ }
49
+
50
+ function withRollbackFailureCause(error: unknown, rollbackFailures: readonly unknown[]): Error {
51
+ const rollbackCause = new AggregateError(
52
+ rollbackFailures,
53
+ 'Failed to restore published artifacts',
54
+ );
55
+
56
+ if (error instanceof Error) {
57
+ Object.defineProperty(error, 'cause', {
58
+ value: rollbackCause,
59
+ configurable: true,
60
+ writable: true,
61
+ });
62
+ return error;
63
+ }
64
+
65
+ return new Error(String(error), { cause: rollbackCause });
66
+ }
67
+
68
+ async function publishPairWithRollback(
69
+ entries: readonly PublishEntry[],
70
+ publicationToken: string,
71
+ ): Promise<void> {
72
+ const replaced: PublishEntry[] = [];
73
+ try {
74
+ for (const entry of entries) {
75
+ await rename(entry.tempPath, entry.outputPath);
76
+ replaced.push(entry);
77
+ }
78
+ } catch (error) {
79
+ const rollbackResults = await Promise.allSettled(
80
+ replaced.map((entry) => restoreArtifact(entry.outputPath, entry.previous, publicationToken)),
81
+ );
82
+ const rollbackFailures = rollbackResults.flatMap((result) =>
83
+ result.status === 'rejected' ? [result.reason] : [],
84
+ );
85
+
86
+ if (rollbackFailures.length > 0) {
87
+ throw withRollbackFailureCause(error, rollbackFailures);
88
+ }
89
+
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ export async function publishContractArtifactPair({
95
+ outputJsonPath,
96
+ outputDtsPath,
97
+ contractJson,
98
+ contractDts,
99
+ publicationToken,
100
+ beforePublish,
101
+ }: {
102
+ readonly outputJsonPath: string;
103
+ readonly outputDtsPath: string;
104
+ readonly contractJson: string;
105
+ readonly contractDts: string;
106
+ readonly publicationToken: string;
107
+ readonly beforePublish?: () => Promise<boolean> | boolean;
108
+ }): Promise<boolean> {
109
+ const tempJsonPath = createTempArtifactPath(outputJsonPath, publicationToken, 'next');
110
+ const tempDtsPath = createTempArtifactPath(outputDtsPath, publicationToken, 'next');
111
+
112
+ try {
113
+ await writeFile(tempJsonPath, contractJson, 'utf-8');
114
+ await writeFile(tempDtsPath, contractDts, 'utf-8');
115
+
116
+ if ((await beforePublish?.()) === false) {
117
+ return false;
118
+ }
119
+
120
+ const previousJson = await readExistingArtifact(outputJsonPath);
121
+ const previousDts = await readExistingArtifact(outputDtsPath);
122
+
123
+ await publishPairWithRollback(
124
+ [
125
+ { tempPath: tempDtsPath, outputPath: outputDtsPath, previous: previousDts },
126
+ { tempPath: tempJsonPath, outputPath: outputJsonPath, previous: previousJson },
127
+ ],
128
+ publicationToken,
129
+ );
130
+ return true;
131
+ } finally {
132
+ await Promise.allSettled([rm(tempJsonPath, { force: true }), rm(tempDtsPath, { force: true })]);
133
+ }
134
+ }
@@ -1,4 +0,0 @@
1
- import { CliStructuredError as CliStructuredError$1 } from "@prisma-next/errors/control";
2
- import "@prisma-next/errors/execution";
3
- import "@prisma-next/errors/migration";
4
- export { CliStructuredError$1 as t };
@@ -1,5 +0,0 @@
1
- import { CliStructuredError as CliStructuredError$1, errorConfigValidation as errorConfigValidation$1, errorContractConfigMissing as errorContractConfigMissing$1, errorContractValidationFailed, errorDatabaseConnectionRequired, errorDriverRequired, errorFileNotFound, errorMigrationPlanningFailed, errorTargetMigrationNotSupported, errorUnexpected as errorUnexpected$1 } from "@prisma-next/errors/control";
2
- import { ERROR_CODE_DESTRUCTIVE_CHANGES, errorDestructiveChanges, errorHashMismatch, errorMarkerMissing, errorRunnerFailed, errorRuntime as errorRuntime$1, errorTargetMismatch } from "@prisma-next/errors/execution";
3
- import "@prisma-next/errors/migration";
4
-
5
- export { errorUnexpected$1 as _, errorContractValidationFailed as a, errorDriverRequired as c, errorMarkerMissing as d, errorMigrationPlanningFailed as f, errorTargetMismatch as g, errorTargetMigrationNotSupported as h, errorContractConfigMissing$1 as i, errorFileNotFound as l, errorRuntime$1 as m, ERROR_CODE_DESTRUCTIVE_CHANGES as n, errorDatabaseConnectionRequired as o, errorRunnerFailed as p, errorConfigValidation$1 as r, errorDestructiveChanges as s, CliStructuredError$1 as t, errorHashMismatch as u };