@prisma-next/cli 0.5.0-dev.4 → 0.5.0-dev.40

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 (165) hide show
  1. package/README.md +56 -21
  2. package/dist/agent-skill-mongo.md +63 -31
  3. package/dist/agent-skill-postgres.md +1 -1
  4. package/dist/cli-errors-By1iVE3z.mjs +34 -0
  5. package/dist/cli-errors-By1iVE3z.mjs.map +1 -0
  6. package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-DDeVsP2Y.d.mts} +1 -0
  7. package/dist/cli.mjs +123 -15
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/{client-TG7rbCWT.mjs → client-1JqqkiC7.mjs} +45 -20
  10. package/dist/client-1JqqkiC7.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -1
  12. package/dist/commands/contract-emit.mjs +2 -2
  13. package/dist/commands/contract-infer.d.mts.map +1 -1
  14. package/dist/commands/contract-infer.mjs +2 -2
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +10 -9
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.mjs +5 -5
  19. package/dist/commands/db-sign.mjs +7 -7
  20. package/dist/commands/db-update.mjs +9 -9
  21. package/dist/commands/db-update.mjs.map +1 -1
  22. package/dist/commands/db-verify.mjs +9 -9
  23. package/dist/commands/migration-apply.d.mts +5 -2
  24. package/dist/commands/migration-apply.d.mts.map +1 -1
  25. package/dist/commands/migration-apply.mjs +55 -56
  26. package/dist/commands/migration-apply.mjs.map +1 -1
  27. package/dist/commands/migration-new.d.mts.map +1 -1
  28. package/dist/commands/migration-new.mjs +26 -32
  29. package/dist/commands/migration-new.mjs.map +1 -1
  30. package/dist/commands/migration-plan.d.mts +14 -5
  31. package/dist/commands/migration-plan.d.mts.map +1 -1
  32. package/dist/commands/migration-plan.mjs +45 -48
  33. package/dist/commands/migration-plan.mjs.map +1 -1
  34. package/dist/commands/migration-ref.d.mts +1 -1
  35. package/dist/commands/migration-ref.d.mts.map +1 -1
  36. package/dist/commands/migration-ref.mjs +6 -10
  37. package/dist/commands/migration-ref.mjs.map +1 -1
  38. package/dist/commands/migration-show.d.mts +13 -7
  39. package/dist/commands/migration-show.d.mts.map +1 -1
  40. package/dist/commands/migration-show.mjs +27 -29
  41. package/dist/commands/migration-show.mjs.map +1 -1
  42. package/dist/commands/migration-status.d.mts +23 -5
  43. package/dist/commands/migration-status.d.mts.map +1 -1
  44. package/dist/commands/migration-status.mjs +3 -3
  45. package/dist/{config-loader-_W4T21X1.mjs → config-loader-ih8ViDb_.mjs} +2 -2
  46. package/dist/config-loader-ih8ViDb_.mjs.map +1 -0
  47. package/dist/config-loader.mjs +1 -1
  48. package/dist/contract-emit-LjzCoicC.mjs +4 -0
  49. package/dist/contract-emit-RZBWzkop.mjs +329 -0
  50. package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
  51. package/dist/contract-emit-rt_Nmdwq.mjs +150 -0
  52. package/dist/contract-emit-rt_Nmdwq.mjs.map +1 -0
  53. package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
  54. package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
  55. package/dist/{contract-infer-BS4kIX9c.mjs → contract-infer-Cf5J2wVg.mjs} +11 -19
  56. package/dist/contract-infer-Cf5J2wVg.mjs.map +1 -0
  57. package/dist/exports/control-api.d.mts +86 -21
  58. package/dist/exports/control-api.d.mts.map +1 -1
  59. package/dist/exports/control-api.mjs +5 -5
  60. package/dist/exports/index.mjs +3 -3
  61. package/dist/exports/init-output.d.mts +39 -0
  62. package/dist/exports/init-output.d.mts.map +1 -0
  63. package/dist/exports/init-output.mjs +3 -0
  64. package/dist/{framework-components-DfZKQBQ2.mjs → framework-components-Bgcre3Z6.mjs} +2 -2
  65. package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
  66. package/dist/init-C7dE9KOJ.mjs +2062 -0
  67. package/dist/init-C7dE9KOJ.mjs.map +1 -0
  68. package/dist/{inspect-live-schema-BsoFVoS1.mjs → inspect-live-schema-LWtXfxm_.mjs} +9 -9
  69. package/dist/inspect-live-schema-LWtXfxm_.mjs.map +1 -0
  70. package/dist/migration-cli.d.mts +41 -11
  71. package/dist/migration-cli.d.mts.map +1 -1
  72. package/dist/migration-cli.mjs +308 -84
  73. package/dist/migration-cli.mjs.map +1 -1
  74. package/dist/{migration-command-scaffold-DOXnheFa.mjs → migration-command-scaffold-CU452v9h.mjs} +7 -7
  75. package/dist/{migration-command-scaffold-DOXnheFa.mjs.map → migration-command-scaffold-CU452v9h.mjs.map} +1 -1
  76. package/dist/{migration-status-Ry3TnEya.mjs → migration-status-DoPrFIOQ.mjs} +114 -57
  77. package/dist/migration-status-DoPrFIOQ.mjs.map +1 -0
  78. package/dist/{migrations-fU0xoKjS.mjs → migrations-MEoKMiV5.mjs} +42 -21
  79. package/dist/migrations-MEoKMiV5.mjs.map +1 -0
  80. package/dist/output-BpcQrnnq.mjs +103 -0
  81. package/dist/output-BpcQrnnq.mjs.map +1 -0
  82. package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DgRGldpT.mjs} +1 -1
  83. package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
  84. package/dist/quick-reference-mongo.md +34 -13
  85. package/dist/quick-reference-postgres.md +11 -9
  86. package/dist/{result-handler-BJwA7ufw.mjs → result-handler-Ch6hVnOo.mjs} +35 -93
  87. package/dist/result-handler-Ch6hVnOo.mjs.map +1 -0
  88. package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-u2YgKghu.mjs} +76 -2
  89. package/dist/terminal-ui-u2YgKghu.mjs.map +1 -0
  90. package/dist/{verify-bl__PkXk.mjs → verify-BT9tgCOH.mjs} +2 -2
  91. package/dist/{verify-bl__PkXk.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
  92. package/package.json +23 -17
  93. package/src/cli.ts +32 -6
  94. package/src/commands/contract-emit.ts +67 -163
  95. package/src/commands/contract-infer.ts +7 -20
  96. package/src/commands/db-init.ts +1 -0
  97. package/src/commands/db-update.ts +1 -1
  98. package/src/commands/init/detect-pnpm-catalog.ts +141 -0
  99. package/src/commands/init/errors.ts +254 -0
  100. package/src/commands/init/exit-codes.ts +62 -0
  101. package/src/commands/init/hygiene-gitattributes.ts +97 -0
  102. package/src/commands/init/hygiene-gitignore.ts +48 -0
  103. package/src/commands/init/hygiene-package-scripts.ts +91 -0
  104. package/src/commands/init/index.ts +112 -7
  105. package/src/commands/init/init.ts +766 -144
  106. package/src/commands/init/inputs.ts +421 -0
  107. package/src/commands/init/output.ts +147 -0
  108. package/src/commands/init/probe-db.ts +308 -0
  109. package/src/commands/init/reinit-cleanup.ts +83 -0
  110. package/src/commands/init/templates/agent-skill-mongo.md +63 -31
  111. package/src/commands/init/templates/agent-skill-postgres.md +1 -1
  112. package/src/commands/init/templates/agent-skill.ts +25 -3
  113. package/src/commands/init/templates/code-templates.ts +125 -32
  114. package/src/commands/init/templates/env.ts +80 -0
  115. package/src/commands/init/templates/quick-reference-mongo.md +34 -13
  116. package/src/commands/init/templates/quick-reference-postgres.md +11 -9
  117. package/src/commands/init/templates/quick-reference.ts +42 -3
  118. package/src/commands/init/templates/tsconfig.ts +167 -5
  119. package/src/commands/inspect-live-schema.ts +10 -5
  120. package/src/commands/migration-apply.ts +84 -63
  121. package/src/commands/migration-new.ts +28 -34
  122. package/src/commands/migration-plan.ts +80 -56
  123. package/src/commands/migration-ref.ts +8 -7
  124. package/src/commands/migration-show.ts +53 -36
  125. package/src/commands/migration-status.ts +194 -58
  126. package/src/config-path-validation.ts +0 -1
  127. package/src/control-api/client.ts +21 -0
  128. package/src/control-api/operations/contract-emit.ts +198 -115
  129. package/src/control-api/operations/db-init.ts +10 -6
  130. package/src/control-api/operations/db-update.ts +10 -6
  131. package/src/control-api/operations/migration-apply.ts +30 -9
  132. package/src/control-api/types.ts +69 -7
  133. package/src/exports/control-api.ts +2 -1
  134. package/src/exports/init-output.ts +10 -0
  135. package/src/migration-cli.ts +445 -122
  136. package/src/utils/cli-errors.ts +49 -2
  137. package/src/utils/command-helpers.ts +45 -23
  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-DHq6GQGu.mjs +0 -5
  143. package/dist/client-TG7rbCWT.mjs.map +0 -1
  144. package/dist/config-loader-_W4T21X1.mjs.map +0 -1
  145. package/dist/contract-emit-CQfj7xJn.mjs +0 -122
  146. package/dist/contract-emit-CQfj7xJn.mjs.map +0 -1
  147. package/dist/contract-emit-DpPjuFy-.mjs +0 -195
  148. package/dist/contract-emit-DpPjuFy-.mjs.map +0 -1
  149. package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
  150. package/dist/contract-infer-BS4kIX9c.mjs.map +0 -1
  151. package/dist/extract-operation-statements-DZUJNmL3.mjs +0 -13
  152. package/dist/extract-operation-statements-DZUJNmL3.mjs.map +0 -1
  153. package/dist/extract-sql-ddl-DDMX-9mz.mjs +0 -26
  154. package/dist/extract-sql-ddl-DDMX-9mz.mjs.map +0 -1
  155. package/dist/init-CQfo_4Ro.mjs +0 -430
  156. package/dist/init-CQfo_4Ro.mjs.map +0 -1
  157. package/dist/inspect-live-schema-BsoFVoS1.mjs.map +0 -1
  158. package/dist/migration-status-Ry3TnEya.mjs.map +0 -1
  159. package/dist/migrations-fU0xoKjS.mjs.map +0 -1
  160. package/dist/result-handler-BJwA7ufw.mjs.map +0 -1
  161. package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
  162. package/dist/validate-contract-deps-esa-VQ0h.mjs +0 -37
  163. package/dist/validate-contract-deps-esa-VQ0h.mjs.map +0 -1
  164. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  165. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Re-export all domain error factories from @prisma-next/errors for convenience.
3
- * CLI-specific errors (e.g., Commander.js argument validation) can be added here if needed.
3
+ * CLI-specific errors (e.g., Commander argument validation in the main CLI, or
4
+ * clipanion parse errors in the migration-file CLI) can be added here if needed.
4
5
  */
5
6
  export type { CliErrorConflict, CliErrorEnvelope } from '@prisma-next/errors/control';
6
- export {
7
+
8
+ import {
7
9
  CliStructuredError,
8
10
  errorConfigFileNotFound,
9
11
  errorConfigValidation,
@@ -15,11 +17,33 @@ export {
15
17
  errorFamilyReadMarkerSqlRequired,
16
18
  errorFileNotFound,
17
19
  errorMigrationCliInvalidConfigArg,
20
+ errorMigrationCliUnknownFlag,
18
21
  errorMigrationPlanningFailed,
19
22
  errorQueryRunnerFactoryRequired,
20
23
  errorTargetMigrationNotSupported,
21
24
  errorUnexpected,
22
25
  } from '@prisma-next/errors/control';
26
+ import { errorRuntime } from '@prisma-next/errors/execution';
27
+ import type { MigrationToolsError } from '@prisma-next/migration-tools/errors';
28
+
29
+ export {
30
+ CliStructuredError,
31
+ errorConfigFileNotFound,
32
+ errorConfigValidation,
33
+ errorContractConfigMissing,
34
+ errorContractMissingExtensionPacks,
35
+ errorContractValidationFailed,
36
+ errorDatabaseConnectionRequired,
37
+ errorDriverRequired,
38
+ errorFamilyReadMarkerSqlRequired,
39
+ errorFileNotFound,
40
+ errorMigrationCliInvalidConfigArg,
41
+ errorMigrationCliUnknownFlag,
42
+ errorMigrationPlanningFailed,
43
+ errorQueryRunnerFactoryRequired,
44
+ errorTargetMigrationNotSupported,
45
+ errorUnexpected,
46
+ };
23
47
  export {
24
48
  ERROR_CODE_DESTRUCTIVE_CHANGES,
25
49
  errorDestructiveChanges,
@@ -38,3 +62,26 @@ export {
38
62
  errorUnfilledPlaceholder,
39
63
  placeholder,
40
64
  } from '@prisma-next/errors/migration';
65
+
66
+ /**
67
+ * Maps a `MigrationToolsError` raised by the migration-tools loader/graph
68
+ * surface (`readMigrationPackage`, `readMigrationsDir`, `readRefs`,
69
+ * `resolveRef`, `reconstructGraph`, ...) into a CLI `errorRuntime` envelope.
70
+ *
71
+ * The full `error.details` payload is forwarded into `meta` so machine
72
+ * consumers (`--json`) see structural fields like `dir`, `storedHash`,
73
+ * `computedHash` (for `MIGRATION.HASH_MISMATCH`) alongside the stable
74
+ * `code`. The user-visible `summary`/`why`/`fix` text is unchanged.
75
+ *
76
+ * Callers are expected to gate on `MigrationToolsError.is(error)` first
77
+ * (mirroring the original inline pattern); non-`MigrationToolsError`
78
+ * values are caller-classified (rethrow, wrap with command-specific
79
+ * `errorUnexpected`, etc.).
80
+ */
81
+ export function mapMigrationToolsError(error: MigrationToolsError): CliStructuredError {
82
+ return errorRuntime(error.message, {
83
+ why: error.why,
84
+ fix: error.fix,
85
+ meta: { code: error.code, ...(error.details ?? {}) },
86
+ });
87
+ }
@@ -1,10 +1,12 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import type { ControlTargetDescriptor } from '@prisma-next/framework-components/control';
3
3
  import { hasMigrations } from '@prisma-next/framework-components/control';
4
- import type { PathDecision } from '@prisma-next/migration-tools/dag';
5
- import { reconstructGraph } from '@prisma-next/migration-tools/dag';
4
+ import type { NoInvariantPathStructuralEdge } from '@prisma-next/migration-tools/errors';
5
+ import type { MigrationEdge, MigrationGraph } from '@prisma-next/migration-tools/graph';
6
6
  import { readMigrationsDir } from '@prisma-next/migration-tools/io';
7
- import type { MigrationBundle, MigrationGraph } from '@prisma-next/migration-tools/types';
7
+ import type { PathDecision } from '@prisma-next/migration-tools/migration-graph';
8
+ import { reconstructGraph } from '@prisma-next/migration-tools/migration-graph';
9
+ import type { MigrationPackage } from '@prisma-next/migration-tools/package';
8
10
  import { ifDefined } from '@prisma-next/utils/defined';
9
11
  import type { Command } from 'commander';
10
12
  import { relative, resolve } from 'pathe';
@@ -109,14 +111,45 @@ export interface PathDecisionResult {
109
111
  readonly alternativeCount: number;
110
112
  readonly tieBreakReasons: readonly string[];
111
113
  readonly refName?: string;
114
+ readonly requiredInvariants: readonly string[];
115
+ readonly satisfiedInvariants: readonly string[];
112
116
  readonly selectedPath: readonly {
113
117
  readonly dirName: string;
114
- readonly migrationId: string;
118
+ readonly migrationHash: string;
115
119
  readonly from: string;
116
120
  readonly to: string;
121
+ readonly invariants: readonly string[];
117
122
  }[];
118
123
  }
119
124
 
125
+ export function collectDeclaredInvariants(graph: MigrationGraph): ReadonlySet<string> {
126
+ const declared = new Set<string>();
127
+ for (const edges of graph.forwardChain.values()) {
128
+ for (const edge of edges) {
129
+ for (const inv of edge.invariants) {
130
+ declared.add(inv);
131
+ }
132
+ }
133
+ }
134
+ return declared;
135
+ }
136
+
137
+ /**
138
+ * Maps a `MigrationEdge` to the structural-edge shape used in the
139
+ * `MIGRATION.NO_INVARIANT_PATH` error envelope. Shared between
140
+ * `migration apply` and `migration status` so both commands surface
141
+ * the same JSON wire shape when an invariant-aware route is unsatisfiable.
142
+ */
143
+ export function toStructuralEdge(edge: MigrationEdge): NoInvariantPathStructuralEdge {
144
+ return {
145
+ dirName: edge.dirName,
146
+ migrationHash: edge.migrationHash,
147
+ from: edge.from,
148
+ to: edge.to,
149
+ invariants: edge.invariants,
150
+ };
151
+ }
152
+
120
153
  /**
121
154
  * Maps a PathDecision to the slim CLI output representation.
122
155
  */
@@ -126,12 +159,15 @@ export function toPathDecisionResult(decision: PathDecision): PathDecisionResult
126
159
  toHash: decision.toHash,
127
160
  alternativeCount: decision.alternativeCount,
128
161
  tieBreakReasons: decision.tieBreakReasons,
162
+ requiredInvariants: decision.requiredInvariants ?? [],
163
+ satisfiedInvariants: decision.satisfiedInvariants ?? [],
129
164
  ...ifDefined('refName', decision.refName),
130
165
  selectedPath: decision.selectedPath.map((entry) => ({
131
166
  dirName: entry.dirName,
132
- migrationId: entry.migrationId,
167
+ migrationHash: entry.migrationHash,
133
168
  from: entry.from,
134
169
  to: entry.to,
170
+ invariants: entry.invariants,
135
171
  })),
136
172
  };
137
173
  }
@@ -146,13 +182,13 @@ export function getTargetMigrations(target: ControlTargetDescriptor<string, stri
146
182
 
147
183
  /**
148
184
  * Reads the migrations directory and builds the migration graph from all
149
- * bundles. Throws on I/O or graph errors — callers handle error mapping.
185
+ * packages. Throws on I/O or graph errors — callers handle error mapping.
150
186
  *
151
- * Every on-disk bundle is content-addressed (`migrationId` is always a
187
+ * Every on-disk package is content-addressed (`migrationHash` is always a
152
188
  * string); there is no draft state to filter out.
153
189
  */
154
- export async function loadMigrationBundles(migrationsDir: string): Promise<{
155
- bundles: readonly MigrationBundle[];
190
+ export async function loadMigrationPackages(migrationsDir: string): Promise<{
191
+ bundles: readonly MigrationPackage[];
156
192
  graph: MigrationGraph;
157
193
  }> {
158
194
  const bundles = await readMigrationsDir(migrationsDir);
@@ -160,20 +196,6 @@ export async function loadMigrationBundles(migrationsDir: string): Promise<{
160
196
  return { bundles, graph };
161
197
  }
162
198
 
163
- export interface MigrationBundleSet {
164
- readonly bundles: readonly MigrationBundle[];
165
- readonly graph: MigrationGraph;
166
- }
167
-
168
- /**
169
- * Alias of `loadMigrationBundles` retained for naming-clarity in commands
170
- * that previously needed both attested and draft splits. With the
171
- * collapse of the draft state, both helpers do the same thing.
172
- */
173
- export async function loadAllBundles(migrationsDir: string): Promise<MigrationBundleSet> {
174
- return loadMigrationBundles(migrationsDir);
175
- }
176
-
177
199
  /**
178
200
  * The subset of the emitted contract.json that the framework layer can
179
201
  * safely type. The emitter adds these fields on top of the family-specific
@@ -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,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 };