@prisma-next/cli 0.12.0-dev.51 → 0.12.0-dev.53

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 (65) hide show
  1. package/dist/cli.mjs +7 -7
  2. package/dist/{client-DC-UlBLy.mjs → client-DIcitJdy.mjs} +113 -49
  3. package/dist/client-DIcitJdy.mjs.map +1 -0
  4. package/dist/commands/contract-infer.mjs +1 -1
  5. package/dist/commands/db-init.mjs +2 -2
  6. package/dist/commands/db-schema.mjs +1 -1
  7. package/dist/commands/db-sign.mjs +1 -1
  8. package/dist/commands/db-update.mjs +2 -2
  9. package/dist/commands/db-verify.mjs +1 -1
  10. package/dist/commands/migrate.d.mts +35 -1
  11. package/dist/commands/migrate.d.mts.map +1 -1
  12. package/dist/commands/migrate.mjs +287 -6
  13. package/dist/commands/migrate.mjs.map +1 -1
  14. package/dist/commands/migration-check.mjs +2 -2
  15. package/dist/commands/migration-graph.d.mts.map +1 -1
  16. package/dist/commands/migration-graph.mjs +4 -2
  17. package/dist/commands/migration-graph.mjs.map +1 -1
  18. package/dist/commands/migration-list.d.mts +1 -0
  19. package/dist/commands/migration-list.d.mts.map +1 -1
  20. package/dist/commands/migration-list.mjs +1 -1
  21. package/dist/commands/migration-log.mjs +1 -1
  22. package/dist/commands/migration-show.mjs +2 -2
  23. package/dist/commands/migration-status.d.mts.map +1 -1
  24. package/dist/commands/migration-status.mjs +2 -2
  25. package/dist/{contract-infer-DpGN9SAj.mjs → contract-infer-BAdhYGQH.mjs} +2 -2
  26. package/dist/{contract-infer-DpGN9SAj.mjs.map → contract-infer-BAdhYGQH.mjs.map} +1 -1
  27. package/dist/{db-verify-Cq16Obsw.mjs → db-verify-CiUCDXnv.mjs} +2 -2
  28. package/dist/{db-verify-Cq16Obsw.mjs.map → db-verify-CiUCDXnv.mjs.map} +1 -1
  29. package/dist/exports/control-api.mjs +1 -1
  30. package/dist/{inspect-live-schema-CRDKTNcf.mjs → inspect-live-schema-DegaqKFT.mjs} +2 -2
  31. package/dist/{inspect-live-schema-CRDKTNcf.mjs.map → inspect-live-schema-DegaqKFT.mjs.map} +1 -1
  32. package/dist/{migration-check-BxWlQBOs.mjs → migration-check-B2ccCHe7.mjs} +3 -3
  33. package/dist/{migration-check-BxWlQBOs.mjs.map → migration-check-B2ccCHe7.mjs.map} +1 -1
  34. package/dist/{migration-command-scaffold-BDd9abqW.mjs → migration-command-scaffold-D6UeN71F.mjs} +2 -2
  35. package/dist/{migration-command-scaffold-BDd9abqW.mjs.map → migration-command-scaffold-D6UeN71F.mjs.map} +1 -1
  36. package/dist/{migration-graph-space-render-CeNXh_Wy.mjs → migration-graph-space-render-B0HkTNj3.mjs} +488 -84
  37. package/dist/migration-graph-space-render-B0HkTNj3.mjs.map +1 -0
  38. package/dist/{migration-list-vJWFuXca.mjs → migration-list-mYmj2j33.mjs} +6 -4
  39. package/dist/migration-list-mYmj2j33.mjs.map +1 -0
  40. package/dist/{migration-log-6rcHQSI4.mjs → migration-log-Dzs18GU7.mjs} +3 -3
  41. package/dist/{migration-log-6rcHQSI4.mjs.map → migration-log-Dzs18GU7.mjs.map} +1 -1
  42. package/dist/{migration-path-target-UkxkgXnv.mjs → migration-path-target-DK-B7POa.mjs} +1 -1
  43. package/dist/{migration-path-target-UkxkgXnv.mjs.map → migration-path-target-DK-B7POa.mjs.map} +1 -1
  44. package/dist/{migration-status-Bjv91dE7.mjs → migration-status-BT9eCQsf.mjs} +8 -5
  45. package/dist/migration-status-BT9eCQsf.mjs.map +1 -0
  46. package/dist/{schemas-DJY2O09F.mjs → schemas-B4xeMrNt.mjs} +1 -1
  47. package/dist/{schemas-DJY2O09F.mjs.map → schemas-B4xeMrNt.mjs.map} +1 -1
  48. package/dist/types-qV41eEXH.d.mts.map +1 -1
  49. package/package.json +18 -18
  50. package/src/commands/migrate.ts +512 -2
  51. package/src/commands/migration-graph.ts +2 -0
  52. package/src/commands/migration-list.ts +3 -0
  53. package/src/commands/migration-status.ts +4 -0
  54. package/src/control-api/operations/db-run.ts +14 -3
  55. package/src/control-api/operations/db-verify.ts +15 -5
  56. package/src/control-api/operations/migrate.ts +149 -56
  57. package/src/utils/formatters/migration-graph-layout.ts +187 -42
  58. package/src/utils/formatters/migration-graph-space-render.ts +11 -1
  59. package/src/utils/formatters/migration-graph-tree-render.ts +609 -59
  60. package/src/utils/formatters/migration-list-render.ts +12 -0
  61. package/src/utils/formatters/migration-list-styler.ts +5 -7
  62. package/dist/client-DC-UlBLy.mjs.map +0 -1
  63. package/dist/migration-graph-space-render-CeNXh_Wy.mjs.map +0 -1
  64. package/dist/migration-list-vJWFuXca.mjs.map +0 -1
  65. package/dist/migration-status-Bjv91dE7.mjs.map +0 -1
