@prisma-next/migration-tools 0.11.0 → 0.12.0

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 (118) hide show
  1. package/README.md +4 -4
  2. package/dist/{errors-DGYwcwXs.mjs → errors-vFROOhCR.mjs} +46 -21
  3. package/dist/errors-vFROOhCR.mjs.map +1 -0
  4. package/dist/exports/aggregate.d.mts +328 -204
  5. package/dist/exports/aggregate.d.mts.map +1 -1
  6. package/dist/exports/aggregate.mjs +480 -243
  7. package/dist/exports/aggregate.mjs.map +1 -1
  8. package/dist/exports/errors.d.mts +2 -2
  9. package/dist/exports/errors.d.mts.map +1 -1
  10. package/dist/exports/errors.mjs +1 -1
  11. package/dist/exports/graph.d.mts +1 -1
  12. package/dist/exports/hash.d.mts +8 -9
  13. package/dist/exports/hash.d.mts.map +1 -1
  14. package/dist/exports/hash.mjs +1 -1
  15. package/dist/exports/invariants.d.mts +1 -1
  16. package/dist/exports/invariants.d.mts.map +1 -1
  17. package/dist/exports/invariants.mjs +1 -1
  18. package/dist/exports/io.d.mts +2 -83
  19. package/dist/exports/io.mjs +1 -1
  20. package/dist/exports/metadata.d.mts +2 -2
  21. package/dist/exports/migration-graph.d.mts +9 -2
  22. package/dist/exports/migration-graph.d.mts.map +1 -0
  23. package/dist/exports/migration-graph.mjs +3 -2
  24. package/dist/exports/migration-ts.d.mts.map +1 -1
  25. package/dist/exports/migration-ts.mjs.map +1 -1
  26. package/dist/exports/migration.d.mts +5 -6
  27. package/dist/exports/migration.d.mts.map +1 -1
  28. package/dist/exports/migration.mjs +14 -32
  29. package/dist/exports/migration.mjs.map +1 -1
  30. package/dist/exports/package.d.mts +1 -1
  31. package/dist/exports/ref-resolution.d.mts +2 -2
  32. package/dist/exports/ref-resolution.d.mts.map +1 -1
  33. package/dist/exports/ref-resolution.mjs +1 -1
  34. package/dist/exports/ref-resolution.mjs.map +1 -1
  35. package/dist/exports/refs.d.mts +15 -2
  36. package/dist/exports/refs.d.mts.map +1 -0
  37. package/dist/exports/refs.mjs +3 -2
  38. package/dist/exports/spaces.d.mts +31 -132
  39. package/dist/exports/spaces.d.mts.map +1 -1
  40. package/dist/exports/spaces.mjs +13 -9
  41. package/dist/exports/spaces.mjs.map +1 -1
  42. package/dist/{graph-BrLXqoUc.d.mts → graph-3dLMZp5l.d.mts} +1 -2
  43. package/dist/graph-3dLMZp5l.d.mts.map +1 -0
  44. package/dist/graph-membership-BV23F1IV.mjs +15 -0
  45. package/dist/graph-membership-BV23F1IV.mjs.map +1 -0
  46. package/dist/{hash-Cr4WIr4Z.mjs → hash--Y7vCpN3.mjs} +8 -9
  47. package/dist/hash--Y7vCpN3.mjs.map +1 -0
  48. package/dist/{invariants-0daYEzyo.mjs → invariants-C23nXy1c.mjs} +2 -2
  49. package/dist/{invariants-0daYEzyo.mjs.map → invariants-C23nXy1c.mjs.map} +1 -1
  50. package/dist/{io-BPLfzvZe.mjs → io-BGlPOt9b.mjs} +100 -13
  51. package/dist/io-BGlPOt9b.mjs.map +1 -0
  52. package/dist/io-BH4G3F-i.d.mts +124 -0
  53. package/dist/io-BH4G3F-i.d.mts.map +1 -0
  54. package/dist/metadata-Bp9X04gM.d.mts +2 -0
  55. package/dist/{migration-graph-nlS4TRpn.mjs → migration-graph-BMAqSfv9.mjs} +6 -26
  56. package/dist/migration-graph-BMAqSfv9.mjs.map +1 -0
  57. package/dist/{migration-graph-De0dUZoC.d.mts → migration-graph-CWEM2SLR.d.mts} +6 -6
  58. package/dist/migration-graph-CWEM2SLR.d.mts.map +1 -0
  59. package/dist/op-schema-D5qkXfEf.mjs.map +1 -1
  60. package/dist/{package-DZj8YvD0.d.mts → package-Ca-J_z_0.d.mts} +1 -1
  61. package/dist/package-Ca-J_z_0.d.mts.map +1 -0
  62. package/dist/{read-contract-space-contract-DRueB4Aa.mjs → read-contract-space-contract-TbeXuJXL.mjs} +32 -5
  63. package/dist/read-contract-space-contract-TbeXuJXL.mjs.map +1 -0
  64. package/dist/{refs-BDHo5l_g.mjs → refs-C-_WUrPw.mjs} +97 -4
  65. package/dist/refs-C-_WUrPw.mjs.map +1 -0
  66. package/dist/refs-C7wuYFqZ.d.mts +42 -0
  67. package/dist/refs-C7wuYFqZ.d.mts.map +1 -0
  68. package/dist/snapshot-Bazwo13S.mjs +137 -0
  69. package/dist/snapshot-Bazwo13S.mjs.map +1 -0
  70. package/dist/verify-contract-spaces-BdysZdQk.d.mts +132 -0
  71. package/dist/verify-contract-spaces-BdysZdQk.d.mts.map +1 -0
  72. package/package.json +18 -9
  73. package/src/aggregate/aggregate.ts +266 -0
  74. package/src/aggregate/check-integrity.ts +243 -0
  75. package/src/aggregate/loader.ts +161 -334
  76. package/src/aggregate/planner-types.ts +14 -14
  77. package/src/aggregate/planner.ts +20 -23
  78. package/src/aggregate/project-schema-to-space.ts +3 -8
  79. package/src/aggregate/strategies/graph-walk.ts +15 -10
  80. package/src/aggregate/strategies/synth.ts +4 -4
  81. package/src/aggregate/types.ts +81 -62
  82. package/src/aggregate/verifier.ts +23 -23
  83. package/src/assert-descriptor-self-consistency.ts +6 -0
  84. package/src/compute-extension-space-apply-path.ts +1 -1
  85. package/src/emit-contract-space-artefacts.ts +4 -3
  86. package/src/errors.ts +58 -2
  87. package/src/exports/aggregate.ts +29 -19
  88. package/src/exports/io.ts +2 -0
  89. package/src/exports/metadata.ts +1 -1
  90. package/src/exports/migration-graph.ts +1 -0
  91. package/src/exports/refs.ts +11 -0
  92. package/src/exports/spaces.ts +3 -0
  93. package/src/graph-membership.ts +17 -0
  94. package/src/graph.ts +0 -1
  95. package/src/hash.ts +7 -8
  96. package/src/integrity-violation.ts +114 -0
  97. package/src/io.ts +139 -14
  98. package/src/metadata.ts +1 -1
  99. package/src/migration-base.ts +10 -30
  100. package/src/migration-graph.ts +7 -35
  101. package/src/read-contract-space-head-ref.ts +5 -2
  102. package/src/refs/snapshot.ts +199 -0
  103. package/src/refs.ts +124 -1
  104. package/src/space-layout.ts +30 -0
  105. package/dist/errors-DGYwcwXs.mjs.map +0 -1
  106. package/dist/exports/io.d.mts.map +0 -1
  107. package/dist/graph-BrLXqoUc.d.mts.map +0 -1
  108. package/dist/hash-Cr4WIr4Z.mjs.map +0 -1
  109. package/dist/io-BPLfzvZe.mjs.map +0 -1
  110. package/dist/metadata-BFX0xdz8.d.mts +0 -2
  111. package/dist/migration-graph-De0dUZoC.d.mts.map +0 -1
  112. package/dist/migration-graph-nlS4TRpn.mjs.map +0 -1
  113. package/dist/package-DZj8YvD0.d.mts.map +0 -1
  114. package/dist/read-contract-space-contract-DRueB4Aa.mjs.map +0 -1
  115. package/dist/refs-BDHo5l_g.mjs.map +0 -1
  116. package/dist/refs-CDaNerhT.d.mts +0 -16
  117. package/dist/refs-CDaNerhT.d.mts.map +0 -1
  118. package/src/aggregate/extract-storage-element-names.ts +0 -75
