@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,243 @@
1
+ import { elementCoordinates } from '@prisma-next/framework-components/ir';
2
+ import { EMPTY_CONTRACT_HASH } from '../constants';
3
+ import { MigrationToolsError } from '../errors';
4
+ import type {
5
+ DeclaredExtensionEntry,
6
+ IntegrityQueryOptions,
7
+ IntegrityViolation,
8
+ } from '../integrity-violation';
9
+ import type { PackageLoadProblem } from '../io';
10
+ import type { OnDiskMigrationPackage } from '../package';
11
+ import type { RefLoadProblem } from '../refs';
12
+ import type { ContractSpaceMember } from './types';
13
+
14
+ /**
15
+ * One space's load-time facts that `checkIntegrity` judges: the loaded
16
+ * member, the load-time problems `readMigrationsDir` surfaced for it, and
17
+ * whether it is the app space (the app head ref is synthesised, so the
18
+ * head-ref checks are skipped for it).
19
+ */
20
+ export interface IntegritySpaceState {
21
+ readonly member: ContractSpaceMember;
22
+ readonly problems: readonly PackageLoadProblem[];
23
+ /** Per-ref problems: a user ref `*.json` that exists but is unparseable. */
24
+ readonly refProblems: readonly RefLoadProblem[];
25
+ /**
26
+ * The space's `refs/head.json` problem when it exists but is unparseable.
27
+ * `null` means the head ref was read cleanly or is genuinely absent —
28
+ * the absent case is judged `headRefMissing`, the corrupt case here is
29
+ * judged `refUnreadable` (and suppresses `headRefMissing`).
30
+ */
31
+ readonly headRefProblem: RefLoadProblem | null;
32
+ readonly isApp: boolean;
33
+ }
34
+
35
+ export interface IntegrityComputationInput {
36
+ readonly targetId: string;
37
+ readonly spaces: readonly IntegritySpaceState[];
38
+ }
39
+
40
+ /**
41
+ * Walk the loaded model and return **every** integrity violation — never
42
+ * bailing at the first. Structurally-derivable violations (load-time
43
+ * problems, self-edges, missing / unreachable head refs) are always
44
+ * produced; layout-drift checks require `declaredExtensions`, and
45
+ * contract / target / disjointness checks require `checkContracts`.
46
+ */
47
+ export function computeIntegrityViolations(
48
+ input: IntegrityComputationInput,
49
+ opts?: IntegrityQueryOptions,
50
+ ): readonly IntegrityViolation[] {
51
+ const violations: IntegrityViolation[] = [];
52
+
53
+ for (const { member, problems, refProblems, headRefProblem, isApp } of input.spaces) {
54
+ const { spaceId } = member;
55
+
56
+ for (const problem of problems) {
57
+ violations.push(loadProblemToViolation(spaceId, problem));
58
+ }
59
+
60
+ for (const refProblem of refProblems) {
61
+ violations.push({
62
+ kind: 'refUnreadable',
63
+ spaceId,
64
+ refName: refProblem.refName,
65
+ detail: refProblem.detail,
66
+ });
67
+ }
68
+ if (headRefProblem !== null) {
69
+ violations.push({
70
+ kind: 'refUnreadable',
71
+ spaceId,
72
+ refName: headRefProblem.refName,
73
+ detail: headRefProblem.detail,
74
+ });
75
+ }
76
+
77
+ for (const pkg of member.packages) {
78
+ const from = pkg.metadata.from ?? EMPTY_CONTRACT_HASH;
79
+ const isSelfEdge = from === pkg.metadata.to;
80
+ const hasDataOp = pkg.ops.some((op) => op.operationClass === 'data');
81
+ if (isSelfEdge && !hasDataOp) {
82
+ violations.push({ kind: 'sameSourceAndTarget', spaceId, dirName: pkg.dirName, hash: from });
83
+ }
84
+ }
85
+
86
+ violations.push(...duplicateMigrationHashViolations(spaceId, member.packages));
87
+
88
+ // The app head ref is synthesised from the live contract, so it is
89
+ // always present and reachable; only extension spaces read their head
90
+ // ref from disk and can be missing or point outside the graph. A head
91
+ // ref that exists but is unparseable is already surfaced above as
92
+ // `refUnreadable`, so it is not also reported as `headRefMissing`.
93
+ if (!isApp && headRefProblem === null) {
94
+ if (member.headRef === null) {
95
+ violations.push({ kind: 'headRefMissing', spaceId });
96
+ } else if (!headRefPresentInGraph(member, member.headRef.hash)) {
97
+ violations.push({ kind: 'headRefNotInGraph', spaceId, hash: member.headRef.hash });
98
+ }
99
+ }
100
+ }
101
+
102
+ if (opts?.declaredExtensions !== undefined) {
103
+ violations.push(...layoutViolations(input.spaces, opts.declaredExtensions));
104
+ }
105
+
106
+ if (opts?.checkContracts === true) {
107
+ violations.push(...contractViolations(input));
108
+ }
109
+
110
+ return violations;
111
+ }
112
+
113
+ export function loadProblemToViolation(
114
+ spaceId: string,
115
+ problem: PackageLoadProblem,
116
+ ): IntegrityViolation {
117
+ switch (problem.kind) {
118
+ case 'hashMismatch':
119
+ return {
120
+ kind: 'hashMismatch',
121
+ spaceId,
122
+ dirName: problem.dirName,
123
+ stored: problem.stored,
124
+ computed: problem.computed,
125
+ };
126
+ case 'providedInvariantsMismatch':
127
+ return { kind: 'providedInvariantsMismatch', spaceId, dirName: problem.dirName };
128
+ case 'packageUnloadable':
129
+ return {
130
+ kind: 'packageUnloadable',
131
+ spaceId,
132
+ dirName: problem.dirName,
133
+ detail: problem.detail,
134
+ };
135
+ }
136
+ }
137
+
138
+ function duplicateMigrationHashViolations(
139
+ spaceId: string,
140
+ packages: readonly OnDiskMigrationPackage[],
141
+ ): readonly IntegrityViolation[] {
142
+ const dirNamesByHash = new Map<string, string[]>();
143
+ for (const pkg of packages) {
144
+ const hash = pkg.metadata.migrationHash;
145
+ const dirNames = dirNamesByHash.get(hash);
146
+ if (dirNames) dirNames.push(pkg.dirName);
147
+ else dirNamesByHash.set(hash, [pkg.dirName]);
148
+ }
149
+
150
+ const out: IntegrityViolation[] = [];
151
+ for (const [migrationHash, dirNames] of dirNamesByHash) {
152
+ if (dirNames.length > 1) {
153
+ out.push({
154
+ kind: 'duplicateMigrationHash',
155
+ spaceId,
156
+ migrationHash,
157
+ dirNames: [...dirNames].sort(),
158
+ });
159
+ }
160
+ }
161
+ return out;
162
+ }
163
+
164
+ /**
165
+ * Whether a space's head-ref hash is present in its reconstructed graph.
166
+ * An empty graph is reachable only by the empty-contract sentinel.
167
+ */
168
+ function headRefPresentInGraph(member: ContractSpaceMember, headHash: string): boolean {
169
+ const graph = member.graph();
170
+ if (graph.nodes.size === 0) {
171
+ return headHash === EMPTY_CONTRACT_HASH;
172
+ }
173
+ return graph.nodes.has(headHash);
174
+ }
175
+
176
+ function layoutViolations(
177
+ spaces: readonly IntegritySpaceState[],
178
+ declaredExtensions: readonly DeclaredExtensionEntry[],
179
+ ): readonly IntegrityViolation[] {
180
+ const out: IntegrityViolation[] = [];
181
+ const extensionSpaceIds = new Set(spaces.filter((s) => !s.isApp).map((s) => s.member.spaceId));
182
+ const declaredIds = new Set(declaredExtensions.map((d) => d.id));
183
+
184
+ for (const id of [...extensionSpaceIds].sort()) {
185
+ if (!declaredIds.has(id)) {
186
+ out.push({ kind: 'orphanSpaceDir', spaceId: id });
187
+ }
188
+ }
189
+ for (const id of [...declaredIds].sort()) {
190
+ if (!extensionSpaceIds.has(id)) {
191
+ out.push({ kind: 'declaredButUnmigrated', spaceId: id });
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+
197
+ function contractViolations(input: IntegrityComputationInput): readonly IntegrityViolation[] {
198
+ const out: IntegrityViolation[] = [];
199
+ const elementClaimedBy = new Map<string, string[]>();
200
+
201
+ for (const { member } of input.spaces) {
202
+ let contract: ReturnType<ContractSpaceMember['contract']>;
203
+ try {
204
+ contract = member.contract();
205
+ } catch (error) {
206
+ out.push({ kind: 'contractUnreadable', spaceId: member.spaceId, detail: detailOf(error) });
207
+ continue;
208
+ }
209
+
210
+ if (contract.target !== input.targetId) {
211
+ out.push({
212
+ kind: 'targetMismatch',
213
+ spaceId: member.spaceId,
214
+ expected: input.targetId,
215
+ actual: contract.target,
216
+ });
217
+ }
218
+
219
+ for (const { entityName: elementName } of elementCoordinates(contract.storage)) {
220
+ const claimers = elementClaimedBy.get(elementName);
221
+ if (claimers) claimers.push(member.spaceId);
222
+ else elementClaimedBy.set(elementName, [member.spaceId]);
223
+ }
224
+ }
225
+
226
+ const disjointness: IntegrityViolation[] = [];
227
+ for (const [element, claimedBy] of elementClaimedBy) {
228
+ if (claimedBy.length > 1) {
229
+ disjointness.push({ kind: 'disjointness', element, claimedBy: [...claimedBy].sort() });
230
+ }
231
+ }
232
+ disjointness.sort((a, b) =>
233
+ a.kind === 'disjointness' && b.kind === 'disjointness' ? a.element.localeCompare(b.element) : 0,
234
+ );
235
+ out.push(...disjointness);
236
+ return out;
237
+ }
238
+
239
+ function detailOf(error: unknown): string {
240
+ if (MigrationToolsError.is(error)) return error.why;
241
+ if (error instanceof Error) return error.message;
242
+ return String(error);
243
+ }