@@ -17,9 +17,11 @@ import type {
17
17
  import { hasOperationPreview } from '@prisma-next/framework-components/control';
18
18
  import {
19
19
  type ContractSpaceAggregate,
20
+ collectAggregateNamespaces,
20
21
  type PlannerError,
21
22
  planMigration,
22
23
  } from '@prisma-next/migration-tools/aggregate';
24
+ import { blindCast } from '@prisma-next/utils/casts';
23
25
  import { ifDefined } from '@prisma-next/utils/defined';
24
26
  import { notOk, ok } from '@prisma-next/utils/result';
25
27
  import { CliStructuredError } from '../../utils/cli-errors';
@@ -157,7 +159,10 @@ export async function executeRun<TFamilyId extends string, TTargetId extends str
157
159
  spanId: SPAN_IDS.introspect,
158
160
  label: 'Introspecting database schema',
159
161
  });
160
- const schemaIR = await familyInstance.introspect({ driver });
162
+ const schemaIR = await familyInstance.introspect({
163
+ driver,
164
+ contract: collectAggregateNamespaces(aggregate),
165
+ });
161
166
  onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.introspect, outcome: 'ok' });
162
167
 
163
168
  // 3. Plan via aggregate planner. App is forced through synth (today's
@@ -328,7 +333,10 @@ function mapPlannerError(error: PlannerError): DbInitResult | DbUpdateResult {
328
333
  why: undefined,
329
334
  meta: undefined,
330
335
  };
331
- return notOk(failure) as DbInitResult | DbUpdateResult;
336
+ return blindCast<
337
+ DbInitResult | DbUpdateResult,
338
+ 'notOk(failure) is shape-compatible with both DbInitResult and DbUpdateResult; the union is the return type of the surrounding function'
339
+ >(notOk(failure));
332
340
  }
