@prisma-next/cli 0.16.0-dev.4 → 0.16.0-dev.6

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 (114) hide show
  1. package/README.md +3 -4
  2. package/dist/cli.mjs +13 -13
  3. package/dist/{client-KuBQftxz.mjs → client-2kwLMBSf.mjs} +4 -4
  4. package/dist/{client-KuBQftxz.mjs.map → client-2kwLMBSf.mjs.map} +1 -1
  5. package/dist/{command-helpers-Cpq44aVa.mjs → command-helpers-CVn3U9Uz.mjs} +12 -4
  6. package/dist/command-helpers-CVn3U9Uz.mjs.map +1 -0
  7. package/dist/commands/contract-emit.mjs +1 -1
  8. package/dist/commands/contract-infer.mjs +1 -1
  9. package/dist/commands/db-init.d.mts.map +1 -1
  10. package/dist/commands/db-init.mjs +5 -4
  11. package/dist/commands/db-init.mjs.map +1 -1
  12. package/dist/commands/db-schema.mjs +3 -3
  13. package/dist/commands/db-sign.d.mts.map +1 -1
  14. package/dist/commands/db-sign.mjs +8 -10
  15. package/dist/commands/db-sign.mjs.map +1 -1
  16. package/dist/commands/db-update.d.mts.map +1 -1
  17. package/dist/commands/db-update.mjs +11 -13
  18. package/dist/commands/db-update.mjs.map +1 -1
  19. package/dist/commands/db-verify.mjs +1 -1
  20. package/dist/commands/migrate.d.mts.map +1 -1
  21. package/dist/commands/migrate.mjs +7 -7
  22. package/dist/commands/migrate.mjs.map +1 -1
  23. package/dist/commands/migration-check.d.mts +3 -1
  24. package/dist/commands/migration-check.d.mts.map +1 -1
  25. package/dist/commands/migration-check.mjs +1 -1
  26. package/dist/commands/migration-graph.mjs +3 -3
  27. package/dist/commands/migration-list.d.mts.map +1 -1
  28. package/dist/commands/migration-list.mjs +1 -1
  29. package/dist/commands/migration-log.mjs +1 -1
  30. package/dist/commands/migration-new.d.mts.map +1 -1
  31. package/dist/commands/migration-new.mjs +14 -38
  32. package/dist/commands/migration-new.mjs.map +1 -1
  33. package/dist/commands/migration-plan.d.mts.map +1 -1
  34. package/dist/commands/migration-plan.mjs +1 -1
  35. package/dist/commands/migration-show.mjs +3 -3
  36. package/dist/commands/migration-status.mjs +1 -1
  37. package/dist/commands/ref.d.mts.map +1 -1
  38. package/dist/commands/ref.mjs +13 -18
  39. package/dist/commands/ref.mjs.map +1 -1
  40. package/dist/commands/telemetry/index.mjs +1 -1
  41. package/dist/contract-at-errors-C-l4-JEY.mjs +36 -0
  42. package/dist/contract-at-errors-C-l4-JEY.mjs.map +1 -0
  43. package/dist/{contract-emit-CgnXSANN.mjs → contract-emit-DzOkTs11.mjs} +3 -3
  44. package/dist/{contract-emit-CgnXSANN.mjs.map → contract-emit-DzOkTs11.mjs.map} +1 -1
  45. package/dist/{contract-emit-cJQBlvGb.mjs → contract-emit-iSMJVs26.mjs} +3 -3
  46. package/dist/{contract-emit-cJQBlvGb.mjs.map → contract-emit-iSMJVs26.mjs.map} +1 -1
  47. package/dist/{contract-infer-Cg8evbeg.mjs → contract-infer-BVqworBB.mjs} +3 -3
  48. package/dist/{contract-infer-Cg8evbeg.mjs.map → contract-infer-BVqworBB.mjs.map} +1 -1
  49. package/dist/{contract-space-aggregate-loader-D_3VeHVb.mjs → contract-space-aggregate-loader-hPVymNLw.mjs} +2 -2
  50. package/dist/{contract-space-aggregate-loader-D_3VeHVb.mjs.map → contract-space-aggregate-loader-hPVymNLw.mjs.map} +1 -1
  51. package/dist/{db-verify-BEclUzki.mjs → db-verify-BpwCMyWJ.mjs} +4 -4
  52. package/dist/{db-verify-BEclUzki.mjs.map → db-verify-BpwCMyWJ.mjs.map} +1 -1
  53. package/dist/exports/control-api.mjs +2 -2
  54. package/dist/exports/index.mjs +2 -2
  55. package/dist/exports/init-output.d.mts.map +1 -1
  56. package/dist/exports/init-output.mjs +1 -1
  57. package/dist/{format-DAUAZqPt.mjs → format-CefCDzkw.mjs} +2 -2
  58. package/dist/{format-DAUAZqPt.mjs.map → format-CefCDzkw.mjs.map} +1 -1
  59. package/dist/{framework-components-CnbUbiJw.mjs → framework-components-VMQJbAwl.mjs} +2 -2
  60. package/dist/{framework-components-CnbUbiJw.mjs.map → framework-components-VMQJbAwl.mjs.map} +1 -1
  61. package/dist/{init-C3JhkHg-.mjs → init-BSgPxVXA.mjs} +55 -47
  62. package/dist/init-BSgPxVXA.mjs.map +1 -0
  63. package/dist/{inspect-live-schema-CX7EA5C3.mjs → inspect-live-schema-DxsD_a4d.mjs} +3 -3
  64. package/dist/{inspect-live-schema-CX7EA5C3.mjs.map → inspect-live-schema-DxsD_a4d.mjs.map} +1 -1
  65. package/dist/{migration-check-MHT3w7_v.mjs → migration-check-Biu1YZXu.mjs} +52 -25
  66. package/dist/migration-check-Biu1YZXu.mjs.map +1 -0
  67. package/dist/{migration-command-scaffold-CRyYrHZ9.mjs → migration-command-scaffold-DB_i9nBC.mjs} +3 -3
  68. package/dist/{migration-command-scaffold-CRyYrHZ9.mjs.map → migration-command-scaffold-DB_i9nBC.mjs.map} +1 -1
  69. package/dist/{migration-list-BAKujUjh.mjs → migration-list-cnSKilYH.mjs} +5 -5
  70. package/dist/migration-list-cnSKilYH.mjs.map +1 -0
  71. package/dist/{migration-log-C_xam2HZ.mjs → migration-log-CEqFiez9.mjs} +3 -3
  72. package/dist/{migration-log-C_xam2HZ.mjs.map → migration-log-CEqFiez9.mjs.map} +1 -1
  73. package/dist/{migration-path-target-DpPTsRCg.mjs → migration-path-target-C0TQuV5n.mjs} +2 -2
  74. package/dist/{migration-path-target-DpPTsRCg.mjs.map → migration-path-target-C0TQuV5n.mjs.map} +1 -1
  75. package/dist/{migration-plan-CreSVtWZ.mjs → migration-plan-BJHulFZC.mjs} +53 -61
  76. package/dist/migration-plan-BJHulFZC.mjs.map +1 -0
  77. package/dist/{migration-status-YgEvXf-u.mjs → migration-status-Df4zK5lO.mjs} +6 -5
  78. package/dist/{migration-status-YgEvXf-u.mjs.map → migration-status-Df4zK5lO.mjs.map} +1 -1
  79. package/dist/{output-mEQ74_nd.mjs → output-DWNYqZGc.mjs} +5 -6
  80. package/dist/output-DWNYqZGc.mjs.map +1 -0
  81. package/dist/{ref-advancement-BkXlikCA.mjs → ref-advancement-BCz7X7qJ.mjs} +13 -6
  82. package/dist/ref-advancement-BCz7X7qJ.mjs.map +1 -0
  83. package/dist/{telemetry-DVr6RFnq.mjs → telemetry-BEyNMfco.mjs} +2 -2
  84. package/dist/{telemetry-DVr6RFnq.mjs.map → telemetry-BEyNMfco.mjs.map} +1 -1
  85. package/dist/{verify-CgDVZjhc.mjs → verify-C4RazDE1.mjs} +2 -2
  86. package/dist/{verify-CgDVZjhc.mjs.map → verify-C4RazDE1.mjs.map} +1 -1
  87. package/package.json +21 -21
  88. package/src/commands/db-init.ts +1 -0
  89. package/src/commands/db-sign.ts +6 -4
  90. package/src/commands/db-update.ts +14 -6
  91. package/src/commands/init/hygiene-gitattributes.ts +37 -21
  92. package/src/commands/init/init.ts +6 -6
  93. package/src/commands/init/output.ts +4 -5
  94. package/src/commands/init/reinit-cleanup.ts +16 -17
  95. package/src/commands/migrate.ts +1 -0
  96. package/src/commands/migration-check.ts +69 -32
  97. package/src/commands/migration-list.ts +1 -5
  98. package/src/commands/migration-new.ts +15 -39
  99. package/src/commands/migration-plan.ts +55 -77
  100. package/src/commands/ref.ts +19 -14
  101. package/src/utils/cli-errors.ts +13 -5
  102. package/src/utils/contract-at-errors.ts +13 -22
  103. package/src/utils/contract-space-seed-phase.ts +8 -6
  104. package/src/utils/plan-resolution.ts +6 -9
  105. package/src/utils/ref-advancement.ts +22 -3
  106. package/dist/command-helpers-Cpq44aVa.mjs.map +0 -1
  107. package/dist/contract-at-errors-D9gEjYtg.mjs +0 -42
  108. package/dist/contract-at-errors-D9gEjYtg.mjs.map +0 -1
  109. package/dist/init-C3JhkHg-.mjs.map +0 -1
  110. package/dist/migration-check-MHT3w7_v.mjs.map +0 -1
  111. package/dist/migration-list-BAKujUjh.mjs.map +0 -1
  112. package/dist/migration-plan-CreSVtWZ.mjs.map +0 -1
  113. package/dist/output-mEQ74_nd.mjs.map +0 -1
  114. package/dist/ref-advancement-BkXlikCA.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { readFile } from 'node:fs/promises';
