@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.
- package/README.md +4 -4
- package/dist/{errors-DGYwcwXs.mjs → errors-vFROOhCR.mjs} +46 -21
- package/dist/errors-vFROOhCR.mjs.map +1 -0
- package/dist/exports/aggregate.d.mts +328 -204
- package/dist/exports/aggregate.d.mts.map +1 -1
- package/dist/exports/aggregate.mjs +480 -243
- package/dist/exports/aggregate.mjs.map +1 -1
- package/dist/exports/errors.d.mts +2 -2
- package/dist/exports/errors.d.mts.map +1 -1
- package/dist/exports/errors.mjs +1 -1
- package/dist/exports/graph.d.mts +1 -1
- package/dist/exports/hash.d.mts +8 -9
- package/dist/exports/hash.d.mts.map +1 -1
- package/dist/exports/hash.mjs +1 -1
- package/dist/exports/invariants.d.mts +1 -1
- package/dist/exports/invariants.d.mts.map +1 -1
- package/dist/exports/invariants.mjs +1 -1
- package/dist/exports/io.d.mts +2 -83
- package/dist/exports/io.mjs +1 -1
- package/dist/exports/metadata.d.mts +2 -2
- package/dist/exports/migration-graph.d.mts +9 -2
- package/dist/exports/migration-graph.d.mts.map +1 -0
- package/dist/exports/migration-graph.mjs +3 -2
- package/dist/exports/migration-ts.d.mts.map +1 -1
- package/dist/exports/migration-ts.mjs.map +1 -1
- package/dist/exports/migration.d.mts +5 -6
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +14 -32
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/package.d.mts +1 -1
- package/dist/exports/ref-resolution.d.mts +2 -2
- package/dist/exports/ref-resolution.d.mts.map +1 -1
- package/dist/exports/ref-resolution.mjs +1 -1
- package/dist/exports/ref-resolution.mjs.map +1 -1
- package/dist/exports/refs.d.mts +15 -2
- package/dist/exports/refs.d.mts.map +1 -0
- package/dist/exports/refs.mjs +3 -2
- package/dist/exports/spaces.d.mts +31 -132
- package/dist/exports/spaces.d.mts.map +1 -1
- package/dist/exports/spaces.mjs +13 -9
- package/dist/exports/spaces.mjs.map +1 -1
- package/dist/{graph-BrLXqoUc.d.mts → graph-3dLMZp5l.d.mts} +1 -2
- package/dist/graph-3dLMZp5l.d.mts.map +1 -0
- package/dist/graph-membership-BV23F1IV.mjs +15 -0
- package/dist/graph-membership-BV23F1IV.mjs.map +1 -0
- package/dist/{hash-Cr4WIr4Z.mjs → hash--Y7vCpN3.mjs} +8 -9
- package/dist/hash--Y7vCpN3.mjs.map +1 -0
- package/dist/{invariants-0daYEzyo.mjs → invariants-C23nXy1c.mjs} +2 -2
- package/dist/{invariants-0daYEzyo.mjs.map → invariants-C23nXy1c.mjs.map} +1 -1
- package/dist/{io-BPLfzvZe.mjs → io-BGlPOt9b.mjs} +100 -13
- package/dist/io-BGlPOt9b.mjs.map +1 -0
- package/dist/io-BH4G3F-i.d.mts +124 -0
- package/dist/io-BH4G3F-i.d.mts.map +1 -0
- package/dist/metadata-Bp9X04gM.d.mts +2 -0
- package/dist/{migration-graph-nlS4TRpn.mjs → migration-graph-BMAqSfv9.mjs} +6 -26
- package/dist/migration-graph-BMAqSfv9.mjs.map +1 -0
- package/dist/{migration-graph-De0dUZoC.d.mts → migration-graph-CWEM2SLR.d.mts} +6 -6
- package/dist/migration-graph-CWEM2SLR.d.mts.map +1 -0
- package/dist/op-schema-D5qkXfEf.mjs.map +1 -1
- package/dist/{package-DZj8YvD0.d.mts → package-Ca-J_z_0.d.mts} +1 -1
- package/dist/package-Ca-J_z_0.d.mts.map +1 -0
- package/dist/{read-contract-space-contract-DRueB4Aa.mjs → read-contract-space-contract-TbeXuJXL.mjs} +32 -5
- package/dist/read-contract-space-contract-TbeXuJXL.mjs.map +1 -0
- package/dist/{refs-BDHo5l_g.mjs → refs-C-_WUrPw.mjs} +97 -4
- package/dist/refs-C-_WUrPw.mjs.map +1 -0
- package/dist/refs-C7wuYFqZ.d.mts +42 -0
- package/dist/refs-C7wuYFqZ.d.mts.map +1 -0
- package/dist/snapshot-Bazwo13S.mjs +137 -0
- package/dist/snapshot-Bazwo13S.mjs.map +1 -0
- package/dist/verify-contract-spaces-BdysZdQk.d.mts +132 -0
- package/dist/verify-contract-spaces-BdysZdQk.d.mts.map +1 -0
- package/package.json +18 -9
- package/src/aggregate/aggregate.ts +266 -0
- package/src/aggregate/check-integrity.ts +243 -0
- package/src/aggregate/loader.ts +161 -334
- package/src/aggregate/planner-types.ts +14 -14
- package/src/aggregate/planner.ts +20 -23
- package/src/aggregate/project-schema-to-space.ts +3 -8
- package/src/aggregate/strategies/graph-walk.ts +15 -10
- package/src/aggregate/strategies/synth.ts +4 -4
- package/src/aggregate/types.ts +81 -62
- package/src/aggregate/verifier.ts +23 -23
- package/src/assert-descriptor-self-consistency.ts +6 -0
- package/src/compute-extension-space-apply-path.ts +1 -1
- package/src/emit-contract-space-artefacts.ts +4 -3
- package/src/errors.ts +58 -2
- package/src/exports/aggregate.ts +29 -19
- package/src/exports/io.ts +2 -0
- package/src/exports/metadata.ts +1 -1
- package/src/exports/migration-graph.ts +1 -0
- package/src/exports/refs.ts +11 -0
- package/src/exports/spaces.ts +3 -0
- package/src/graph-membership.ts +17 -0
- package/src/graph.ts +0 -1
- package/src/hash.ts +7 -8
- package/src/integrity-violation.ts +114 -0
- package/src/io.ts +139 -14
- package/src/metadata.ts +1 -1
- package/src/migration-base.ts +10 -30
- package/src/migration-graph.ts +7 -35
- package/src/read-contract-space-head-ref.ts +5 -2
- package/src/refs/snapshot.ts +199 -0
- package/src/refs.ts +124 -1
- package/src/space-layout.ts +30 -0
- package/dist/errors-DGYwcwXs.mjs.map +0 -1
- package/dist/exports/io.d.mts.map +0 -1
- package/dist/graph-BrLXqoUc.d.mts.map +0 -1
- package/dist/hash-Cr4WIr4Z.mjs.map +0 -1
- package/dist/io-BPLfzvZe.mjs.map +0 -1
- package/dist/metadata-BFX0xdz8.d.mts +0 -2
- package/dist/migration-graph-De0dUZoC.d.mts.map +0 -1
- package/dist/migration-graph-nlS4TRpn.mjs.map +0 -1
- package/dist/package-DZj8YvD0.d.mts.map +0 -1
- package/dist/read-contract-space-contract-DRueB4Aa.mjs.map +0 -1
- package/dist/refs-BDHo5l_g.mjs.map +0 -1
- package/dist/refs-CDaNerhT.d.mts +0 -16
- package/dist/refs-CDaNerhT.d.mts.map +0 -1
- 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.
|
|
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.
|
|
10
|
-
"@prisma-next/framework-components": "0.
|
|
11
|
-
"@prisma-next/utils": "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/
|
|
18
|
-
"@prisma-next/
|
|
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
|
+
}
|