333
341
  if (error.kind === 'extensionPathUnreachable') {
334
342
  return buildRunnerFailure({
@@ -423,5 +431,8 @@ function buildRunnerFailure(args: {
423
431
  conflicts: undefined,
424
432
  ...ifDefined('warnings', args.warnings),
425
433
  };
426
- return notOk(failure) as DbInitResult | DbUpdateResult;
434
+ return blindCast<
435
+ DbInitResult | DbUpdateResult,
436
+ 'notOk(failure) is shape-compatible with both DbInitResult and DbUpdateResult; the union is the return type of the surrounding function'
437
+ >(notOk(failure));
427
438
  }
@@ -8,11 +8,12 @@ import type {
8
8
  } from '@prisma-next/framework-components/control';
9
9
  import {
10
10
  type ContractSpaceMember,
11
+ collectAggregateNamespaces,
11
12
  requireHeadRef,
12
13
  type VerifierOutput,
13
14
  verifyMigration,
14
15
  } from '@prisma-next/migration-tools/aggregate';
15
- import { castAs } from '@prisma-next/utils/casts';
16
+ import { blindCast, castAs } from '@prisma-next/utils/casts';
16
17
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
17
18
  import { CliStructuredError } from '../../utils/cli-errors';
18
19
  import {
@@ -101,7 +102,12 @@ export async function executeDbVerify<TFamilyId extends string, TTargetId extend
101
102
  const markersBySpaceId = await familyInstance.readAllMarkers({ driver });
102
103
  const schemaIntrospection = skipSchema
103
104
  ? null
104
- : await runIntrospection({ driver, familyInstance, onProgress });
105
+ : await runIntrospection({
106
+ driver,
107
+ familyInstance,
108
+ onProgress,
109
+ contract: collectAggregateNamespaces(aggregate),
110
+ });
105
111
 
106
112
  emitVerifySpan(onProgress, 'spanStart');
107
113
  const verifyResult = verifyMigration({
@@ -130,8 +136,9 @@ async function runIntrospection<TFamilyId extends string, TTargetId extends stri
130
136
  driver: ControlDriverInstance<TFamilyId, TTargetId>;
131
137
  familyInstance: ControlFamilyInstance<TFamilyId, unknown>;
132
138
  onProgress: OnControlProgress | undefined;
139
+ contract: unknown;
133
140
  }): Promise<unknown> {
134
- const { driver, familyInstance, onProgress } = args;
141
+ const { driver, familyInstance, onProgress, contract } = args;
135
142
  onProgress?.({
136
143
  action: 'dbVerify',
137
144
  kind: 'spanStart',
@@ -139,7 +146,7 @@ async function runIntrospection<TFamilyId extends string, TTargetId extends stri
139
146
  label: 'Introspecting database schema',
140
147
  });
141
148
  try {
142
- const result = await familyInstance.introspect({ driver });
149
+ const result = await familyInstance.introspect({ driver, contract });
143
150
  onProgress?.({
144
151
  action: 'dbVerify',
145
152
  kind: 'spanEnd',
@@ -179,7 +186,10 @@ export function createPerMemberVerifier<TFamilyId extends string, TTargetId exte
179
186
  // The family's `TSchemaIR` is opaque to migration-tools; the
180
187
  // aggregate verifier passes through whatever we hand it. The
181
188
  // family expects its own IR shape on the way back.
182
- schema: projectedSchema as never,
189
+ schema: blindCast<
190
+ never,
191
+ 'family TSchemaIR is opaque to migration-tools; projectedSchema is passed straight through'
192
+ >(projectedSchema),
183
193
  strict: verifyMode === 'strict',
184
194
  frameworkComponents,
185
195
  });
@@ -156,58 +156,36 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
156
156
  // The aggregate passed the integrity gate, so every member's head ref
157
157
  // is resolved (the app's is synthesised from the live contract).
158
158
  const headRef = requireHeadRef(member);
159
- const targetHash = isAppMember && refHash !== undefined ? refHash : headRef.hash;
159
+ const memberTargetHash = isAppMember && refHash !== undefined ? refHash : headRef.hash;
160
+ const memberRefInvariants = isAppMember && refHash !== undefined ? refInvariants : undefined;
160
161
  const liveMarker = markerRows.get(member.spaceId) ?? null;
161
162
 
162
- // Empty-graph members fail loudly: replay needs an on-disk path
163
- // and an empty graph means the user has never planned this space.
164
- if (member.graph().nodes.size === 0) {
165
- // Edge case: target == EMPTY (greenfield, nothing to do) or
166
- // the live marker already matches the target. Loader integrity
167
- // allows this for extensions whose head ref is the empty
168
- // sentinel. Record a zero-op resolution so the aggregate result
169
- // still surfaces the member in `perSpace[]` as already-at-head;
170
- // the runner is not invoked for these members because they have
171
- // no authored ops and (for greenfield extensions) no marker to
172
- // advance.
173
- const liveHash = liveMarker?.storageHash;
174
- if (
175
- targetHash === liveHash ||
176
- (liveHash === undefined && targetHash === EMPTY_CONTRACT_HASH)
177
- ) {
178
- atHeadResolutions.set(
179
- member.spaceId,
180
- buildAtHeadResolution({
181
- aggregateTargetId: aggregate.targetId,
182
- member,
183
- targetHash,
184
- liveMarker,
185
- }),
186
- );
187
- continue;
188
- }
189
- return notOk(buildNeverPlannedFailure(member.spaceId, targetHash));
190
- }
191
-
192
- const targetInvariants =
193
- isAppMember && refHash !== undefined && refInvariants !== undefined
194
- ? refInvariants
195
- : headRef.invariants;
196
- const targetMember: ContractSpaceMember =
197
- targetHash === headRef.hash && targetInvariants === headRef.invariants
198
- ? member
199
- : { ...member, headRef: { hash: targetHash, invariants: targetInvariants } };
200
-
201
- const walked = graphWalkStrategy({
202
- aggregateTargetId: aggregate.targetId,
203
- member: targetMember,
204
- currentMarker: liveMarker,
205
- ...(isAppMember && refName !== undefined ? { refName } : {}),
163
+ const outcome = planMemberPath({
164
+ member,
165
+ aggregate,
166
+ targetHash: memberTargetHash,
167
+ refInvariants: memberRefInvariants,
168
+ liveMarker,
169
+ ...(isAppMember ? { refName } : {}),
206
170
  });
207
- if (walked.kind === 'unreachable') {
208
- return notOk(buildPathNotFoundFailure(member.spaceId, liveMarker, targetHash));
171
+
172
+ if (outcome.kind === 'at-head') {
173
+ // Empty-graph member whose live marker already matches the target.
174
+ // Kept out of the runner schedule so we don't write spurious markers
175
+ // for greenfield extensions, but merged back into the success envelope
176
+ // so every loaded member is represented.
177
+ atHeadResolutions.set(member.spaceId, outcome.plan);
178
+ continue;
179
+ }
180
+ if (outcome.kind === 'never-planned') {
181
+ return notOk(buildNeverPlannedFailure(outcome.spaceId, outcome.targetHash));
182
+ }
183
+ if (outcome.kind === 'unreachable') {
184
+ return notOk(
185
+ buildPathNotFoundFailure(outcome.spaceId, outcome.liveMarker, outcome.targetHash),
186
+ );
209
187
  }
210
- if (walked.kind === 'unsatisfiable') {
188
+ if (outcome.kind === 'unsatisfiable') {
211
189
  // Surface the canonical MIGRATION.NO_INVARIANT_PATH envelope
212
190
  // (the error rendering pipeline maps it to meta.code +
213
191
  // meta.required + meta.missing + meta.structuralPath that the
@@ -218,10 +196,12 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
218
196
  // string here would leave the structural lookup with a hash that
219
197
  // is never a graph node, producing an empty `structuralPath` and
220
198
  // a less actionable diagnostic.
221
- const fromHash = liveMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
222
- const structural = findPathWithDecision(targetMember.graph(), fromHash, targetHash, {
223
- required: new Set<string>(),
224
- });
199
+ const structural = findPathWithDecision(
200
+ outcome.targetMember.graph(),
201
+ outcome.liveHash,
202
+ memberTargetHash,
203
+ { required: new Set<string>() },
204
+ );
225
205
  const structuralPath =
226
206
  structural.kind === 'ok'
227
207
  ? structural.decision.selectedPath.map((edge) => ({
@@ -233,14 +213,14 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
233
213
  }))
234
214
  : [];
235
215
  throw errorNoInvariantPath({
236
- ...(isAppMember && refName !== undefined ? { refName } : {}),
237
- required: targetInvariants,
238
- missing: walked.missing,
216
+ ...(outcome.refName !== undefined ? { refName: outcome.refName } : {}),
217
+ required: outcome.targetInvariants,
218
+ missing: outcome.missing,
239
219
  structuralPath,
240
220
  });
241
221
  }
242
222
 
243
- perSpacePlans.set(member.spaceId, walked.result);
223
+ perSpacePlans.set(member.spaceId, outcome.plan);
244
224
  }
245
225
 
246
226
  const canonicalOrder = [...aggregate.extensions.map((m) => m.spaceId), aggregate.app.spaceId];
@@ -340,6 +320,119 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
340
320
  );
341
321
  }
342
322
 
323
+ /**
324
+ * Outcome variants for one member's path computation.
325
+ *
326
+ * Callers switch on `kind` and map to their own error representation:
327
+ * `executeMigrate` throws / returns `notOk`; `executeMigrateShowCommand`
328
+ * returns a CLI structured error. The shared discriminant guarantees both
329
+ * paths feed `graphWalkStrategy` the same inputs.
330
+ *
331
+ * @internal Exported for `executeMigrateShowCommand` to call.
332
+ */
333
+ export type MemberPathOutcome =
334
+ | { readonly kind: 'ok'; readonly plan: PerSpacePlan }
335
+ | { readonly kind: 'at-head'; readonly plan: PerSpacePlan }
336
+ | { readonly kind: 'never-planned'; readonly spaceId: string; readonly targetHash: string }
337
+ | {
338
+ readonly kind: 'unreachable';
339
+ readonly spaceId: string;
340
+ readonly liveMarker: ContractMarkerRecordLike | null;
341
+ readonly targetHash: string;
342
+ }
343
+ | {
344
+ readonly kind: 'unsatisfiable';
345
+ readonly spaceId: string;
346
+ readonly isAppMember: boolean;
347
+ readonly missing: readonly string[];
348
+ readonly targetInvariants: readonly string[];
349
+ readonly targetMember: ContractSpaceMember;
350
+ readonly liveHash: string;
351
+ readonly refName: string | undefined;
352
+ };
353
+
354
+ /**
355
+ * Compute the graph-walk path for one contract-space member.
356
+ *
357
+ * Encapsulates the invariant-correct input assembly that both
358
+ * `executeMigrate` and `executeMigrateShowCommand` must use:
359
+ * - `currentMarker` carries the full live marker including `invariants`
360
+ * (not a stripped `{ storageHash, invariants: [] }` shell).
361
+ * - `targetInvariants` uses the caller-supplied `refInvariants` when a
362
+ * `--to` ref was resolved (not always the file head ref's invariants).
363
+ *
364
+ * Both callers map the returned `MemberPathOutcome` to their own error
365
+ * representation; the path-compute logic is shared and identical.
366
+ *
367
+ * @internal Exported for `executeMigrateShowCommand`.
368
+ */
369
+ export function planMemberPath({
370
+ member,
371
+ aggregate,
372
+ targetHash,
373
+ refInvariants,
374
+ liveMarker,
375
+ refName,
376
+ }: {
377
+ readonly member: ContractSpaceMember;
378
+ readonly aggregate: Pick<ContractSpaceAggregate, 'targetId' | 'app'>;
379
+ readonly targetHash: string;
380
+ readonly refInvariants: readonly string[] | undefined;
381
+ readonly liveMarker: ContractMarkerRecordLike | null;
382
+ readonly refName?: string;
383
+ }): MemberPathOutcome {
384
+ const isAppMember = member.spaceId === aggregate.app.spaceId;
385
+ const headRef = requireHeadRef(member);
386
+
387
+ if (member.graph().nodes.size === 0) {
388
+ const liveHash = liveMarker?.storageHash;
389
+ if (targetHash === liveHash || (liveHash === undefined && targetHash === EMPTY_CONTRACT_HASH)) {
390
+ return {
391
+ kind: 'at-head',
392
+ plan: buildAtHeadResolution({
393
+ aggregateTargetId: aggregate.targetId,
394
+ member,
395
+ targetHash,
396
+ liveMarker,
397
+ }),
398
+ };
399
+ }
400
+ return { kind: 'never-planned', spaceId: member.spaceId, targetHash };
401
+ }
402
+
403
+ const targetInvariants =
404
+ isAppMember && refInvariants !== undefined ? refInvariants : headRef.invariants;
405
+ const targetMember: ContractSpaceMember =
406
+ targetHash === headRef.hash && targetInvariants === headRef.invariants
407
+ ? member
408
+ : { ...member, headRef: { hash: targetHash, invariants: targetInvariants } };
409
+
410
+ const walked = graphWalkStrategy({
411
+ aggregateTargetId: aggregate.targetId,
412
+ member: targetMember,
413
+ currentMarker: liveMarker,
414
+ ...(isAppMember && refName !== undefined ? { refName } : {}),
415
+ });
416
+
417
+ if (walked.kind === 'unreachable') {
418
+ return { kind: 'unreachable', spaceId: member.spaceId, liveMarker, targetHash };
419
+ }
420
+ if (walked.kind === 'unsatisfiable') {
421
+ const liveHash = liveMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
422
+ return {
423
+ kind: 'unsatisfiable',
424
+ spaceId: member.spaceId,
425
+ isAppMember,
426
+ missing: walked.missing,
427
+ targetInvariants,
428
+ targetMember,
429
+ liveHash,
430
+ refName,
431
+ };
432
+ }
433
+ return { kind: 'ok', plan: walked.result };
434
+ }
435
+
343
436
  /**
344
437
  * Build a zero-op {@link PerSpacePlan} for an empty-graph
345
438
  * member whose live marker already matches the target. Lets the apply