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

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 (61) hide show
  1. package/README.md +3 -4
  2. package/dist/cli.mjs +6 -6
  3. package/dist/commands/db-sign.d.mts.map +1 -1
  4. package/dist/commands/db-sign.mjs +4 -6
  5. package/dist/commands/db-sign.mjs.map +1 -1
  6. package/dist/commands/db-update.d.mts.map +1 -1
  7. package/dist/commands/db-update.mjs +5 -8
  8. package/dist/commands/db-update.mjs.map +1 -1
  9. package/dist/commands/migrate.mjs +2 -2
  10. package/dist/commands/migration-check.d.mts +3 -1
  11. package/dist/commands/migration-check.d.mts.map +1 -1
  12. package/dist/commands/migration-check.mjs +1 -1
  13. package/dist/commands/migration-graph.mjs +1 -1
  14. package/dist/commands/migration-list.d.mts.map +1 -1
  15. package/dist/commands/migration-list.mjs +1 -1
  16. package/dist/commands/migration-new.d.mts.map +1 -1
  17. package/dist/commands/migration-new.mjs +12 -36
  18. package/dist/commands/migration-new.mjs.map +1 -1
  19. package/dist/commands/migration-plan.d.mts.map +1 -1
  20. package/dist/commands/migration-plan.mjs +1 -1
  21. package/dist/commands/migration-status.mjs +1 -1
  22. package/dist/commands/ref.d.mts.map +1 -1
  23. package/dist/commands/ref.mjs +7 -9
  24. package/dist/commands/ref.mjs.map +1 -1
  25. package/dist/{contract-at-errors-D9gEjYtg.mjs → contract-at-errors-CTtzKR6q.mjs} +6 -10
  26. package/dist/contract-at-errors-CTtzKR6q.mjs.map +1 -0
  27. package/dist/exports/init-output.d.mts.map +1 -1
  28. package/dist/exports/init-output.mjs +1 -1
  29. package/dist/{init-C3JhkHg-.mjs → init-CgKEi_t5.mjs} +53 -45
  30. package/dist/init-CgKEi_t5.mjs.map +1 -0
  31. package/dist/{migration-check-MHT3w7_v.mjs → migration-check-CcuayFB2.mjs} +49 -22
  32. package/dist/migration-check-CcuayFB2.mjs.map +1 -0
  33. package/dist/{migration-list-BAKujUjh.mjs → migration-list-6z6vuXHa.mjs} +3 -3
  34. package/dist/migration-list-6z6vuXHa.mjs.map +1 -0
  35. package/dist/{migration-plan-CreSVtWZ.mjs → migration-plan-DuMEUejG.mjs} +46 -54
  36. package/dist/migration-plan-DuMEUejG.mjs.map +1 -0
  37. package/dist/{migration-status-YgEvXf-u.mjs → migration-status-B61YOaMl.mjs} +3 -2
  38. package/dist/{migration-status-YgEvXf-u.mjs.map → migration-status-B61YOaMl.mjs.map} +1 -1
  39. package/dist/{output-mEQ74_nd.mjs → output-DWNYqZGc.mjs} +5 -6
  40. package/dist/output-DWNYqZGc.mjs.map +1 -0
  41. package/package.json +21 -21
  42. package/src/commands/db-sign.ts +6 -4
  43. package/src/commands/db-update.ts +13 -6
  44. package/src/commands/init/hygiene-gitattributes.ts +37 -21
  45. package/src/commands/init/init.ts +6 -6
  46. package/src/commands/init/output.ts +4 -5
  47. package/src/commands/init/reinit-cleanup.ts +16 -17
  48. package/src/commands/migration-check.ts +69 -32
  49. package/src/commands/migration-list.ts +1 -5
  50. package/src/commands/migration-new.ts +15 -39
  51. package/src/commands/migration-plan.ts +54 -76
  52. package/src/commands/ref.ts +18 -7
  53. package/src/utils/contract-at-errors.ts +9 -13
  54. package/src/utils/contract-space-seed-phase.ts +8 -6
  55. package/src/utils/plan-resolution.ts +1 -4
  56. package/dist/contract-at-errors-D9gEjYtg.mjs.map +0 -1
  57. package/dist/init-C3JhkHg-.mjs.map +0 -1
  58. package/dist/migration-check-MHT3w7_v.mjs.map +0 -1
  59. package/dist/migration-list-BAKujUjh.mjs.map +0 -1
  60. package/dist/migration-plan-CreSVtWZ.mjs.map +0 -1
  61. package/dist/output-mEQ74_nd.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { loadConfig } from '@prisma-next/config-loader';