@@ -0,0 +1,137 @@
1
+ import { g as errorInvalidRefName, h as errorInvalidRefFile, t as MigrationToolsError } from "./errors-vFROOhCR.mjs";
2
+ import { d as writeRef, l as validateRefName, n as deleteRef } from "./refs-C-_WUrPw.mjs";
3
+ import { basename, dirname, join } from "pathe";
4
+ import { access, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
5
+ import { type } from "arktype";
6
+ import { randomBytes } from "node:crypto";
7
+ import { canonicalizeJson } from "@prisma-next/framework-components/utils";
8
+ //#region src/refs/snapshot.ts
9
+ const ContractIrSchema = type({
10
+ targetFamily: "string",
11
+ target: "string",
12
+ profileHash: "string",
13
+ storage: type({ storageHash: "string" }),
14
+ domain: type({ namespaces: "object" })
15
+ });
16
+ function hasErrnoCode(error, code) {
17
+ return error instanceof Error && error.code === code;
18
+ }
19
+ function snapshotJsonPath(refsDir, name) {
20
+ return join(refsDir, `${name}.contract.json`);
21
+ }
22
+ function snapshotDtsPath(refsDir, name) {
23
+ return join(refsDir, `${name}.contract.d.ts`);
24
+ }
25
+ function tmpPathFor(finalPath) {
26
+ return join(dirname(finalPath), `.${basename(finalPath)}.${Date.now()}-${randomBytes(4).toString("hex")}.tmp`);
27
+ }
28
+ async function atomicWriteFile(finalPath, content) {
29
+ await mkdir(dirname(finalPath), { recursive: true });
30
+ const tmpPath = tmpPathFor(finalPath);
31
+ await writeFile(tmpPath, content);
32
+ await rename(tmpPath, finalPath);
33
+ }
34
+ async function unlinkIfExists(filePath) {
35
+ try {
36
+ await unlink(filePath);
37
+ } catch (error) {
38
+ if (hasErrnoCode(error, "ENOENT")) return;
39
+ throw error;
40
+ }
41
+ }
42
+ function parseContractSnapshotJson(filePath, raw) {
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(raw);
46
+ } catch {
47
+ throw errorInvalidRefFile(filePath, "Failed to parse as JSON");
48
+ }
49
+ const result = ContractIrSchema(parsed);
50
+ if (result instanceof type.errors) throw errorInvalidRefFile(filePath, result.summary);
51
+ return result;
52
+ }
53
+ async function writeRefSnapshot(refsDir, name, snapshot) {
54
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
55
+ const jsonPath = snapshotJsonPath(refsDir, name);
56
+ const dtsPath = snapshotDtsPath(refsDir, name);
57
+ const jsonContent = `${canonicalizeJson(snapshot.contract)}\n`;
58
+ const dtsContent = snapshot.contractDts.endsWith("\n") ? snapshot.contractDts : `${snapshot.contractDts}\n`;
59
+ try {
60
+ await atomicWriteFile(jsonPath, jsonContent);
61
+ } catch (error) {
62
+ await unlinkIfExists(jsonPath);
63
+ throw error;
64
+ }
65
+ try {
66
+ await atomicWriteFile(dtsPath, dtsContent);
67
+ } catch (error) {
68
+ await unlinkIfExists(jsonPath);
69
+ await unlinkIfExists(dtsPath);
70
+ throw error;
71
+ }
72
+ }
73
+ async function readRefSnapshot(refsDir, name) {
74
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
75
+ const jsonPath = snapshotJsonPath(refsDir, name);
76
+ const dtsPath = snapshotDtsPath(refsDir, name);
77
+ let raw;
78
+ try {
79
+ raw = await readFile(jsonPath, "utf-8");
80
+ } catch (error) {
81
+ if (hasErrnoCode(error, "ENOENT")) return null;
82
+ throw error;
83
+ }
84
+ const contract = parseContractSnapshotJson(jsonPath, raw);
85
+ let contractDts;
86
+ try {
87
+ contractDts = await readFile(dtsPath, "utf-8");
88
+ } catch (error) {
89
+ if (hasErrnoCode(error, "ENOENT")) throw errorInvalidRefFile(dtsPath, "Missing paired contract.d.ts snapshot file");
90
+ throw error;
91
+ }
92
+ return {
93
+ contract,
94
+ contractDts
95
+ };
96
+ }
97
+ async function deleteRefSnapshot(refsDir, name) {
98
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
99
+ await unlinkIfExists(snapshotJsonPath(refsDir, name));
100
+ await unlinkIfExists(snapshotDtsPath(refsDir, name));
101
+ }
102
+ async function writeRefPaired(refsDir, name, entry, snapshot) {
103
+ await writeRefSnapshot(refsDir, name, snapshot);
104
+ try {
105
+ await writeRef(refsDir, name, entry);
106
+ } catch (writeError) {
107
+ try {
108
+ await deleteRefSnapshot(refsDir, name);
109
+ } catch {}
110
+ throw writeError;
111
+ }
112
+ }
113
+ function isUnknownRefError(error) {
114
+ return MigrationToolsError.is(error) && error.code === "MIGRATION.UNKNOWN_REF";
115
+ }
116
+ async function snapshotFilesExist(refsDir, name) {
117
+ if (!validateRefName(name)) throw errorInvalidRefName(name);
118
+ const paths = [snapshotJsonPath(refsDir, name), snapshotDtsPath(refsDir, name)];
119
+ return (await Promise.allSettled(paths.map((filePath) => access(filePath)))).some((result) => result.status === "fulfilled");
120
+ }
121
+ async function deleteRefPaired(refsDir, name) {
122
+ if (await snapshotFilesExist(refsDir, name)) {
123
+ try {
124
+ await deleteRef(refsDir, name);
125
+ } catch (error) {
126
+ if (!isUnknownRefError(error)) throw error;
127
+ }
128
+ await deleteRefSnapshot(refsDir, name);
129
+ return;
130
+ }
131
+ await deleteRef(refsDir, name);
132
+ await deleteRefSnapshot(refsDir, name);
133
+ }
134
+ //#endregion
135
+ export { writeRefSnapshot as a, writeRefPaired as i, deleteRefSnapshot as n, readRefSnapshot as r, deleteRefPaired as t };
136
+
137
+ //# sourceMappingURL=snapshot-Bazwo13S.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-Bazwo13S.mjs","names":[],"sources":["../src/refs/snapshot.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { access, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';\nimport { canonicalizeJson } from '@prisma-next/framework-components/utils';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport { errorInvalidRefFile, errorInvalidRefName, MigrationToolsError } from '../errors';\nimport { deleteRef, type RefEntry, validateRefName, writeRef } from '../refs';\n\nexport interface ContractIR {\n readonly contract: unknown;\n readonly contractDts: string;\n}\n\nconst ContractIrSchema = type({\n targetFamily: 'string',\n target: 'string',\n profileHash: 'string',\n storage: type({\n storageHash: 'string',\n }),\n domain: type({\n namespaces: 'object',\n }),\n});\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nfunction snapshotJsonPath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.contract.json`);\n}\n\nfunction snapshotDtsPath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.contract.d.ts`);\n}\n\nfunction tmpPathFor(finalPath: string): string {\n const dir = dirname(finalPath);\n const fileName = basename(finalPath);\n return join(dir, `.${fileName}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`);\n}\n\nasync function atomicWriteFile(finalPath: string, content: string): Promise<void> {\n const dir = dirname(finalPath);\n await mkdir(dir, { recursive: true });\n const tmpPath = tmpPathFor(finalPath);\n await writeFile(tmpPath, content);\n await rename(tmpPath, finalPath);\n}\n\nasync function unlinkIfExists(filePath: string): Promise<void> {\n try {\n await unlink(filePath);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) return;\n throw error;\n }\n}\n\nfunction parseContractSnapshotJson(filePath: string, raw: string): unknown {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n }\n\n const result = ContractIrSchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n return result;\n}\n\nexport async function writeRefSnapshot(\n refsDir: string,\n name: string,\n snapshot: ContractIR,\n): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const jsonPath = snapshotJsonPath(refsDir, name);\n const dtsPath = snapshotDtsPath(refsDir, name);\n const jsonContent = `${canonicalizeJson(snapshot.contract)}\\n`;\n const dtsContent = snapshot.contractDts.endsWith('\\n')\n ? snapshot.contractDts\n : `${snapshot.contractDts}\\n`;\n\n try {\n await atomicWriteFile(jsonPath, jsonContent);\n } catch (error) {\n await unlinkIfExists(jsonPath);\n throw error;\n }\n\n try {\n await atomicWriteFile(dtsPath, dtsContent);\n } catch (error) {\n await unlinkIfExists(jsonPath);\n await unlinkIfExists(dtsPath);\n throw error;\n }\n}\n\nexport async function readRefSnapshot(refsDir: string, name: string): Promise<ContractIR | null> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const jsonPath = snapshotJsonPath(refsDir, name);\n const dtsPath = snapshotDtsPath(refsDir, name);\n\n let raw: string;\n try {\n raw = await readFile(jsonPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return null;\n }\n throw error;\n }\n\n const contract = parseContractSnapshotJson(jsonPath, raw);\n\n let contractDts: string;\n try {\n contractDts = await readFile(dtsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorInvalidRefFile(dtsPath, 'Missing paired contract.d.ts snapshot file');\n }\n throw error;\n }\n\n return { contract, contractDts };\n}\n\nexport async function deleteRefSnapshot(refsDir: string, name: string): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n await unlinkIfExists(snapshotJsonPath(refsDir, name));\n await unlinkIfExists(snapshotDtsPath(refsDir, name));\n}\n\nexport async function writeRefPaired(\n refsDir: string,\n name: string,\n entry: RefEntry,\n snapshot: ContractIR,\n): Promise<void> {\n await writeRefSnapshot(refsDir, name, snapshot);\n try {\n await writeRef(refsDir, name, entry);\n } catch (writeError) {\n try {\n await deleteRefSnapshot(refsDir, name);\n } catch {\n // Rollback failure is secondary; preserve the original writeRef error.\n }\n throw writeError;\n }\n}\n\nfunction isUnknownRefError(error: unknown): boolean {\n return MigrationToolsError.is(error) && error.code === 'MIGRATION.UNKNOWN_REF';\n}\n\nasync function snapshotFilesExist(refsDir: string, name: string): Promise<boolean> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const paths = [snapshotJsonPath(refsDir, name), snapshotDtsPath(refsDir, name)];\n const checks = await Promise.allSettled(paths.map((filePath) => access(filePath)));\n return checks.some((result) => result.status === 'fulfilled');\n}\n\nexport async function deleteRefPaired(refsDir: string, name: string): Promise<void> {\n if (await snapshotFilesExist(refsDir, name)) {\n try {\n await deleteRef(refsDir, name);\n } catch (error) {\n if (!isUnknownRefError(error)) {\n throw error;\n }\n }\n await deleteRefSnapshot(refsDir, name);\n return;\n }\n\n await deleteRef(refsDir, name);\n await deleteRefSnapshot(refsDir, name);\n}\n"],"mappings":"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"}
@@ -0,0 +1,132 @@
1
+ //#region src/verify-contract-spaces.d.ts
2
+ /**
3
+ * List the per-space subdirectories under
4
+ * `<projectRoot>/migrations/`. Returns space-id directory names (sorted
5
+ * alphabetically) — i.e. any non-dot-prefixed subdirectory whose root
6
+ * does **not** contain a `migration.json` manifest. The manifest is the
7
+ * structural marker of a user-authored migration directory (see
8
+ * `readMigrationsDir` in `./io`); directory names themselves belong to
9
+ * the user and are not part of the contract.
10
+ *
11
+ * Returns `[]` if the migrations directory does not exist (greenfield
12
+ * project).
13
+ *
14
+ * Reads only the user's repo. **No descriptor import.** The caller
15
+ * (verifier) feeds the result into {@link verifyContractSpaces} alongside
16
+ * the loaded-space set and the marker rows.
17
+ */
18
+ declare function listContractSpaceDirectories(projectMigrationsDir: string): Promise<readonly string[]>;
19
+ /**
20
+ * On-disk head value (`(hash, invariants)`) for one contract space.
21
+ * The verifier compares this against the marker row for the same space
22
+ * to detect drift between the user-emitted artefacts and the live DB
23
+ * marker.
24
+ */
25
+ interface ContractSpaceHeadRecord {
26
+ readonly hash: string;
27
+ readonly invariants: readonly string[];
28
+ }
29
+ /**
30
+ * Marker row read from `prisma_contract.marker` (one per `space`).
31
+ * Caller resolves these via the family runtime's marker reader before
32
+ * invoking {@link verifyContractSpaces}.
33
+ */
34
+ interface SpaceMarkerRecord {
35
+ readonly hash: string;
36
+ readonly invariants: readonly string[];
37
+ }
38
+ interface VerifyContractSpacesInputs {
39
+ /**
40
+ * Set of contract spaces the project declares: `'app'` plus each
41
+ * extension space in `extensionPacks`. The caller's discovery path
42
+ * never reads the extension descriptor module — it walks the
43
+ * `extensionPacks` configuration in `prisma-next.config.ts` for the
44
+ * space ids.
45
+ */
46
+ readonly loadedSpaces: ReadonlySet<string>;
47
+ /**
48
+ * Per-space subdirectories observed under
49
+ * `<projectRoot>/migrations/`. Resolved via
50
+ * {@link listContractSpaceDirectories}.
51
+ */
52
+ readonly spaceDirsOnDisk: readonly string[];
53
+ /**
54
+ * Head ref per space, keyed by space id. Caller reads
55
+ * `<projectRoot>/migrations/<space-id>/contract.json` and
56
+ * `<projectRoot>/migrations/<space-id>/refs/head.json` to construct
57
+ * this map. Spaces with no contract-space dir on disk simply omit a
58
+ * map entry.
59
+ */
60
+ readonly headRefsBySpace: ReadonlyMap<string, ContractSpaceHeadRecord>;
61
+ /**
62
+ * Marker rows keyed by `space`. Caller reads them from the
63
+ * `prisma_contract.marker` table.
64
+ */
65
+ readonly markerRowsBySpace: ReadonlyMap<string, SpaceMarkerRecord>;
66
+ }
67
+ type SpaceVerifierViolation = {
68
+ readonly kind: 'declaredButUnmigrated';
69
+ readonly spaceId: string;
70
+ readonly remediation: string;
71
+ } | {
72
+ readonly kind: 'orphanMarker';
73
+ readonly spaceId: string;
74
+ readonly remediation: string;
75
+ } | {
76
+ readonly kind: 'orphanSpaceDir';
77
+ readonly spaceId: string;
78
+ readonly remediation: string;
79
+ } | {
80
+ readonly kind: 'hashMismatch';
81
+ readonly spaceId: string;
82
+ readonly priorHeadHash: string;
83
+ readonly markerHash: string;
84
+ readonly remediation: string;
85
+ } | {
86
+ readonly kind: 'invariantsMismatch';
87
+ readonly spaceId: string;
88
+ readonly onDiskInvariants: readonly string[];
89
+ readonly markerInvariants: readonly string[];
90
+ readonly remediation: string;
91
+ };
92
+ type VerifyContractSpacesResult = {
93
+ readonly ok: true;
94
+ } | {
95
+ readonly ok: false;
96
+ readonly violations: readonly SpaceVerifierViolation[];
97
+ };
98
+ /**
99
+ * Pure structural verifier for the per-space mechanism. Aggregates the
100
+ * three orphan / missing checks plus per-space hash and invariant
101
+ * comparison.
102
+ *
103
+ * Algorithm:
104
+ *
105
+ * - For every extension space declared in `loadedSpaces` (`'app'`
106
+ * excluded — the per-space verifier is scoped to extension members;
107
+ * the app is verified through the aggregate path):
108
+ * - If no contract-space dir on disk → `declaredButUnmigrated`.
109
+ * - Else if `markerRowsBySpace` lacks an entry → no violation here;
110
+ * the live-DB compare done outside this helper is where the
111
+ * absence shows up.
112
+ * - Else compare marker hash / invariants vs. on-disk head hash /
113
+ * invariants → `hashMismatch` / `invariantsMismatch` on drift.
114
+ * - For every contract-space dir on disk that is not in `loadedSpaces` →
115
+ * `orphanSpaceDir`.
116
+ * - For every marker row whose `space` is not in `loadedSpaces` →
117
+ * `orphanMarker`. The app-space marker is always loaded (`'app'` is
118
+ * in `loadedSpaces` by definition).
119
+ *
120
+ * Output is deterministic: violations are sorted first by `kind`
121
+ * (`declaredButUnmigrated` → `orphanMarker` → `orphanSpaceDir` →
122
+ * `hashMismatch` → `invariantsMismatch`) then by `spaceId`. Two callers
123
+ * passing equivalent inputs see byte-identical violation lists.
124
+ *
125
+ * Synchronous, pure, no I/O. **Does not import the extension descriptor**
126
+ * (the inputs are pre-resolved by the caller); the verifier reads only
127
+ * the user repo, not `node_modules`.
128
+ */
129
+ declare function verifyContractSpaces(inputs: VerifyContractSpacesInputs): VerifyContractSpacesResult;
130
+ //#endregion
131
+ export { VerifyContractSpacesResult as a, VerifyContractSpacesInputs as i, SpaceMarkerRecord as n, listContractSpaceDirectories as o, SpaceVerifierViolation as r, verifyContractSpaces as s, ContractSpaceHeadRecord as t };
132
+ //# sourceMappingURL=verify-contract-spaces-BdysZdQk.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-contract-spaces-BdysZdQk.d.mts","names":[],"sources":["../src/verify-contract-spaces.ts"],"mappings":";;AAyBA;;;;AAEU;AAyCV;;;;AAEqB;AAQrB;;;;AAEqB;iBAvDC,4BAAA,CACpB,oBAAA,WACC,OAAO;;;;;;;UAyCO,uBAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;;;;;;UAQJ,iBAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;AAAA,UAGJ,0BAAA;EAiCL;;;;;;;EAAA,SAzBD,YAAA,EAAc,WAAA;EAiCV;;;;;EAAA,SA1BJ,eAAA;EAoCI;;;;;;;EAAA,SA3BJ,eAAA,EAAiB,WAAA,SAAoB,uBAAA;EAqCjC;;AAAW;AAG1B;EAHe,SA/BJ,iBAAA,EAAmB,WAAA,SAAoB,iBAAA;AAAA;AAAA,KAGtC,sBAAA;EAAA,SAEG,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,aAAA;EAAA,SACA,UAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,gBAAA;EAAA,SACA,gBAAA;EAAA,SACA,WAAA;AAAA;AAAA,KAGH,0BAAA;EAAA,SACG,EAAA;AAAA;EAAA,SACA,EAAA;EAAA,SAAoB,UAAA,WAAqB,sBAAsB;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiC9D,oBAAA,CACd,MAAA,EAAQ,0BAAA,GACP,0BAA0B"}
package/package.json CHANGED
@@ -1,32 +1,38 @@
1
1
  {
2
2
  "name": "@prisma-next/migration-tools",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "On-disk migration persistence, hash verification, and chain reconstruction for Prisma Next",
8
8
  "dependencies": {
9
- "@prisma-next/contract": "0.11.0",
10
- "@prisma-next/framework-components": "0.11.0",
11
- "@prisma-next/utils": "0.11.0",
9
+ "@prisma-next/contract": "0.12.0",
10
+ "@prisma-next/framework-components": "0.12.0",
11
+ "@prisma-next/utils": "0.12.0",
12
12
  "arktype": "^2.2.0",
13
13
  "pathe": "^2.0.3",
14
14
  "prettier": "^3.8.3"
15
15
  },
16
16
  "devDependencies": {
17
- "@prisma-next/tsconfig": "0.11.0",
18
- "@prisma-next/tsdown": "0.11.0",
17
+ "@prisma-next/test-utils": "0.12.0",
18
+ "@prisma-next/tsconfig": "0.12.0",
19
+ "@prisma-next/tsdown": "0.12.0",
19
20
  "tsdown": "0.22.0",
20
21
  "typescript": "5.9.3",
21
22
  "vitest": "4.1.6"
22
23
  },
24
+ "peerDependencies": {
25
+ "typescript": ">=5.9"
26
+ },
27
+ "peerDependenciesMeta": {
28
+ "typescript": {
29
+ "optional": true
30
+ }
31
+ },
23
32
  "files": [
24
33
  "dist",
25
34
  "src"
26
35
  ],
27
- "engines": {
28
- "node": ">=20"
29
- },
30
36
  "exports": {
31
37
  "./metadata": {
32
38
  "types": "./dist/exports/metadata.d.mts",
@@ -90,6 +96,9 @@
90
96
  },
91
97
  "./package.json": "./package.json"
92
98
  },
99
+ "engines": {
100
+ "node": ">=24"
101
+ },
93
102
  "repository": {
94
103
  "type": "git",
95
104
  "url": "https://github.com/prisma/prisma-next.git",
@@ -0,0 +1,266 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Contract } from '@prisma-next/contract/types';
3
+ import { join } from 'pathe';
4
+ import {
5
+ errorBundleNotFoundForGraphNode,
6
+ errorContractDeserializationFailed,
7
+ errorHashNotInGraph,
8
+ errorInvalidJson,
9
+ errorMissingFile,
10
+ errorSnapshotMissing,
11
+ MigrationToolsError,
12
+ } from '../errors';
13
+ import type { MigrationGraph } from '../graph';
14
+ import { isGraphNode } from '../graph-membership';
15
+ import type { IntegrityQueryOptions, IntegrityViolation } from '../integrity-violation';
16
+ import { reconstructGraph } from '../migration-graph';
17
+ import type { OnDiskMigrationPackage } from '../package';
18
+ import type { Refs } from '../refs';
19
+ import { readRefSnapshot } from '../refs/snapshot';
20
+ import type { ContractSpaceHeadRecord } from '../verify-contract-spaces';
21
+ import type {
22
+ ContractAtOptions,
23
+ ContractAtResult,
24
+ ContractSpaceAggregate,
25
+ ContractSpaceMember,
26
+ } from './types';
27
+
28
+ function hasErrnoCode(error: unknown, code: string): boolean {
29
+ return error instanceof Error && (error as { code?: string }).code === code;
30
+ }
31
+
32
+ function contractAtMemoKey(hash: string, refName: string | undefined): string {
33
+ return `${hash}\0${refName ?? ''}`;
34
+ }
35
+
36
+ function deserializeContractAtPath(
37
+ filePath: string,
38
+ contractJson: unknown,
39
+ deserializeContract: (raw: unknown) => Contract,
40
+ ): Contract {
41
+ try {
42
+ return deserializeContract(contractJson);
43
+ } catch (error) {
44
+ if (MigrationToolsError.is(error)) {
45
+ throw error;
46
+ }
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ throw errorContractDeserializationFailed(filePath, message);
49
+ }
50
+ }
51
+
52
+ async function readGraphNodeEndContract(
53
+ packageDir: string,
54
+ deserializeContract: (raw: unknown) => Contract,
55
+ ): Promise<{ contractJson: unknown; contractDts: string; contract: Contract }> {
56
+ const jsonPath = join(packageDir, 'end-contract.json');
57
+ const dtsPath = join(packageDir, 'end-contract.d.ts');
58
+
59
+ let rawJson: string;
60
+ try {
61
+ rawJson = await readFile(jsonPath, 'utf-8');
62
+ } catch (error) {
63
+ if (hasErrnoCode(error, 'ENOENT')) {
64
+ throw errorMissingFile('end-contract.json', packageDir);
65
+ }
66
+ throw error;
67
+ }
68
+
69
+ let contractJson: unknown;
70
+ try {
71
+ contractJson = JSON.parse(rawJson);
72
+ } catch (error) {
73
+ throw errorInvalidJson(jsonPath, error instanceof Error ? error.message : String(error));
74
+ }
75
+
76
+ let contractDts: string;
77
+ try {
78
+ contractDts = await readFile(dtsPath, 'utf-8');
79
+ } catch (error) {
80
+ if (hasErrnoCode(error, 'ENOENT')) {
81
+ throw errorMissingFile('end-contract.d.ts', packageDir);
82
+ }
83
+ throw error;
84
+ }
85
+
86
+ const contract = deserializeContractAtPath(jsonPath, contractJson, deserializeContract);
87
+ return { contractJson, contractDts, contract };
88
+ }
89
+
90
+ async function resolveContractAt(args: {
91
+ readonly hash: string;
92
+ readonly opts: ContractAtOptions | undefined;
93
+ readonly refsDir: string;
94
+ readonly packages: readonly OnDiskMigrationPackage[];
95
+ readonly graph: MigrationGraph;
96
+ readonly deserializeContract: (raw: unknown) => Contract;
97
+ }): Promise<ContractAtResult> {
98
+ const { hash, opts, refsDir, packages, graph, deserializeContract } = args;
99
+ const refName = opts?.refName;
100
+
101
+ if (refName !== undefined) {
102
+ const snapshot = await readRefSnapshot(refsDir, refName);
103
+ if (snapshot) {
104
+ const jsonPath = join(refsDir, `${refName}.contract.json`);
105
+ return {
106
+ hash,
107
+ contractJson: snapshot.contract,
108
+ contractDts: snapshot.contractDts,
109
+ contract: deserializeContractAtPath(jsonPath, snapshot.contract, deserializeContract),
110
+ provenance: 'snapshot',
111
+ };
112
+ }
113
+
114
+ if (isGraphNode(hash, graph)) {
115
+ return resolveGraphNodeContractAt({
116
+ hash,
117
+ packages,
118
+ deserializeContract,
119
+ explicitLabel: refName,
120
+ });
121
+ }
122
+
123
+ throw errorSnapshotMissing(refName);
124
+ }
125
+
126
+ if (isGraphNode(hash, graph)) {
127
+ return resolveGraphNodeContractAt({ hash, packages, deserializeContract });
128
+ }
129
+
130
+ throw errorHashNotInGraph(hash, graph);
131
+ }
132
+
133
+ async function resolveGraphNodeContractAt(args: {
134
+ readonly hash: string;
135
+ readonly packages: readonly OnDiskMigrationPackage[];
136
+ readonly deserializeContract: (raw: unknown) => Contract;
137
+ readonly explicitLabel?: string;
138
+ }): Promise<ContractAtResult> {
139
+ const { hash, packages, deserializeContract, explicitLabel } = args;
140
+ const matchingBundle = packages.find((pkg) => pkg.metadata.to === hash);
141
+ if (!matchingBundle) {
142
+ throw errorBundleNotFoundForGraphNode(hash, explicitLabel);
143
+ }
144
+
145
+ const { contractJson, contractDts, contract } = await readGraphNodeEndContract(
146
+ matchingBundle.dirPath,
147
+ deserializeContract,
148
+ );
149
+ return {
150
+ hash,
151
+ contractJson,
152
+ contractDts,
153
+ contract,
154
+ provenance: 'graph-node',
155
+ sourceDir: matchingBundle.dirPath,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Resolve a member's head ref, asserting it is present. The apply/verify
161
+ * engine only runs after `checkIntegrity` has refused on `headRefMissing`,
162
+ * so a member reaching the planner / verifier without a head ref is a
163
+ * programming error (the integrity gate was skipped), not a user-facing
164
+ * state. The app member's head ref is always synthesised, so this only
165
+ * ever guards an ungated extension space.
166
+ */
167
+ export function requireHeadRef(member: ContractSpaceMember): ContractSpaceHeadRecord {
168
+ if (member.headRef === null) {
169
+ throw new Error(
170
+ `Contract space "${member.spaceId}" has no head ref; the integrity gate must refuse a missing head ref before planning or verifying.`,
171
+ );
172
+ }
173
+ return member.headRef;
174
+ }
175
+
176
+ /**
177
+ * Build a {@link ContractSpaceMember} with lazily-memoised `graph()`,
178
+ * `contract()`, and `contractAt()` facets.
179
+ *
180
+ * `graph()` reconstructs the migration graph from `packages` on first
181
+ * call and caches it. `contract()` calls `resolveContract` on first call
182
+ * and caches the result; a throwing `resolveContract` (e.g. a missing or
183
+ * undeserializable on-disk contract) re-throws on each call rather than
184
+ * caching a value — `checkIntegrity` surfaces that as `contractUnreadable`.
185
+ * `contractAt()` materializes the contract at an arbitrary graph node with
186
+ * the same resolution order as plan-time ref resolution: ref snapshot first
187
+ * (when `opts.refName` is set), else the matching package's `end-contract.*`.
188
+ */
189
+ export function createContractSpaceMember(args: {
190
+ readonly spaceId: string;
191
+ readonly packages: readonly OnDiskMigrationPackage[];
192
+ readonly refs: Refs;
193
+ readonly headRef: ContractSpaceHeadRecord | null;
194
+ readonly refsDir: string;
195
+ readonly resolveContract: () => Contract;
196
+ readonly deserializeContract: (raw: unknown) => Contract;
197
+ }): ContractSpaceMember {
198
+ const { spaceId, packages, refs, headRef, refsDir, resolveContract, deserializeContract } = args;
199
+ let graphMemo: MigrationGraph | undefined;
200
+ let contractMemo: Contract | undefined;
201
+ const contractAtMemo = new Map<string, ContractAtResult>();
202
+
203
+ function memberGraph(): MigrationGraph {
204
+ graphMemo ??= reconstructGraph(packages);
205
+ return graphMemo;
206
+ }
207
+
208
+ return {
209
+ spaceId,
210
+ packages,
211
+ refs,
212
+ headRef,
213
+ graph: memberGraph,
214
+ contract() {
215
+ contractMemo ??= resolveContract();
216
+ return contractMemo;
217
+ },
218
+ async contractAt(hash, opts) {
219
+ const key = contractAtMemoKey(hash, opts?.refName);
220
+ const cached = contractAtMemo.get(key);
221
+ if (cached) {
222
+ return cached;
223
+ }
224
+
225
+ const result = await resolveContractAt({
226
+ hash,
227
+ opts,
228
+ refsDir,
229
+ packages,
230
+ graph: memberGraph(),
231
+ deserializeContract,
232
+ });
233
+ contractAtMemo.set(key, result);
234
+ return result;
235
+ },
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Assemble a {@link ContractSpaceAggregate} value from its members and a
241
+ * `checkIntegrity` implementation. The query methods (`listSpaces` /
242
+ * `hasSpace` / `space` / `spaces`) are derived here so every aggregate —
243
+ * loader-built or test-built — shares one query surface: `app` first,
244
+ * then `extensions` in the order supplied (the loader sorts them
245
+ * lex-ascending by `spaceId`).
246
+ */
247
+ export function createContractSpaceAggregate(args: {
248
+ readonly targetId: string;
249
+ readonly app: ContractSpaceMember;
250
+ readonly extensions: readonly ContractSpaceMember[];
251
+ readonly checkIntegrity: (opts?: IntegrityQueryOptions) => readonly IntegrityViolation[];
252
+ }): ContractSpaceAggregate {
253
+ const { targetId, app, extensions, checkIntegrity } = args;
254
+ const ordered: readonly ContractSpaceMember[] = [app, ...extensions];
255
+ const byId = new Map(ordered.map((m) => [m.spaceId, m]));
256
+ return {
257
+ targetId,
258
+ app,
259
+ extensions,
260
+ listSpaces: () => ordered.map((m) => m.spaceId),
261
+ hasSpace: (id) => byId.has(id),
262
+ space: (id) => byId.get(id),
263
+ spaces: () => ordered,
264
+ checkIntegrity,
265
+ };
266
+ }