2
2
  import { loadConfig } from '@prisma-next/config-loader';
3
3
  import type { Contract } from '@prisma-next/contract/types';
4
4
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
@@ -9,15 +9,14 @@ import {
9
9
  type OperationPreview,
10
10
  type SchemaOwnership,
11
11
  } from '@prisma-next/framework-components/control';
12
- import { canonicalizeJson } from '@prisma-next/framework-components/utils';
12
+ import {
13
+ snapshotsImportPathFrom,
14
+ writeContractSnapshot,
15
+ } from '@prisma-next/migration-tools/contract-snapshot-store';
13
16
  import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
14
17
  import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
15
18
  import { deriveProvidedInvariants } from '@prisma-next/migration-tools/invariants';
16
- import {
17
- copyFilesWithRename,
18
- formatMigrationDirName,
19
- writeMigrationPackage,
20
- } from '@prisma-next/migration-tools/io';
19
+ import { formatMigrationDirName, writeMigrationPackage } from '@prisma-next/migration-tools/io';
21
20
  import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
22
21
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
23
22
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
@@ -62,27 +61,6 @@ interface MigrationPlanOptions extends CommonCommandOptions {
62
61
  readonly to?: string;
63
62
  }
64
63
 
65
- async function writeSnapshotContractArtifacts(
66
- packageDir: string,
67
- contractJson: unknown,
68
- contractDts: string,
69
- artifactBasename: 'start-contract' | 'end-contract',
70
- ): Promise<void> {
71
- await mkdir(packageDir, { recursive: true });
72
- const jsonContent = `${canonicalizeJson(contractJson)}\n`;
73
- const dtsContent = contractDts.endsWith('\n') ? contractDts : `${contractDts}\n`;
74
- await writeFile(join(packageDir, `${artifactBasename}.json`), jsonContent);
75
- await writeFile(join(packageDir, `${artifactBasename}.d.ts`), dtsContent);
76
- }
77
-
78
- async function writeSnapshotStartContract(
79
- packageDir: string,
80
- contractJson: unknown,
81
- contractDts: string,
82
- ): Promise<void> {
83
- await writeSnapshotContractArtifacts(packageDir, contractJson, contractDts, 'start-contract');
84
- }
85
-
86
64
  type PlannerSuccess = {
87
65
  readonly plannedOps: readonly MigrationPlanOperation[];
88
66
  readonly migrationTsContent: string;
@@ -99,6 +77,7 @@ async function runPlannerLeg(
99
77
  fromContract: Contract | null,
100
78
  spaceId: string,
101
79
  ownership: SchemaOwnership,
80
+ snapshotsImportPath: string,
102
81
  ): Promise<Result<PlannerSuccess, CliStructuredError>> {
103
82
  const fromSchema = migrations.contractToSchema(fromContract, frameworkComponents);
104
83
  const plannerResult = planner.plan({
@@ -114,6 +93,7 @@ async function runPlannerLeg(
114
93
  // space in the aggregate, so it stays a genuine drop; a table another
115
94
  // space owns is never dropped.
116
95
  ownership,
96
+ snapshotsImportPath,
117
97
  });
118
98
  if (plannerResult.kind === 'failure') {
119
99
  return notOk(
@@ -309,14 +289,18 @@ async function executeMigrationPlanCommand(
309
289
  let toStorageHash: string = rawStorageHash;
310
290
 
311
291
  // When `--to <ref>` resolves a non-default destination, these carry its raw
312
- // artifacts so the planned package's `end-contract.*` is written from the
313
- // resolved target rather than copied from the emitted `contract.json`.
292
+ // artifacts so the planned package's destination snapshot store entry is
293
+ // written from the resolved target rather than copied from the emitted
294
+ // `contract.json`.
314
295
  let toArtifacts: { contractJson: unknown; contractDts: string } | null = null;
315
296
 
316
297
  let fromContract: Contract | null = null;
317
298
  let fromHash: string | null = null;
318
- let fromContractSourceDir: string | null = null;
319
- let snapshotStartContract: { contractJson: unknown; contractDts: string } | null = null;
299
+ let snapshotStartContract: {
300
+ readonly fromHash: string;
301
+ readonly contractJson: unknown;
302
+ readonly contractDts: string;
303
+ } | null = null;
320
304
  let isAutoBaseline = false;
321
305
 
322
306
  const tolerantAggregateResult = await loadContractSpaceAggregateForCli({
@@ -346,12 +330,12 @@ async function executeMigrationPlanCommand(
346
330
  case 'graph-node':
347
331
  fromHash = resolutionResult.value.fromHash;
348
332
  fromContract = resolutionResult.value.fromContract;
349
- fromContractSourceDir = resolutionResult.value.sourceDir;
350
333
  break;
351
- case 'snapshot':
334
+ case 'ref':
352
335
  fromHash = resolutionResult.value.fromHash;
353
336
  fromContract = resolutionResult.value.fromContract;
354
337
  snapshotStartContract = {
338
+ fromHash: resolutionResult.value.fromHash,
355
339
  contractJson: resolutionResult.value.contractJson,
356
340
  contractDts: resolutionResult.value.contractDts,
357
341
  };
@@ -360,6 +344,7 @@ async function executeMigrationPlanCommand(
360
344
  fromHash = resolutionResult.value.fromHash;
361
345
  fromContract = resolutionResult.value.fromContract;
362
346
  snapshotStartContract = {
347
+ fromHash: resolutionResult.value.fromHash,
363
348
  contractJson: resolutionResult.value.contractJson,
364
349
  contractDts: resolutionResult.value.contractDts,
365
350
  };
@@ -369,7 +354,7 @@ async function executeMigrationPlanCommand(
369
354
 
370
355
  // `--to <ref>` swaps the planner destination to an arbitrary resolved
371
356
  // contract (e.g. an ancestor / rollback target). The from-side resolution
372
- // above is untouched; only the destination + its emitted `end-contract.*`
357
+ // above is untouched; only the destination + its snapshot store entry
373
358
  // change.
374
359
  if (options.to !== undefined) {
375
360
  const toResolution = await resolveToForPlan(options.to, {
@@ -386,11 +371,11 @@ async function executeMigrationPlanCommand(
386
371
  };
387
372
  }
388
373
 
389
- // Phase 1 — seed: unconditionally re-emit per-space pinned artefacts
374
+ // Phase 1 — seed: unconditionally re-emit per-space pinned artifacts
390
375
  // (contract.json / contract.d.ts / refs/head.json) and materialise any
391
376
  // descriptor-shipped migration packages not yet on disk. Runs before
392
377
  // the no-op check so that an extension bump alone (with no structural
393
- // app-space change) still re-pins extension artefacts on disk.
378
+ // app-space change) still re-pins extension artifacts on disk.
394
379
  const canonicalExtensionInputs = toExtensionInputs(config.extensionPacks ?? []);
395
380
  const seedResult = await runContractSpaceSeedPhase({
396
381
  migrationsDir,
@@ -463,24 +448,26 @@ async function executeMigrationPlanCommand(
463
448
  [config.target, config.adapter, ...(config.extensionPacks ?? [])],
464
449
  );
465
450
 
466
- // Write the planned package's destination `end-contract.*`. With `--to`, the
467
- // resolved target's raw artifacts are written; otherwise the emitted
468
- // `contract.json` / `contract.d.ts` are copied verbatim (today's behaviour).
469
- async function writeDestinationEndContract(packageDir: string): Promise<void> {
451
+ // Write the planned package's destination contract into the snapshot store.
452
+ // With `--to`, the resolved target's raw artifacts are written; otherwise
453
+ // the emitted `contract.json` / `contract.d.ts` are read from disk.
454
+ async function writeDestinationSnapshot(destHash: string): Promise<void> {
470
455
  if (toArtifacts !== null) {
471
- await writeSnapshotContractArtifacts(
472
- packageDir,
473
- toArtifacts.contractJson,
474
- toArtifacts.contractDts,
475
- 'end-contract',
476
- );
456
+ await writeContractSnapshot(migrationsDir, destHash, {
457
+ contractJson: toArtifacts.contractJson,
458
+ contractDts: toArtifacts.contractDts,
459
+ });
477
460
  return;
478
461
  }
479
462
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
480
- await copyFilesWithRename(packageDir, [
481
- { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
482
- { sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
463
+ const [contractJsonRaw, contractDts] = await Promise.all([
464
+ readFile(destinationArtifacts.jsonPath, 'utf-8'),
465
+ readFile(destinationArtifacts.dtsPath, 'utf-8'),
483
466
  ]);
467
+ await writeContractSnapshot(migrationsDir, destHash, {
468
+ contractJson: JSON.parse(contractJsonRaw) as unknown,
469
+ contractDts,
470
+ });
484
471
  }
485
472
 
486
473
  try {
@@ -507,6 +494,7 @@ async function executeMigrationPlanCommand(
507
494
  null,
508
495
  aggregate.app.spaceId,
509
496
  aggregate,
497
+ snapshotsImportPathFrom(baselinePackageDir, migrationsDir),
510
498
  );
511
499
  if (!baselineLeg.ok) {
512
500
  return notOk(baselineLeg.failure);
@@ -519,12 +507,10 @@ async function executeMigrationPlanCommand(
519
507
  baselineTimestamp,
520
508
  baselineLeg.value,
521
509
  );
522
- await writeSnapshotContractArtifacts(
523
- baselinePackageDir,
524
- snapshotStartContract.contractJson,
525
- snapshotStartContract.contractDts,
526
- 'end-contract',
527
- );
510
+ await writeContractSnapshot(migrationsDir, fromHash, {
511
+ contractJson: snapshotStartContract.contractJson,
512
+ contractDts: snapshotStartContract.contractDts,
513
+ });
528
514
 
529
515
  if (fromHash === toStorageHash) {
530
516
  const baselineOps = baselineLeg.value.hasPlaceholders ? [] : baselineLeg.value.plannedOps;
@@ -577,6 +563,7 @@ async function executeMigrationPlanCommand(
577
563
  fromContract,
578
564
  aggregate.app.spaceId,
579
565
  aggregate,
566
+ snapshotsImportPathFrom(deltaPackageDir, migrationsDir),
580
567
  );
581
568
  if (!deltaLeg.ok) {
582
569
  return notOk(deltaLeg.failure);
@@ -589,12 +576,11 @@ async function executeMigrationPlanCommand(
589
576
  deltaTimestamp,
590
577
  deltaLeg.value,
591
578
  );
592
- await writeDestinationEndContract(deltaPackageDir);
593
- await writeSnapshotStartContract(
594
- deltaPackageDir,
595
- snapshotStartContract.contractJson,
596
- snapshotStartContract.contractDts,
597
- );
579
+ await writeDestinationSnapshot(toStorageHash);
580
+ await writeContractSnapshot(migrationsDir, fromHash, {
581
+ contractJson: snapshotStartContract.contractJson,
582
+ contractDts: snapshotStartContract.contractDts,
583
+ });
598
584
 
599
585
  const deltaOps = deltaLeg.value.hasPlaceholders ? [] : deltaLeg.value.plannedOps;
600
586
  if (deltaLeg.value.hasPlaceholders) {
@@ -651,6 +637,7 @@ async function executeMigrationPlanCommand(
651
637
  fromContract,
652
638
  aggregate.app.spaceId,
653
639
  aggregate,
640
+ snapshotsImportPathFrom(packageDir, migrationsDir),
654
641
  );
655
642
  if (!deltaLeg.ok) {
656
643
  return notOk(deltaLeg.failure);
@@ -663,21 +650,12 @@ async function executeMigrationPlanCommand(
663
650
  timestamp,
664
651
  deltaLeg.value,
665
652
  );
666
- await writeDestinationEndContract(packageDir);
667
- if (fromContractSourceDir !== null) {
668
- const sourceArtifacts = getEmittedArtifactPaths(
669
- join(fromContractSourceDir, 'end-contract.json'),
670
- );
671
- await copyFilesWithRename(packageDir, [
672
- { sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
673
- { sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
674
- ]);
675
- } else if (snapshotStartContract !== null) {
676
- await writeSnapshotStartContract(
677
- packageDir,
678
- snapshotStartContract.contractJson,
679
- snapshotStartContract.contractDts,
680
- );
653
+ await writeDestinationSnapshot(toStorageHash);
654
+ if (snapshotStartContract !== null) {
655
+ await writeContractSnapshot(migrationsDir, snapshotStartContract.fromHash, {
656
+ contractJson: snapshotStartContract.contractJson,
657
+ contractDts: snapshotStartContract.contractDts,
658
+ });
681
659
  }
682
660
 
683
661
  if (deltaLeg.value.hasPlaceholders) {
@@ -1,16 +1,19 @@
1
- import { readFile } from 'node:fs/promises';
2
1
  import { loadConfig } from '@prisma-next/config-loader';
3
2
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
3
+ import {
4
+ contractSnapshotDir,
5
+ readContractSnapshotJson,
6
+ } from '@prisma-next/migration-tools/contract-snapshot-store';
4
7
  import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
5
8
  import { findLatestMigration, isGraphNode } from '@prisma-next/migration-tools/migration-graph';
6
9
  import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
7
10
  import type { RefEntry } from '@prisma-next/migration-tools/refs';
8
11
  import {
9
- deleteRefPaired,
12
+ deleteRef,
10
13
  readRefs,
11
14
  validateRefName,
12
15
  validateRefValue,
13
- writeRefPaired,
16
+ writeRef,
14
17
  } from '@prisma-next/migration-tools/refs';
15
18
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
16
19
  import { Command } from 'commander';
@@ -34,7 +37,6 @@ import {
34
37
  import { buildReadAggregate } from '../utils/contract-space-aggregate-loader';
35
38
  import { formatCommandHelp } from '../utils/formatters/help';
36
39
  import { parseGlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
37
- import { readContractIR } from '../utils/ref-advancement';
38
40
  import { handleResult } from '../utils/result-handler';
39
41
  import { createTerminalUI } from '../utils/terminal-ui';
40
42
 
@@ -114,26 +116,29 @@ export async function executeRefSetCommand(
114
116
  return notOk(errorRefSetBundleNotFound(resolvedHash));
115
117
  }
116
118
 
117
- const contractJsonPath = join(matchingBundle.dirPath, 'end-contract.json');
118
- let contractJson: Record<string, unknown>;
119
+ const contractJsonPath = join(
120
+ contractSnapshotDir(migrationsDir, resolvedHash),
121
+ 'contract.json',
122
+ );
119
123
  try {
120
- const raw = await readFile(contractJsonPath, 'utf-8');
121
- contractJson = JSON.parse(raw) as Record<string, unknown>;
124
+ await readContractSnapshotJson(migrationsDir, resolvedHash);
122
125
  } catch (readError) {
123
- if (readError instanceof Error && (readError as NodeJS.ErrnoException).code === 'ENOENT') {
126
+ if (
127
+ MigrationToolsError.is(readError) &&
128
+ readError.code === 'MIGRATION.CONTRACT_SNAPSHOT_MISSING'
129
+ ) {
124
130
  return notOk(
125
131
  errorFileNotFound(contractJsonPath, {
126
- why: `Migration bundle for hash ${resolvedHash} is missing its end-contract snapshot at ${contractJsonPath}`,
127
- fix: 'Run `pnpm fixtures:check`, or re-emit the migration so its end-contract.json is restored.',
132
+ why: `Migration bundle for hash ${resolvedHash} is missing its contract snapshot at ${contractJsonPath}`,
133
+ fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
128
134
  }),
129
135
  );
130
136
  }
131
137
  throw readError;
132
138
  }
133
139
 
134
- const contractIR = await readContractIR(contractJson, contractJsonPath);
135
140
  const entry: RefEntry = { hash: resolvedHash, invariants: [] };
136
- await writeRefPaired(refsDir, name, entry, contractIR);
141
+ await writeRef(refsDir, name, entry);
137
142
  return ok({ ok: true as const, ref: name, hash: resolvedHash, invariants: [] });
138
143
  } catch (error) {
139
144
  if (error instanceof CliStructuredError) return notOk(error);
@@ -148,7 +153,7 @@ export async function executeRefDeleteCommand(
148
153
  try {
149
154
  const config = await loadConfig(options.config);
150
155
  const { refsDir } = resolveMigrationPaths(options.config, config);
151
- await deleteRefPaired(refsDir, name);
156
+ await deleteRef(refsDir, name);
152
157
  return ok({ ok: true as const, ref: name, deleted: true as const });
153
158
  } catch (error) {
154
159
  if (error instanceof CliStructuredError) return notOk(error);
@@ -215,22 +215,30 @@ export function errorPlanForgotTheFlag(
215
215
  });
216
216
  }
217
217
 
218
+ /**
219
+ * `viaRef: true` (the default) mirrors migration-tools' `errorRefNotResolvable`:
220
+ * a ref name with no pointer file, where the fallback hash isn't a graph
221
+ * node either — there's nothing to materialize a contract from.
222
+ * `viaRef: false` is a distinct, ref-independent case: an explicit `--from
223
+ * <hash>` that doesn't name a ref, on an empty migration graph, so there is
224
+ * no graph node and no ref to resolve a contract through.
225
+ */
218
226
  export function errorSnapshotMissing(
219
227
  identifier: string,
220
228
  options?: { readonly viaRef?: boolean },
221
229
  ): CliStructuredError {
222
230
  const viaRef = options?.viaRef !== false;
223
231
  const fix = viaRef
224
- ? `Run "prisma-next db update --advance-ref ${identifier}" to repopulate the snapshot, or "prisma-next ref delete ${identifier}" to clear the orphan pointer.`
225
- : `No contract source exists for hash "${identifier}" on an empty migration graph. Use --from with a ref name that has a paired snapshot, or run db update first.`;
232
+ ? `Create the ref with "prisma-next ref set ${identifier} <hash>" (or advance it via "prisma-next db update --advance-ref ${identifier}"), or pass a hash that is a node in the migration graph.`
233
+ : `No contract source exists for hash "${identifier}" on an empty migration graph. Use --from with a ref name (its contract resolves through the snapshot store), or run db update first.`;
226
234
  return errorRuntime(
227
235
  viaRef
228
- ? `Ref "${identifier}" has no paired contract snapshot`
236
+ ? `Ref "${identifier}" is not resolvable`
229
237
  : `No contract source for from-hash "${identifier}"`,
230
238
  {
231
239
  why: viaRef
232
- ? `Ref "${identifier}" exists but its paired snapshot files are missing.`
233
- : `Hash "${identifier}" is not a graph node and no paired ref snapshot supplies a contract.`,
240
+ ? `Ref "${identifier}" has no pointer file, and the hash being resolved is not a node in the migration graph either.`
241
+ : `Hash "${identifier}" is not a node in the migration graph (the graph is empty), and it does not name a ref either.`,
234
242
  fix,
235
243
  meta: {
236
244
  code: 'MIGRATION.SNAPSHOT_MISSING',
@@ -1,6 +1,5 @@
1
1
  import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
2
2
  import { notOk, type Result } from '@prisma-next/utils/result';
3
- import { join } from 'pathe';
4
3
  import {
5
4
  CliStructuredError,
6
5
  errorContractValidationFailed,
@@ -16,7 +15,7 @@ export function mapContractAtError(
16
15
  ): Result<never, CliStructuredError> {
17
16
  if (MigrationToolsError.is(error)) {
18
17
  switch (error.code) {
19
- case 'MIGRATION.SNAPSHOT_MISSING': {
18
+ case 'MIGRATION.REF_NOT_RESOLVABLE': {
20
19
  const refName =
21
20
  typeof error.details?.['refName'] === 'string'
22
21
  ? error.details['refName']
@@ -27,18 +26,13 @@ export function mapContractAtError(
27
26
  }
28
27
  case 'MIGRATION.CONTRACT_DESERIALIZATION_FAILED': {
29
28
  const filePath =
30
- typeof error.details?.['filePath'] === 'string'
31
- ? error.details['filePath']
32
- : 'ref-snapshot';
29
+ typeof error.details?.['filePath'] === 'string' ? error.details['filePath'] : 'unknown';
33
30
  const message =
34
31
  typeof error.details?.['message'] === 'string' ? error.details['message'] : error.message;
35
- const isRefSnapshot = filePath.endsWith('.contract.json');
36
32
  return notOk(
37
33
  errorContractValidationFailed(
38
- isRefSnapshot
39
- ? `Ref snapshot contract failed to deserialize: ${message}`
40
- : `Predecessor contract at ${filePath} failed to deserialize: ${message}`,
41
- { where: { path: isRefSnapshot ? 'ref-snapshot' : filePath } },
34
+ `Predecessor contract at ${filePath} failed to deserialize: ${message}`,
35
+ { where: { path: filePath } },
42
36
  ),
43
37
  );
44
38
  }
@@ -66,22 +60,19 @@ export function mapContractAtError(
66
60
  fix: error.fix,
67
61
  }),
68
62
  );
69
- case 'MIGRATION.FILE_MISSING': {
70
- const file =
71
- typeof error.details?.['file'] === 'string' ? error.details['file'] : 'end-contract.json';
72
- const dir = typeof error.details?.['dir'] === 'string' ? error.details['dir'] : '';
73
- const jsonPath = dir ? join(dir, 'end-contract.json') : file;
63
+ case 'MIGRATION.CONTRACT_SNAPSHOT_MISSING': {
64
+ const expectedPath =
65
+ typeof error.details?.['expectedPath'] === 'string'
66
+ ? error.details['expectedPath']
67
+ : 'migrations/snapshots/';
74
68
  const role = options?.artifactRole ?? 'from';
75
69
  return notOk(
76
- errorFileNotFound(jsonPath, {
70
+ errorFileNotFound(expectedPath, {
77
71
  why:
78
72
  role === 'to'
79
- ? `Target migration is missing its destination contract snapshot at ${jsonPath}`
80
- : `Predecessor migration is missing its destination contract snapshot at ${jsonPath}`,
81
- fix:
82
- role === 'to'
83
- ? 'Re-emit the target migration so its sibling `end-contract.json` / `end-contract.d.ts` are restored, then re-run this command.'
84
- : 'Re-emit the predecessor migration (`prisma-next migration plan` from its source) so its sibling `end-contract.json` is restored, then re-run this command.',
73
+ ? `Target migration is missing its contract snapshot at ${expectedPath}`
74
+ : `Predecessor migration is missing its contract snapshot at ${expectedPath}`,
75
+ fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
85
76
  }),
86
77
  );
87
78
  }
@@ -2,7 +2,7 @@ import { materialiseExtensionMigrationPackageIfMissing } from '@prisma-next/migr
2
2
  import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
3
3
  import type { MigrationOps } from '@prisma-next/migration-tools/package';
4
4
  import {
5
- emitContractSpaceArtefacts,
5
+ emitContractSpaceArtifacts,
6
6
  planAllSpaces,
7
7
  readContractSpaceHeadRef,
8
8
  type SpacePlanOutput,
@@ -53,7 +53,7 @@ export interface ContractSpaceSeedPhaseInputs {
53
53
  * - `action: 'unchanged'` — the on-disk head already matched the
54
54
  * descriptor and no new migration packages needed to be written.
55
55
  *
56
- * Either way, the artefacts (`contract.json`, `contract.d.ts`,
56
+ * Either way, the artifacts (`contract.json`, `contract.d.ts`,
57
57
  * `refs/head.json`) are re-emitted: the framework owns those files and
58
58
  * makes the re-emit observably idempotent at the byte level.
59
59
  */
@@ -75,9 +75,11 @@ export interface ContractSpaceSeedPhaseResult {
75
75
  * For every extension that exposes a `contractSpace`:
76
76
  *
77
77
  * 1. Read the on-disk head ref (returns `null` on first emit).
78
- * 2. Re-emit `contract.json` / `contract.d.ts` / `refs/head.json`
79
- * unconditionally via {@link emitContractSpaceArtefacts}. The
80
- * framework owns these files; re-emit is the contract.
78
+ * 2. Write the head contract into the migrations-root snapshot store
79
+ * (write-if-absent, keyed by hash) and unconditionally re-emit
80
+ * `refs/head.json`, via {@link emitContractSpaceArtifacts}. The
81
+ * framework owns `refs/head.json`; re-emit is the contract for that
82
+ * file. The snapshot itself is only written once per distinct hash.
81
83
  * 3. Materialise any descriptor-shipped migration packages not yet on
82
84
  * disk via {@link materialiseExtensionMigrationPackageIfMissing}.
83
85
  * Existing packages are left untouched (by-existence skip).
@@ -144,7 +146,7 @@ export async function runContractSpaceSeedPhase(
144
146
  const onDiskHeadRef = await readContractSpaceHeadRef(inputs.migrationsDir, space.spaceId);
145
147
  const priorHash = onDiskHeadRef?.hash ?? null;
146
148
 
147
- await emitContractSpaceArtefacts(inputs.migrationsDir, space.spaceId, {
149
+ await emitContractSpaceArtifacts(inputs.migrationsDir, space.spaceId, {
148
150
  contract: descriptor.contractJson,
149
151
  contractDts: buildPlaceholderContractDts(space.spaceId),
150
152
  headRef: { hash: descriptor.headRef.hash, invariants: descriptor.headRef.invariants },
@@ -27,9 +27,9 @@ export function looksLikeFullHash(input: string): boolean {
27
27
 
28
28
  export type FromResolution =
29
29
  | { kind: 'greenfield'; fromHash: null; fromContract: null }
30
- | { kind: 'graph-node'; fromHash: string; fromContract: Contract; sourceDir: string }
30
+ | { kind: 'graph-node'; fromHash: string; fromContract: Contract }
31
31
  | {
32
- kind: 'snapshot';
32
+ kind: 'ref';
33
33
  fromHash: string;
34
34
  fromContract: Contract;
35
35
  contractDts: string;
@@ -81,7 +81,7 @@ export function assertFromIsGraphNode(
81
81
 
82
82
  type RefContractResolution =
83
83
  | {
84
- kind: 'snapshot';
84
+ kind: 'ref';
85
85
  hash: string;
86
86
  contract: Contract;
87
87
  contractJson: unknown;
@@ -93,7 +93,6 @@ type RefContractResolution =
93
93
  contract: Contract;
94
94
  contractJson: unknown;
95
95
  contractDts: string;
96
- sourceDir: string;
97
96
  };
98
97
 
99
98
  async function resolveContractRef(
@@ -107,9 +106,9 @@ async function resolveContractRef(
107
106
  try {
108
107
  const at = await space.contractAt(hash, refName !== undefined ? { refName } : undefined);
109
108
 
110
- if (at.provenance === 'snapshot') {
109
+ if (at.provenance === 'ref') {
111
110
  return ok({
112
- kind: 'snapshot',
111
+ kind: 'ref',
113
112
  hash: at.hash,
114
113
  contract: at.contract,
115
114
  contractJson: at.contractJson,
@@ -123,7 +122,6 @@ async function resolveContractRef(
123
122
  contract: at.contract,
124
123
  contractJson: at.contractJson,
125
124
  contractDts: at.contractDts,
126
- sourceDir: at.sourceDir,
127
125
  });
128
126
  } catch (error) {
129
127
  return mapContractAtError(
@@ -152,7 +150,6 @@ async function resolveFromPolicy(
152
150
  kind: 'graph-node',
153
151
  fromHash: resolution.value.hash,
154
152
  fromContract: resolution.value.contract,
155
- sourceDir: resolution.value.sourceDir,
156
153
  });
157
154
  }
158
155
 
@@ -178,7 +175,7 @@ async function resolveFromPolicy(
178
175
  throw error;
179
176
  }
180
177
  return ok({
181
- kind: 'snapshot',
178
+ kind: 'ref',
182
179
  fromHash: hash,
183
180
  fromContract: contract,
184
181
  contractDts,
@@ -1,8 +1,14 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import type { ContractIR } from '@prisma-next/migration-tools/refs';
3
- import { writeRefPaired } from '@prisma-next/migration-tools/refs';
2
+ import { writeContractSnapshot } from '@prisma-next/migration-tools/contract-snapshot-store';
3
+ import { errorInvalidRefName } from '@prisma-next/migration-tools/errors';
4
+ import { validateRefName, writeRef } from '@prisma-next/migration-tools/refs';
4
5
  import { ifDefined } from '@prisma-next/utils/defined';
5
6
 
7
+ export interface ContractIR {
8
+ readonly contract: unknown;
9
+ readonly contractDts: string;
10
+ }
11
+
6
12
  export interface RefAdvancementFields {
7
13
  readonly advancedRef: { readonly name: string; readonly hash: string } | null;
8
14
  readonly plannedAdvanceRef: { readonly name: string; readonly hash: string } | null;
@@ -32,11 +38,22 @@ export async function readContractIR(
32
38
 
33
39
  export async function executeRefAdvancement(
34
40
  refsDir: string,
41
+ migrationsDir: string,
35
42
  name: string,
36
43
  hash: string,
37
44
  contractIR: ContractIR,
38
45
  ): Promise<{ name: string; hash: string }> {
39
- await writeRefPaired(refsDir, name, { hash, invariants: [] }, contractIR);
46
+ // Validate the ref name before writing anything: writeRef validates it too,
47
+ // but only after the store write below, which would otherwise leave a
48
+ // (harmless, but pointless) orphan store entry on an invalid name.
49
+ if (!validateRefName(name)) {
50
+ throw errorInvalidRefName(name);
51
+ }
52
+ await writeContractSnapshot(migrationsDir, hash, {
53
+ contractJson: contractIR.contract,
54
+ contractDts: contractIR.contractDts,
55
+ });
56
+ await writeRef(refsDir, name, { hash, invariants: [] });
40
57
  return { name, hash };
41
58
  }
42
59
 
@@ -44,6 +61,7 @@ export async function buildRefAdvancementFields(options: {
44
61
  readonly advanceRef?: string;
45
62
  readonly db?: string;
46
63
  readonly refsDir: string;
64
+ readonly migrationsDir: string;
47
65
  readonly contractIR: ContractIR;
48
66
  readonly mode: 'plan' | 'apply';
49
67
  readonly hash: string;
@@ -60,6 +78,7 @@ export async function buildRefAdvancementFields(options: {
60
78
  }
61
79
  const advancedRef = await executeRefAdvancement(
62
80
  options.refsDir,
81
+ options.migrationsDir,
63
82
  name,
64
83
  options.hash,
65
84
  options.contractIR,