4
4
  import { createControlStack } from '@prisma-next/framework-components/control';
@@ -7,6 +7,11 @@ import type {
7
7
  IntegrityViolation,
8
8
  } from '@prisma-next/migration-tools/aggregate';
9
9
  import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
10
+ import {
11
+ contractSnapshotDir,
12
+ readContractSnapshotJson,
13
+ } from '@prisma-next/migration-tools/contract-snapshot-store';
14
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
10
15
  import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
11
16
  import { verifyMigrationHash } from '@prisma-next/migration-tools/hash';
12
17
  import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
@@ -18,7 +23,6 @@ import type { Refs } from '@prisma-next/migration-tools/refs';
18
23
  import {
19
24
  isValidSpaceId,
20
25
  listContractSpaceDirectories,
21
- RESERVED_SPACE_SUBDIR_NAMES,
22
26
  spaceMigrationDirectory,
23
27
  spaceRefsDirectory,
24
28
  } from '@prisma-next/migration-tools/spaces';
@@ -92,32 +96,57 @@ function checkFileExists(
92
96
  return null;
93
97
  }
94
98
 
95
- function checkSnapshotConsistency(
99
+ /**
100
+ * Snapshot-store consistency check for one migration package (D6.5):
101
+ * absent store entry is not an issue (runner independence, ADR 199 — a
102
+ * package dir with only `migration.json` + `ops.json` is legitimate); a
103
+ * present entry whose inner `storage.storageHash` disagrees with
104
+ * `pkg.metadata.to` is `MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH`; an unparseable store
105
+ * entry (or a malformed `to`) is `MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE`.
106
+ */
107
+ async function checkSnapshotConsistency(
96
108
  spaceId: string,
97
109
  pkg: OnDiskMigrationPackage,
98
- ): CheckFailure | null {
99
- const endContractPath = join(pkg.dirPath, 'end-contract.json');
100
- if (!existsSync(endContractPath)) return null;
110
+ migrationsDir: string,
111
+ ): Promise<CheckFailure | null> {
112
+ let snapshotDir: string;
101
113
  try {
102
- const raw = JSON.parse(readFileSync(endContractPath, 'utf-8')) as Record<string, unknown>;
103
- const storage = raw['storage'] as Record<string, unknown> | undefined;
104
- const snapshotHash = storage?.['storageHash'];
105
- if (typeof snapshotHash === 'string' && snapshotHash !== pkg.metadata.to) {
106
- return {
107
- space: spaceId,
108
- code: 'MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH',
109
- where: migrationPathRelative(pkg.dirPath),
110
- why: `Migration "${pkg.dirName}" declares to=${pkg.metadata.to} but end-contract.json has storageHash=${snapshotHash}`,
111
- fix: 'Re-emit the migration package so migration.json and end-contract.json agree.',
112
- };
113
- }
114
+ snapshotDir = contractSnapshotDir(migrationsDir, pkg.metadata.to);
114
115
  } catch {
115
116
  return {
116
117
  space: spaceId,
117
118
  code: 'MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE',
118
119
  where: migrationPathRelative(pkg.dirPath),
119
- why: `Migration "${pkg.dirName}" has an unparseable end-contract.json.`,
120
- fix: 'Re-emit the migration package to repair the snapshot file.',
120
+ why: `Migration "${pkg.dirName}" declares to="${pkg.metadata.to}", which is not a well-formed contract snapshot hash.`,
121
+ fix: 'Re-emit the migration package so migration.json declares a valid `sha256:<64hex>` to-hash.',
122
+ };
123
+ }
124
+
125
+ let raw: unknown;
126
+ try {
127
+ raw = await readContractSnapshotJson(migrationsDir, pkg.metadata.to);
128
+ } catch (error) {
129
+ if (MigrationToolsError.is(error) && error.code === 'MIGRATION.CONTRACT_SNAPSHOT_MISSING') {
130
+ return null;
131
+ }
132
+ return {
133
+ space: spaceId,
134
+ code: 'MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE',
135
+ where: migrationPathRelative(pkg.dirPath),
136
+ why: `Migration "${pkg.dirName}" has an unparseable contract snapshot at ${snapshotDir}/contract.json.`,
137
+ fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
138
+ };
139
+ }
140
+ const record = raw !== null && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
141
+ const storage = record['storage'] as Record<string, unknown> | undefined;
142
+ const snapshotHash = storage?.['storageHash'];
143
+ if (typeof snapshotHash === 'string' && snapshotHash !== pkg.metadata.to) {
144
+ return {
145
+ space: spaceId,
146
+ code: 'MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH',
147
+ where: migrationPathRelative(pkg.dirPath),
148
+ why: `Migration "${pkg.dirName}" declares to=${pkg.metadata.to} but its contract snapshot has storageHash=${snapshotHash}`,
149
+ fix: 'Re-emit the migration package so migration.json and its contract snapshot agree.',
121
150
  };
122
151
  }
123
152
  return null;
@@ -137,6 +166,8 @@ export interface CheckSpace {
137
166
  readonly graph: MigrationGraph;
138
167
  readonly migrationsDir: string;
139
168
  readonly refsDir: string;
169
+ /** Migrations root the contract-snapshot store lives under — shared by every space. */
170
+ readonly projectMigrationsDir: string;
140
171
  }
141
172
 
142
173
  /**
@@ -152,9 +183,7 @@ export async function enumerateCheckSpaces(
152
183
  projectMigrationsDir: string,
153
184
  ): Promise<readonly CheckSpace[]> {
154
185
  const candidateDirs = await listContractSpaceDirectories(projectMigrationsDir);
155
- const onDiskSpaceIds = new Set(
156
- candidateDirs.filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name)).filter(isValidSpaceId),
157
- );
186
+ const onDiskSpaceIds = new Set(candidateDirs.filter(isValidSpaceId));
158
187
  const spaces: CheckSpace[] = [];
159
188
  for (const space of aggregate.spaces()) {
160
189
  const spaceId = space.spaceId;
@@ -168,6 +197,7 @@ export async function enumerateCheckSpaces(
168
197
  graph: space.graph(),
169
198
  migrationsDir,
170
199
  refsDir: spaceRefsDirectory(migrationsDir),
200
+ projectMigrationsDir,
171
201
  });
172
202
  }
173
203
  return spaces;
@@ -238,12 +268,15 @@ function checkDanglingRefs(space: CheckSpace): readonly CheckFailure[] {
238
268
  return failures;
239
269
  }
240
270
 
241
- function checkSpace(space: CheckSpace): readonly CheckFailure[] {
271
+ async function checkSpace(space: CheckSpace): Promise<readonly CheckFailure[]> {
272
+ const snapshotFailures = await Promise.all(
273
+ space.packages.map((pkg) =>
274
+ checkSnapshotConsistency(space.spaceId, pkg, space.projectMigrationsDir),
275
+ ),
276
+ );
242
277
  return [
243
278
  ...checkManifestFilesPresent(space),
244
- ...space.packages
245
- .map((pkg) => checkSnapshotConsistency(space.spaceId, pkg))
246
- .filter((f): f is CheckFailure => f !== null),
279
+ ...snapshotFailures.filter((f): f is CheckFailure => f !== null),
247
280
  ...checkReachability(space),
248
281
  ...checkDanglingRefs(space),
249
282
  ];
@@ -272,9 +305,9 @@ export interface RunMigrationCheckInputs {
272
305
  * Aggregate-integrity violations (which already span every space) are folded
273
306
  * in by the caller, not here.
274
307
  */
275
- export function runMigrationCheck(
308
+ export async function runMigrationCheck(
276
309
  inputs: RunMigrationCheckInputs,
277
- ): Result<MigrationCheckResult, CliStructuredError> {
310
+ ): Promise<Result<MigrationCheckResult, CliStructuredError>> {
278
311
  const { spaces, spaceFilter } = inputs;
279
312
 
280
313
  if (spaceFilter !== undefined && !isValidSpaceId(spaceFilter)) {
@@ -287,7 +320,7 @@ export function runMigrationCheck(
287
320
  const scopedSpaces =
288
321
  spaceFilter !== undefined ? spaces.filter((s) => s.spaceId === spaceFilter) : spaces;
289
322
 
290
- const failures = scopedSpaces.flatMap(checkSpace);
323
+ const failures = (await Promise.all(scopedSpaces.map(checkSpace))).flat();
291
324
  if (failures.length === 0) {
292
325
  return ok({ ok: true, failures: [], summary: 'All checks passed' });
293
326
  }
@@ -365,7 +398,7 @@ async function executeMigrationCheckCommand(
365
398
  });
366
399
  }
367
400
 
368
- const checkResult = runMigrationCheck({
401
+ const checkResult = await runMigrationCheck({
369
402
  spaces,
370
403
  ...(options.space !== undefined ? { spaceFilter: options.space } : {}),
371
404
  });
@@ -558,7 +591,11 @@ async function checkSingleTarget(
558
591
  });
559
592
  }
560
593
 
561
- const snapshotFailure = checkSnapshotConsistency(matchedSpace.spaceId, matchedPkg);
594
+ const snapshotFailure = await checkSnapshotConsistency(
595
+ matchedSpace.spaceId,
596
+ matchedPkg,
597
+ matchedSpace.projectMigrationsDir,
598
+ );
562
599
  if (snapshotFailure) failures.push(snapshotFailure);
563
600
 
564
601
  const resolvedSpaceId = matchedSpace.spaceId !== 'app' ? matchedSpace.spaceId : undefined;
@@ -9,7 +9,6 @@ import {
9
9
  APP_SPACE_ID,
10
10
  isValidSpaceId,
11
11
  listContractSpaceDirectories,
12
- RESERVED_SPACE_SUBDIR_NAMES,
13
12
  } from '@prisma-next/migration-tools/spaces';
14
13
  import { ifDefined } from '@prisma-next/utils/defined';
15
14
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
@@ -81,10 +80,7 @@ export function listRefsByContractHash(
81
80
 
82
81
  async function orderedOnDiskSpaceIds(projectMigrationsDir: string): Promise<readonly string[]> {
83
82
  const candidateDirs = await listContractSpaceDirectories(projectMigrationsDir);
84
- return candidateDirs
85
- .filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name))
86
- .filter(isValidSpaceId)
87
- .sort(compareSpaceIds);
83
+ return candidateDirs.filter(isValidSpaceId).sort(compareSpaceIds);
88
84
  }
89
85
 
90
86
  /**
@@ -14,12 +14,13 @@ import type { Contract } from '@prisma-next/contract/types';
14
14
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
15
15
  import { APP_SPACE_ID, createControlStack } from '@prisma-next/framework-components/control';
16
16
  import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
17
- import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
18
17
  import {
19
- copyFilesWithRename,
20
- formatMigrationDirName,
21
- writeMigrationPackage,
22
- } from '@prisma-next/migration-tools/io';
18
+ contractSnapshotDir,
19
+ snapshotsImportPathFrom,
20
+ writeContractSnapshot,
21
+ } from '@prisma-next/migration-tools/contract-snapshot-store';
22
+ import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
23
+ import { formatMigrationDirName, writeMigrationPackage } from '@prisma-next/migration-tools/io';
23
24
  import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
24
25
  import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
25
26
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
@@ -28,7 +29,6 @@ import { Command } from 'commander';
28
29
  import { join, relative } from 'pathe';
29
30
  import {
30
31
  CliStructuredError,
31
- errorFileNotFound,
32
32
  errorRuntime,
33
33
  errorTargetMigrationNotSupported,
34
34
  errorUnexpected,
@@ -133,7 +133,6 @@ async function executeMigrationNewCommand(
133
133
  const graph = aggregate.app.graph();
134
134
 
135
135
  let fromHash: string | null = null;
136
- let fromContractSourceDir: string | null = null;
137
136
 
138
137
  if (packages.length > 0) {
139
138
  if (options.from) {
@@ -147,17 +146,10 @@ async function executeMigrationNewCommand(
147
146
  );
148
147
  }
149
148
  fromHash = match.metadata.to;
150
- fromContractSourceDir = match.dirPath;
151
149
  } else {
152
150
  const latestMigration = findLatestMigration(graph);
153
151
  if (latestMigration) {
154
152
  fromHash = latestMigration.to;
155
- const leafPkg = packages.find(
156
- (p) => p.metadata.migrationHash === latestMigration.migrationHash,
157
- );
158
- if (leafPkg) {
159
- fromContractSourceDir = leafPkg.dirPath;
160
- }
161
153
  }
162
154
  }
163
155
  }
@@ -209,39 +201,23 @@ async function executeMigrationNewCommand(
209
201
 
210
202
  await writeMigrationPackage(packageDir, metadata, []);
211
203
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
212
- await copyFilesWithRename(packageDir, [
213
- { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
214
- { sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
204
+ const [contractJsonRaw, contractDts] = await Promise.all([
205
+ readFile(destinationArtifacts.jsonPath, 'utf-8'),
206
+ readFile(destinationArtifacts.dtsPath, 'utf-8'),
215
207
  ]);
216
- if (fromContractSourceDir !== null) {
217
- const sourceArtifacts = getEmittedArtifactPaths(
218
- join(fromContractSourceDir, 'end-contract.json'),
219
- );
220
- try {
221
- await copyFilesWithRename(packageDir, [
222
- { sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
223
- { sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
224
- ]);
225
- } catch (error) {
226
- if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
227
- return notOk(
228
- errorFileNotFound(sourceArtifacts.jsonPath, {
229
- why: `Predecessor migration is missing its destination contract snapshot at ${sourceArtifacts.jsonPath}`,
230
- fix: '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.',
231
- }),
232
- );
233
- }
234
- throw error;
235
- }
236
- }
208
+ await writeContractSnapshot(migrationsDir, toStorageHash, {
209
+ contractJson: JSON.parse(contractJsonRaw) as unknown,
210
+ contractDts,
211
+ });
237
212
 
238
213
  const planner = migrations.createPlanner(controlAdapter);
239
214
  const emptyPlan = planner.emptyMigration(
240
215
  {
241
216
  packageDir,
242
- contractJsonPath: join(packageDir, 'end-contract.json'),
217
+ contractJsonPath: join(contractSnapshotDir(migrationsDir, toStorageHash), 'contract.json'),
243
218
  fromHash,
244
219
  toHash: toStorageHash,
220
+ snapshotsImportPath: snapshotsImportPathFrom(packageDir, migrationsDir),
245
221
  },
246
222
  APP_SPACE_ID,
247
223
  );
@@ -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
334
  case 'snapshot':
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,6 +1,9 @@
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';
@@ -114,17 +117,25 @@ export async function executeRefSetCommand(
114
117
  return notOk(errorRefSetBundleNotFound(resolvedHash));
115
118
  }
116
119
 
117
- const contractJsonPath = join(matchingBundle.dirPath, 'end-contract.json');
120
+ const contractJsonPath = join(
121
+ contractSnapshotDir(migrationsDir, resolvedHash),
122
+ 'contract.json',
123
+ );
118
124
  let contractJson: Record<string, unknown>;
119
125
  try {
120
- const raw = await readFile(contractJsonPath, 'utf-8');
121
- contractJson = JSON.parse(raw) as Record<string, unknown>;
126
+ contractJson = (await readContractSnapshotJson(migrationsDir, resolvedHash)) as Record<
127
+ string,
128
+ unknown
129
+ >;
122
130
  } catch (readError) {
123
- if (readError instanceof Error && (readError as NodeJS.ErrnoException).code === 'ENOENT') {
131
+ if (
132
+ MigrationToolsError.is(readError) &&
133
+ readError.code === 'MIGRATION.CONTRACT_SNAPSHOT_MISSING'
134
+ ) {
124
135
  return notOk(
125
136
  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.',
137
+ why: `Migration bundle for hash ${resolvedHash} is missing its contract snapshot at ${contractJsonPath}`,
138
+ fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
128
139
  }),
129
140
  );
130
141
  }
@@ -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,
@@ -66,22 +65,19 @@ export function mapContractAtError(
66
65
  fix: error.fix,
67
66
  }),
68
67
  );
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;
68
+ case 'MIGRATION.CONTRACT_SNAPSHOT_MISSING': {
69
+ const expectedPath =
70
+ typeof error.details?.['expectedPath'] === 'string'
71
+ ? error.details['expectedPath']
72
+ : 'migrations/snapshots/';
74
73
  const role = options?.artifactRole ?? 'from';
75
74
  return notOk(
76
- errorFileNotFound(jsonPath, {
75
+ errorFileNotFound(expectedPath, {
77
76
  why:
78
77
  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.',
78
+ ? `Target migration is missing its contract snapshot at ${expectedPath}`
79
+ : `Predecessor migration is missing its contract snapshot at ${expectedPath}`,
80
+ fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
85
81
  }),
86
82
  );
87
83
  }