@prisma-next/contract 0.8.0 → 0.9.0-dev.1
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/dist/contract-validation-error-Dp2vHZt5.mjs +14 -0
- package/dist/contract-validation-error-Dp2vHZt5.mjs.map +1 -0
- package/dist/contract-validation-error.d.mts +10 -0
- package/dist/contract-validation-error.d.mts.map +1 -0
- package/dist/contract-validation-error.mjs +2 -0
- package/dist/{hashing-Dz-16T7H.mjs → hashing-JryrY8ZF.mjs} +23 -14
- package/dist/hashing-JryrY8ZF.mjs.map +1 -0
- package/dist/hashing.d.mts +32 -7
- package/dist/hashing.d.mts.map +1 -1
- package/dist/hashing.mjs +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/validate-domain.mjs +96 -1
- package/dist/validate-domain.mjs.map +1 -0
- package/package.json +6 -6
- package/src/canonicalization.ts +46 -14
- package/src/contract-validation-error.ts +12 -0
- package/src/exports/contract-validation-error.ts +4 -0
- package/src/exports/hashing.ts +6 -1
- package/src/hashing.ts +12 -2
- package/src/validate-domain.ts +1 -1
- package/dist/hashing-Dz-16T7H.mjs.map +0 -1
- package/dist/validate-contract.d.mts +0 -37
- package/dist/validate-contract.d.mts.map +0 -1
- package/dist/validate-contract.mjs +0 -2
- package/dist/validate-domain-WtPdBLia.mjs +0 -163
- package/dist/validate-domain-WtPdBLia.mjs.map +0 -1
- package/src/exports/validate-contract.ts +0 -6
- package/src/validate-contract.ts +0 -101
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/contract-validation-error.ts
|
|
2
|
+
var ContractValidationError = class extends Error {
|
|
3
|
+
code = "CONTRACT.VALIDATION_FAILED";
|
|
4
|
+
phase;
|
|
5
|
+
constructor(message, phase) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ContractValidationError";
|
|
8
|
+
this.phase = phase;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { ContractValidationError as t };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=contract-validation-error-Dp2vHZt5.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-validation-error-Dp2vHZt5.mjs","names":[],"sources":["../src/contract-validation-error.ts"],"sourcesContent":["export type ContractValidationPhase = 'structural' | 'domain' | 'storage';\n\nexport class ContractValidationError extends Error {\n readonly code = 'CONTRACT.VALIDATION_FAILED';\n readonly phase: ContractValidationPhase;\n\n constructor(message: string, phase: ContractValidationPhase) {\n super(message);\n this.name = 'ContractValidationError';\n this.phase = phase;\n }\n}\n"],"mappings":";AAEA,IAAa,0BAAb,cAA6C,MAAM;CACjD,OAAgB;CAChB;CAEA,YAAY,SAAiB,OAAgC;EAC3D,MAAM,QAAQ;EACd,KAAK,OAAO;EACZ,KAAK,QAAQ"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/contract-validation-error.d.ts
|
|
2
|
+
type ContractValidationPhase = 'structural' | 'domain' | 'storage';
|
|
3
|
+
declare class ContractValidationError extends Error {
|
|
4
|
+
readonly code = "CONTRACT.VALIDATION_FAILED";
|
|
5
|
+
readonly phase: ContractValidationPhase;
|
|
6
|
+
constructor(message: string, phase: ContractValidationPhase);
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { ContractValidationError, type ContractValidationPhase };
|
|
10
|
+
//# sourceMappingURL=contract-validation-error.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-validation-error.d.mts","names":[],"sources":["../src/contract-validation-error.ts"],"mappings":";KAAY,uBAAA;AAAA,cAEC,uBAAA,SAAgC,KAAA;EAAA,SAClC,IAAA;EAAA,SACA,KAAA,EAAO,uBAAA;cAEJ,OAAA,UAAiB,KAAA,EAAO,uBAAA;AAAA"}
|
|
@@ -133,20 +133,26 @@ function orderTopLevel(obj) {
|
|
|
133
133
|
for (const key of Array.from(remaining).sort()) ordered[key] = obj[key];
|
|
134
134
|
return ordered;
|
|
135
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Object-form variant of {@link canonicalizeContract}. Exported because the
|
|
138
|
+
* emitter writes the canonical contract through a separate JSON-stringify
|
|
139
|
+
* pass and consumes the structured object directly.
|
|
140
|
+
*/
|
|
136
141
|
function canonicalizeContractToObject(contract, options) {
|
|
142
|
+
const serialized = options.serializeContract(contract);
|
|
137
143
|
const withDefaultsOmitted = omitDefaults({
|
|
138
|
-
...ifDefined("schemaVersion", options
|
|
139
|
-
targetFamily:
|
|
140
|
-
target:
|
|
141
|
-
profileHash:
|
|
142
|
-
roots:
|
|
143
|
-
models:
|
|
144
|
-
...ifDefined("valueObjects",
|
|
145
|
-
storage:
|
|
146
|
-
...ifDefined("execution",
|
|
147
|
-
extensionPacks:
|
|
148
|
-
capabilities:
|
|
149
|
-
meta:
|
|
144
|
+
...ifDefined("schemaVersion", options.schemaVersion),
|
|
145
|
+
targetFamily: serialized["targetFamily"],
|
|
146
|
+
target: serialized["target"],
|
|
147
|
+
profileHash: serialized["profileHash"],
|
|
148
|
+
roots: serialized["roots"],
|
|
149
|
+
models: serialized["models"],
|
|
150
|
+
...ifDefined("valueObjects", serialized["valueObjects"]),
|
|
151
|
+
storage: serialized["storage"],
|
|
152
|
+
...ifDefined("execution", serialized["execution"]),
|
|
153
|
+
extensionPacks: serialized["extensionPacks"],
|
|
154
|
+
capabilities: serialized["capabilities"],
|
|
155
|
+
meta: serialized["meta"]
|
|
150
156
|
}, []);
|
|
151
157
|
const withSortedIndexes = sortIndexesAndUniques(withDefaultsOmitted["storage"]);
|
|
152
158
|
return orderTopLevel(sortObjectKeys({
|
|
@@ -178,7 +184,10 @@ function hashContract(section) {
|
|
|
178
184
|
meta: {},
|
|
179
185
|
profileHash: "",
|
|
180
186
|
...section
|
|
181
|
-
}, {
|
|
187
|
+
}, {
|
|
188
|
+
schemaVersion: SCHEMA_VERSION,
|
|
189
|
+
serializeContract: (c) => JSON.parse(JSON.stringify(c))
|
|
190
|
+
});
|
|
182
191
|
}
|
|
183
192
|
function computeStorageHash(args) {
|
|
184
193
|
return sha256(hashContract(args));
|
|
@@ -192,4 +201,4 @@ function computeProfileHash(args) {
|
|
|
192
201
|
//#endregion
|
|
193
202
|
export { canonicalizeContractToObject as a, canonicalizeContract as i, computeProfileHash as n, computeStorageHash as r, computeExecutionHash as t };
|
|
194
203
|
|
|
195
|
-
//# sourceMappingURL=hashing-
|
|
204
|
+
//# sourceMappingURL=hashing-JryrY8ZF.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hashing-JryrY8ZF.mjs","names":[],"sources":["../src/canonicalization.ts","../src/hashing.ts"],"sourcesContent":["import { isArrayEqual } from '@prisma-next/utils/array-equal';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport type { JsonObject } from '@prisma-next/utils/json';\n\nimport type { Contract } from './contract-types';\n\n/**\n * Per-target contract serializer hook. The framework canonicalizer uses\n * this to convert an in-memory contract (which may carry class-instance\n * IR nodes whose runtime-only fields must not appear in the on-disk\n * envelope) into a plain JsonObject before applying the family-agnostic\n * canonical-key ordering / default-omission / sort steps. Targets whose\n * contract is JSON-clean by construction return the contract unchanged.\n */\nexport type SerializeContract = (contract: Contract) => JsonObject;\n\nconst TOP_LEVEL_ORDER = [\n 'schemaVersion',\n 'canonicalVersion',\n 'targetFamily',\n 'target',\n 'profileHash',\n 'roots',\n 'models',\n 'valueObjects',\n 'storage',\n 'execution',\n 'capabilities',\n 'extensionPacks',\n 'meta',\n] as const;\n\nfunction isDefaultValue(value: unknown): boolean {\n if (value === false) return true;\n if (value === null) return false;\n if (Array.isArray(value) && value.length === 0) return true;\n if (typeof value === 'object' && value !== null) {\n const keys = Object.keys(value);\n return keys.length === 0;\n }\n return false;\n}\n\nfunction omitDefaults(obj: unknown, path: readonly string[]): unknown {\n if (obj === null || typeof obj !== 'object') {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => omitDefaults(item, path));\n }\n\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(obj)) {\n const currentPath = [...path, key];\n\n if (key === '_generated') {\n continue;\n }\n\n if (key === 'generated' && value === false) {\n continue;\n }\n\n if ((key === 'onDelete' || key === 'onUpdate') && value === 'noAction') {\n continue;\n }\n\n if (isDefaultValue(value)) {\n const isRequiredModels = isArrayEqual(currentPath, ['models']);\n const isRequiredTables = isArrayEqual(currentPath, ['storage', 'tables']);\n const isRequiredCollections = isArrayEqual(currentPath, ['storage', 'collections']);\n const isCollectionEntry =\n currentPath.length === 3 &&\n isArrayEqual([currentPath[0], currentPath[1]], ['storage', 'collections']);\n const isRequiredRoots = isArrayEqual(currentPath, ['roots']);\n const isRequiredExtensionPacks = isArrayEqual(currentPath, ['extensionPacks']);\n const isRequiredCapabilities = isArrayEqual(currentPath, ['capabilities']);\n const isRequiredMeta = isArrayEqual(currentPath, ['meta']);\n const isRequiredExecutionDefaults = isArrayEqual(currentPath, [\n 'execution',\n 'mutations',\n 'defaults',\n ]);\n const isExtensionNamespace = currentPath.length === 2 && currentPath[0] === 'extensionPacks';\n const isModelRelations =\n currentPath.length === 3 &&\n isArrayEqual([currentPath[0], currentPath[2]], ['models', 'relations']);\n const isModelStorage =\n currentPath.length === 3 &&\n isArrayEqual([currentPath[0], currentPath[2]], ['models', 'storage']);\n const isTableUniques =\n currentPath.length === 4 &&\n isArrayEqual(\n [currentPath[0], currentPath[1], currentPath[3]],\n ['storage', 'tables', 'uniques'],\n );\n const isTableIndexes =\n currentPath.length === 4 &&\n isArrayEqual(\n [currentPath[0], currentPath[1], currentPath[3]],\n ['storage', 'tables', 'indexes'],\n );\n const isTableForeignKeys =\n currentPath.length === 4 &&\n isArrayEqual(\n [currentPath[0], currentPath[1], currentPath[3]],\n ['storage', 'tables', 'foreignKeys'],\n );\n\n // `storage.types.<name>.typeParams` is part of the StorageTypeInstance\n // shape (validators require it). Preserve it even when empty so the\n // emitted contract.json remains structurally valid after a round-trip.\n const isStorageTypeTypeParams =\n currentPath.length === 4 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'types' &&\n key === 'typeParams';\n\n const isFkBooleanField =\n currentPath.length === 5 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'tables' &&\n currentPath[3] === 'foreignKeys' &&\n (key === 'constraint' || key === 'index');\n\n const isNullableField = key === 'nullable';\n\n if (\n !isRequiredModels &&\n !isRequiredTables &&\n !isRequiredCollections &&\n !isCollectionEntry &&\n !isRequiredRoots &&\n !isRequiredExtensionPacks &&\n !isRequiredCapabilities &&\n !isRequiredMeta &&\n !isRequiredExecutionDefaults &&\n !isExtensionNamespace &&\n !isModelRelations &&\n !isModelStorage &&\n !isTableUniques &&\n !isTableIndexes &&\n !isTableForeignKeys &&\n !isFkBooleanField &&\n !isNullableField &&\n !isStorageTypeTypeParams\n ) {\n continue;\n }\n }\n\n result[key] = omitDefaults(value, currentPath);\n }\n\n return result;\n}\n\nfunction sortObjectKeys(obj: unknown): unknown {\n if (obj === null || typeof obj !== 'object') {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => sortObjectKeys(item));\n }\n\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(obj).sort();\n for (const key of keys) {\n sorted[key] = sortObjectKeys((obj as Record<string, unknown>)[key]);\n }\n\n return sorted;\n}\n\ntype StorageObject = {\n tables?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\ntype TableObject = {\n indexes?: unknown[];\n uniques?: unknown[];\n [key: string]: unknown;\n};\n\nfunction sortIndexesAndUniques(storage: unknown): unknown {\n if (!storage || typeof storage !== 'object') {\n return storage;\n }\n\n const storageObj = storage as StorageObject;\n if (!storageObj.tables || typeof storageObj.tables !== 'object') {\n return storage;\n }\n\n const tables = storageObj.tables;\n const result: StorageObject = { ...storageObj };\n\n result.tables = {};\n const sortedTableNames = Object.keys(tables).sort();\n for (const tableName of sortedTableNames) {\n const table = tables[tableName];\n if (!table || typeof table !== 'object') {\n result.tables[tableName] = table;\n continue;\n }\n\n const tableObj = table as TableObject;\n const sortedTable: TableObject = { ...tableObj };\n\n if (Array.isArray(tableObj.indexes)) {\n sortedTable.indexes = [...tableObj.indexes].sort((a, b) => {\n const nameA = (a as { name?: string })?.name || '';\n const nameB = (b as { name?: string })?.name || '';\n return nameA.localeCompare(nameB);\n });\n }\n\n if (Array.isArray(tableObj.uniques)) {\n sortedTable.uniques = [...tableObj.uniques].sort((a, b) => {\n const nameA = (a as { name?: string })?.name || '';\n const nameB = (b as { name?: string })?.name || '';\n return nameA.localeCompare(nameB);\n });\n }\n\n result.tables[tableName] = sortedTable;\n }\n\n return result;\n}\n\nexport function orderTopLevel(obj: Record<string, unknown>): Record<string, unknown> {\n const ordered: Record<string, unknown> = {};\n const remaining = new Set(Object.keys(obj));\n\n for (const key of TOP_LEVEL_ORDER) {\n if (remaining.has(key)) {\n ordered[key] = obj[key];\n remaining.delete(key);\n }\n }\n\n for (const key of Array.from(remaining).sort()) {\n ordered[key] = obj[key];\n }\n\n return ordered;\n}\n\nexport interface CanonicalizeContractOptions {\n readonly schemaVersion?: string;\n /**\n * Per-target hook that converts the in-memory contract (which may\n * carry class-instance IR nodes) into a plain JsonObject before the\n * family-agnostic canonicalization steps run.\n *\n * Routing through the hook is what lets each target decide which\n * fields appear in the on-disk envelope; runtime-only class API\n * fields stay invisible to the canonicalization walk by virtue of\n * the per-target serializer not putting them in the JSON shape.\n */\n readonly serializeContract: SerializeContract;\n}\n\n/**\n * Object-form variant of {@link canonicalizeContract}. Exported because the\n * emitter writes the canonical contract through a separate JSON-stringify\n * pass and consumes the structured object directly.\n */\nexport function canonicalizeContractToObject(\n contract: Contract,\n options: CanonicalizeContractOptions,\n): Record<string, unknown> {\n const serialized = options.serializeContract(contract);\n const normalized: Record<string, unknown> = {\n ...ifDefined('schemaVersion', options.schemaVersion),\n targetFamily: serialized['targetFamily'],\n target: serialized['target'],\n profileHash: serialized['profileHash'],\n roots: serialized['roots'],\n models: serialized['models'],\n ...ifDefined('valueObjects', serialized['valueObjects']),\n storage: serialized['storage'],\n ...ifDefined('execution', serialized['execution']),\n extensionPacks: serialized['extensionPacks'],\n capabilities: serialized['capabilities'],\n meta: serialized['meta'],\n };\n const withDefaultsOmitted = omitDefaults(normalized, []) as Record<string, unknown>;\n const withSortedIndexes = sortIndexesAndUniques(withDefaultsOmitted['storage']);\n const withSortedStorage = { ...withDefaultsOmitted, storage: withSortedIndexes };\n const withSortedKeys = sortObjectKeys(withSortedStorage) as Record<string, unknown>;\n return orderTopLevel(withSortedKeys);\n}\n\nexport function canonicalizeContract(\n contract: Contract,\n options: CanonicalizeContractOptions,\n): string {\n return JSON.stringify(canonicalizeContractToObject(contract, options), null, 2);\n}\n","import { createHash } from 'node:crypto';\nimport type { JsonObject } from '@prisma-next/utils/json';\nimport { canonicalizeContract } from './canonicalization';\nimport type { Contract } from './contract-types';\nimport type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from './types';\n\nconst SCHEMA_VERSION = '1';\n\nfunction sha256(content: string): string {\n const hash = createHash('sha256');\n hash.update(content);\n return `sha256:${hash.digest('hex')}`;\n}\n\nfunction hashContract(section: Record<string, unknown>): string {\n // Blind cast: the synthesised object is a hash-only stand-in\n // — never returned to callers, never executed as a Contract.\n // `canonicalizeContract` only walks the storage / execution /\n // capabilities slices, all of which are populated above, so the\n // missing precise Contract typing on the other slots is\n // immaterial for the hash result.\n const contract = {\n targetFamily: section['targetFamily'],\n target: section['target'],\n roots: {},\n models: {},\n storage: section['storage'] ?? {},\n execution: section['execution'],\n extensionPacks: {},\n capabilities: section['capabilities'] ?? {},\n meta: {},\n profileHash: '',\n ...section,\n } as unknown as Contract;\n return canonicalizeContract(contract, {\n schemaVersion: SCHEMA_VERSION,\n serializeContract: (c) => JSON.parse(JSON.stringify(c)) as JsonObject,\n });\n}\n\nexport function computeStorageHash(args: {\n target: string;\n targetFamily: string;\n storage: Record<string, unknown>;\n}): StorageHashBase<string> {\n return sha256(hashContract(args)) as StorageHashBase<string>;\n}\n\nexport function computeExecutionHash(args: {\n target: string;\n targetFamily: string;\n execution: Record<string, unknown>;\n}): ExecutionHashBase<string> {\n return sha256(hashContract(args)) as ExecutionHashBase<string>;\n}\n\nexport function computeProfileHash(args: {\n target: string;\n targetFamily: string;\n capabilities: Record<string, Record<string, boolean>>;\n}): ProfileHashBase<string> {\n return sha256(hashContract(args)) as ProfileHashBase<string>;\n}\n"],"mappings":"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"}
|
package/dist/hashing.d.mts
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { S as ProfileHashBase, T as StorageHashBase, d as ExecutionHashBase, t as Contract } from "./contract-types-Kl86EaEa.mjs";
|
|
2
|
+
import { JsonObject } from "@prisma-next/utils/json";
|
|
2
3
|
|
|
3
4
|
//#region src/canonicalization.d.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Per-target contract serializer hook. The framework canonicalizer uses
|
|
7
|
+
* this to convert an in-memory contract (which may carry class-instance
|
|
8
|
+
* IR nodes whose runtime-only fields must not appear in the on-disk
|
|
9
|
+
* envelope) into a plain JsonObject before applying the family-agnostic
|
|
10
|
+
* canonical-key ordering / default-omission / sort steps. Targets whose
|
|
11
|
+
* contract is JSON-clean by construction return the contract unchanged.
|
|
12
|
+
*/
|
|
13
|
+
type SerializeContract = (contract: Contract) => JsonObject;
|
|
14
|
+
interface CanonicalizeContractOptions {
|
|
15
|
+
readonly schemaVersion?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Per-target hook that converts the in-memory contract (which may
|
|
18
|
+
* carry class-instance IR nodes) into a plain JsonObject before the
|
|
19
|
+
* family-agnostic canonicalization steps run.
|
|
20
|
+
*
|
|
21
|
+
* Routing through the hook is what lets each target decide which
|
|
22
|
+
* fields appear in the on-disk envelope; runtime-only class API
|
|
23
|
+
* fields stay invisible to the canonicalization walk by virtue of
|
|
24
|
+
* the per-target serializer not putting them in the JSON shape.
|
|
25
|
+
*/
|
|
26
|
+
readonly serializeContract: SerializeContract;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Object-form variant of {@link canonicalizeContract}. Exported because the
|
|
30
|
+
* emitter writes the canonical contract through a separate JSON-stringify
|
|
31
|
+
* pass and consumes the structured object directly.
|
|
32
|
+
*/
|
|
33
|
+
declare function canonicalizeContractToObject(contract: Contract, options: CanonicalizeContractOptions): Record<string, unknown>;
|
|
34
|
+
declare function canonicalizeContract(contract: Contract, options: CanonicalizeContractOptions): string;
|
|
10
35
|
//#endregion
|
|
11
36
|
//#region src/hashing.d.ts
|
|
12
37
|
declare function computeStorageHash(args: {
|
|
@@ -25,5 +50,5 @@ declare function computeProfileHash(args: {
|
|
|
25
50
|
capabilities: Record<string, Record<string, boolean>>;
|
|
26
51
|
}): ProfileHashBase<string>;
|
|
27
52
|
//#endregion
|
|
28
|
-
export { canonicalizeContract, canonicalizeContractToObject, computeExecutionHash, computeProfileHash, computeStorageHash };
|
|
53
|
+
export { type CanonicalizeContractOptions, type SerializeContract, canonicalizeContract, canonicalizeContractToObject, computeExecutionHash, computeProfileHash, computeStorageHash };
|
|
29
54
|
//# sourceMappingURL=hashing.d.mts.map
|
package/dist/hashing.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hashing.d.mts","names":[],"sources":["../src/canonicalization.ts","../src/hashing.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"hashing.d.mts","names":[],"sources":["../src/canonicalization.ts","../src/hashing.ts"],"mappings":";;;;;;AAcA;;;;;;KAAY,iBAAA,IAAqB,QAAA,EAAU,QAAA,KAAa,UAAA;AAAA,UA+OvC,2BAAA;EAAA,SACN,aAAA;EADiC;;;;;;;;AAoB5C;;EApB4C,SAYjC,iBAAA,EAAmB,iBAAA;AAAA;;;;;;iBAQd,4BAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,EAAS,2BAAA,GACR,MAAA;AAAA,iBAuBa,oBAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,EAAS,2BAAA;;;iBCrQK,kBAAA,CAAmB,IAAA;EACjC,MAAA;EACA,YAAA;EACA,OAAA,EAAS,MAAA;AAAA,IACP,eAAA;AAAA,iBAIY,oBAAA,CAAqB,IAAA;EACnC,MAAA;EACA,YAAA;EACA,SAAA,EAAW,MAAA;AAAA,IACT,iBAAA;AAAA,iBAIY,kBAAA,CAAmB,IAAA;EACjC,MAAA;EACA,YAAA;EACA,YAAA,EAAc,MAAA,SAAe,MAAA;AAAA,IAC3B,eAAA"}
|
package/dist/hashing.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as canonicalizeContractToObject, i as canonicalizeContract, n as computeProfileHash, r as computeStorageHash, t as computeExecutionHash } from "./hashing-
|
|
1
|
+
import { a as canonicalizeContractToObject, i as canonicalizeContract, n as computeProfileHash, r as computeStorageHash, t as computeExecutionHash } from "./hashing-JryrY8ZF.mjs";
|
|
2
2
|
export { canonicalizeContract, canonicalizeContractToObject, computeExecutionHash, computeProfileHash, computeStorageHash };
|
package/dist/testing.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as coreHash } from "./types-CVGwkRLa.mjs";
|
|
2
|
-
import { n as computeProfileHash, r as computeStorageHash, t as computeExecutionHash } from "./hashing-
|
|
2
|
+
import { n as computeProfileHash, r as computeStorageHash, t as computeExecutionHash } from "./hashing-JryrY8ZF.mjs";
|
|
3
3
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
4
|
//#region src/testing-factories.ts
|
|
5
5
|
const DUMMY_HASH = coreHash("sha256:test");
|
package/dist/validate-domain.mjs
CHANGED
|
@@ -1,2 +1,97 @@
|
|
|
1
|
-
import { t as
|
|
1
|
+
import { t as ContractValidationError } from "./contract-validation-error-Dp2vHZt5.mjs";
|
|
2
|
+
//#region src/validate-domain.ts
|
|
3
|
+
function validateContractDomain(contract) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
const modelNames = new Set(Object.keys(contract.models));
|
|
6
|
+
validateRoots(contract, modelNames, errors);
|
|
7
|
+
validateVariantsAndBases(contract, modelNames, errors);
|
|
8
|
+
validateRelationTargets(contract, modelNames, errors);
|
|
9
|
+
validateDiscriminators(contract, errors);
|
|
10
|
+
validateOwnership(contract, modelNames, errors);
|
|
11
|
+
validateValueObjectReferences(contract, errors);
|
|
12
|
+
validateFieldModifiers(contract, errors);
|
|
13
|
+
if (errors.length > 0) throw new ContractValidationError(`Contract domain validation failed:\n- ${errors.join("\n- ")}`, "domain");
|
|
14
|
+
}
|
|
15
|
+
function validateRoots(contract, modelNames, errors) {
|
|
16
|
+
const seenValues = /* @__PURE__ */ new Set();
|
|
17
|
+
for (const [rootKey, modelName] of Object.entries(contract.roots)) {
|
|
18
|
+
if (seenValues.has(modelName)) errors.push(`Duplicate root value: "${modelName}" is mapped by multiple root keys`);
|
|
19
|
+
seenValues.add(modelName);
|
|
20
|
+
if (!modelNames.has(modelName)) errors.push(`Root "${rootKey}" references model "${modelName}" which does not exist in models`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function validateVariantsAndBases(contract, modelNames, errors) {
|
|
24
|
+
const models = new Map(Object.entries(contract.models));
|
|
25
|
+
for (const [modelName, model] of models) {
|
|
26
|
+
if (model.variants) for (const variantName of Object.keys(model.variants)) {
|
|
27
|
+
if (!modelNames.has(variantName)) {
|
|
28
|
+
errors.push(`Model "${modelName}" lists variant "${variantName}" which does not exist in models`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const variantModel = models.get(variantName);
|
|
32
|
+
if (!variantModel) continue;
|
|
33
|
+
if (variantModel.base !== modelName) errors.push(`Variant "${variantName}" has base "${variantModel.base ?? "(none)"}" but expected "${modelName}"`);
|
|
34
|
+
}
|
|
35
|
+
if (model.base) {
|
|
36
|
+
if (!modelNames.has(model.base)) {
|
|
37
|
+
errors.push(`Model "${modelName}" has base "${model.base}" which does not exist in models`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const baseModel = models.get(model.base);
|
|
41
|
+
if (!baseModel) continue;
|
|
42
|
+
if (!baseModel.variants || !Object.hasOwn(baseModel.variants, modelName)) errors.push(`Model "${modelName}" has base "${model.base}" which does not list it as a variant`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function validateRelationTargets(contract, modelNames, errors) {
|
|
47
|
+
for (const [modelName, model] of Object.entries(contract.models)) for (const [relName, relation] of Object.entries(model.relations ?? {})) if (!modelNames.has(relation.to)) errors.push(`Relation "${relName}" on model "${modelName}" targets "${relation.to}" which does not exist in models`);
|
|
48
|
+
}
|
|
49
|
+
function validateDiscriminators(contract, errors) {
|
|
50
|
+
for (const [modelName, model] of Object.entries(contract.models)) {
|
|
51
|
+
if (model.discriminator) {
|
|
52
|
+
if (!model.variants || Object.keys(model.variants).length === 0) errors.push(`Model "${modelName}" has discriminator but no variants`);
|
|
53
|
+
if (!Object.hasOwn(model.fields, model.discriminator.field)) errors.push(`Discriminator field "${model.discriminator.field}" is not a field on model "${modelName}"`);
|
|
54
|
+
}
|
|
55
|
+
if (model.variants && Object.keys(model.variants).length > 0 && !model.discriminator) errors.push(`Model "${modelName}" has variants but no discriminator`);
|
|
56
|
+
if (model.base) {
|
|
57
|
+
if (model.discriminator) errors.push(`Model "${modelName}" has base and must not have discriminator`);
|
|
58
|
+
if (model.variants && Object.keys(model.variants).length > 0) errors.push(`Model "${modelName}" has base and must not have variants`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function validateOwnership(contract, modelNames, errors) {
|
|
63
|
+
for (const [modelName, model] of Object.entries(contract.models)) {
|
|
64
|
+
if (!model.owner) continue;
|
|
65
|
+
if (model.owner === modelName) errors.push(`Model "${modelName}" cannot own itself`);
|
|
66
|
+
if (!modelNames.has(model.owner)) errors.push(`Model "${modelName}" has owner "${model.owner}" which does not exist in models`);
|
|
67
|
+
for (const [rootKey, rootModel] of Object.entries(contract.roots)) if (rootModel === modelName) errors.push(`Owned model "${modelName}" must not appear in roots (found as root "${rootKey}")`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function forEachContractField(contract, callback) {
|
|
71
|
+
for (const [modelName, model] of Object.entries(contract.models)) for (const [fieldName, field] of Object.entries(model.fields)) callback(field, `Model "${modelName}" field "${fieldName}"`);
|
|
72
|
+
for (const [voName, vo] of Object.entries(contract.valueObjects ?? {})) for (const [fieldName, field] of Object.entries(vo.fields)) callback(field, `Value object "${voName}" field "${fieldName}"`);
|
|
73
|
+
}
|
|
74
|
+
function validateValueObjectReferences(contract, errors) {
|
|
75
|
+
const voNames = new Set(Object.keys(contract.valueObjects ?? {}));
|
|
76
|
+
function checkType(type, location) {
|
|
77
|
+
if (!type) return;
|
|
78
|
+
if (type.kind === "valueObject" && type.name && !voNames.has(type.name)) {
|
|
79
|
+
errors.push(`${location} references value object "${type.name}" which does not exist in valueObjects`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (type.kind === "union") for (const member of type.members ?? []) checkType(member, location);
|
|
83
|
+
}
|
|
84
|
+
forEachContractField(contract, (field, location) => {
|
|
85
|
+
checkType(field?.type, location);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function validateFieldModifiers(contract, errors) {
|
|
89
|
+
forEachContractField(contract, (field, location) => {
|
|
90
|
+
const f = field;
|
|
91
|
+
if (f?.many && f?.dict) errors.push(`${location} cannot have both "many" and "dict" modifiers`);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
2
95
|
export { validateContractDomain };
|
|
96
|
+
|
|
97
|
+
//# sourceMappingURL=validate-domain.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-domain.mjs","names":["f"],"sources":["../src/validate-domain.ts"],"sourcesContent":["import { ContractValidationError } from './contract-validation-error';\n\nexport interface DomainModelShape {\n readonly fields: Record<string, unknown>;\n readonly relations?: Record<string, { readonly to: string }>;\n readonly discriminator?: { readonly field: string };\n readonly variants?: Record<string, unknown>;\n readonly base?: string;\n readonly owner?: string;\n}\n\nexport interface DomainContractShape {\n readonly roots: Record<string, string>;\n readonly models: Record<string, DomainModelShape>;\n readonly valueObjects?: Record<string, { readonly fields: Record<string, unknown> }>;\n}\n\nexport function validateContractDomain(contract: DomainContractShape): void {\n const errors: string[] = [];\n const modelNames = new Set(Object.keys(contract.models));\n\n validateRoots(contract, modelNames, errors);\n validateVariantsAndBases(contract, modelNames, errors);\n validateRelationTargets(contract, modelNames, errors);\n validateDiscriminators(contract, errors);\n validateOwnership(contract, modelNames, errors);\n validateValueObjectReferences(contract, errors);\n validateFieldModifiers(contract, errors);\n\n if (errors.length > 0) {\n throw new ContractValidationError(\n `Contract domain validation failed:\\n- ${errors.join('\\n- ')}`,\n 'domain',\n );\n }\n}\n\nfunction validateRoots(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n const seenValues = new Set<string>();\n for (const [rootKey, modelName] of Object.entries(contract.roots)) {\n if (seenValues.has(modelName)) {\n errors.push(`Duplicate root value: \"${modelName}\" is mapped by multiple root keys`);\n }\n seenValues.add(modelName);\n\n if (!modelNames.has(modelName)) {\n errors.push(\n `Root \"${rootKey}\" references model \"${modelName}\" which does not exist in models`,\n );\n }\n }\n}\n\nfunction validateVariantsAndBases(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n const models = new Map(Object.entries(contract.models));\n\n for (const [modelName, model] of models) {\n if (model.variants) {\n for (const variantName of Object.keys(model.variants)) {\n if (!modelNames.has(variantName)) {\n errors.push(\n `Model \"${modelName}\" lists variant \"${variantName}\" which does not exist in models`,\n );\n continue;\n }\n const variantModel = models.get(variantName);\n if (!variantModel) continue;\n if (variantModel.base !== modelName) {\n errors.push(\n `Variant \"${variantName}\" has base \"${variantModel.base ?? '(none)'}\" but expected \"${modelName}\"`,\n );\n }\n }\n }\n\n if (model.base) {\n if (!modelNames.has(model.base)) {\n errors.push(`Model \"${modelName}\" has base \"${model.base}\" which does not exist in models`);\n continue;\n }\n const baseModel = models.get(model.base);\n if (!baseModel) continue;\n if (!baseModel.variants || !Object.hasOwn(baseModel.variants, modelName)) {\n errors.push(\n `Model \"${modelName}\" has base \"${model.base}\" which does not list it as a variant`,\n );\n }\n }\n }\n}\n\nfunction validateRelationTargets(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n for (const [relName, relation] of Object.entries(model.relations ?? {})) {\n if (!modelNames.has(relation.to)) {\n errors.push(\n `Relation \"${relName}\" on model \"${modelName}\" targets \"${relation.to}\" which does not exist in models`,\n );\n }\n }\n }\n}\n\nfunction validateDiscriminators(contract: DomainContractShape, errors: string[]): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n if (model.discriminator) {\n if (!model.variants || Object.keys(model.variants).length === 0) {\n errors.push(`Model \"${modelName}\" has discriminator but no variants`);\n }\n if (!Object.hasOwn(model.fields, model.discriminator.field)) {\n errors.push(\n `Discriminator field \"${model.discriminator.field}\" is not a field on model \"${modelName}\"`,\n );\n }\n }\n\n if (model.variants && Object.keys(model.variants).length > 0 && !model.discriminator) {\n errors.push(`Model \"${modelName}\" has variants but no discriminator`);\n }\n\n if (model.base) {\n if (model.discriminator) {\n errors.push(`Model \"${modelName}\" has base and must not have discriminator`);\n }\n if (model.variants && Object.keys(model.variants).length > 0) {\n errors.push(`Model \"${modelName}\" has base and must not have variants`);\n }\n }\n }\n}\n\nfunction validateOwnership(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n if (!model.owner) continue;\n\n if (model.owner === modelName) {\n errors.push(`Model \"${modelName}\" cannot own itself`);\n }\n\n if (!modelNames.has(model.owner)) {\n errors.push(`Model \"${modelName}\" has owner \"${model.owner}\" which does not exist in models`);\n }\n\n for (const [rootKey, rootModel] of Object.entries(contract.roots)) {\n if (rootModel === modelName) {\n errors.push(\n `Owned model \"${modelName}\" must not appear in roots (found as root \"${rootKey}\")`,\n );\n }\n }\n }\n}\n\ninterface FieldTypeLike {\n readonly kind?: string;\n readonly name?: string;\n readonly members?: readonly FieldTypeLike[];\n}\n\ninterface FieldLike {\n readonly type?: FieldTypeLike;\n readonly many?: boolean;\n readonly dict?: boolean;\n}\n\nfunction forEachContractField(\n contract: DomainContractShape,\n callback: (field: unknown, location: string) => void,\n): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n for (const [fieldName, field] of Object.entries(model.fields)) {\n callback(field, `Model \"${modelName}\" field \"${fieldName}\"`);\n }\n }\n for (const [voName, vo] of Object.entries(contract.valueObjects ?? {})) {\n for (const [fieldName, field] of Object.entries(vo.fields)) {\n callback(field, `Value object \"${voName}\" field \"${fieldName}\"`);\n }\n }\n}\n\nfunction validateValueObjectReferences(contract: DomainContractShape, errors: string[]): void {\n const voNames = new Set(Object.keys(contract.valueObjects ?? {}));\n\n function checkType(type: FieldTypeLike | undefined, location: string): void {\n if (!type) return;\n if (type.kind === 'valueObject' && type.name && !voNames.has(type.name)) {\n errors.push(\n `${location} references value object \"${type.name}\" which does not exist in valueObjects`,\n );\n return;\n }\n if (type.kind === 'union') {\n for (const member of type.members ?? []) checkType(member, location);\n }\n }\n\n forEachContractField(contract, (field, location) => {\n const f = field as FieldLike | undefined;\n checkType(f?.type, location);\n });\n}\n\nfunction validateFieldModifiers(contract: DomainContractShape, errors: string[]): void {\n forEachContractField(contract, (field, location) => {\n const f = field as FieldLike | undefined;\n if (f?.many && f?.dict) {\n errors.push(`${location} cannot have both \"many\" and \"dict\" modifiers`);\n }\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-dev.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "Data contract type definitions and JSON schema for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/utils": "0.
|
|
9
|
+
"@prisma-next/utils": "0.9.0-dev.1",
|
|
10
10
|
"@standard-schema/spec": "^1.1.0",
|
|
11
11
|
"arktype": "^2.1.29"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@prisma-next/test-utils": "0.
|
|
15
|
-
"@prisma-next/tsconfig": "0.
|
|
16
|
-
"@prisma-next/tsdown": "0.
|
|
14
|
+
"@prisma-next/test-utils": "0.9.0-dev.1",
|
|
15
|
+
"@prisma-next/tsconfig": "0.9.0-dev.1",
|
|
16
|
+
"@prisma-next/tsdown": "0.9.0-dev.1",
|
|
17
17
|
"tsdown": "0.22.0",
|
|
18
18
|
"typescript": "5.9.3",
|
|
19
19
|
"vitest": "4.1.5"
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"node": ">=20"
|
|
28
28
|
},
|
|
29
29
|
"exports": {
|
|
30
|
+
"./contract-validation-error": "./dist/contract-validation-error.mjs",
|
|
30
31
|
"./hashing": "./dist/hashing.mjs",
|
|
31
32
|
"./testing": "./dist/testing.mjs",
|
|
32
33
|
"./types": "./dist/types.mjs",
|
|
33
|
-
"./validate-contract": "./dist/validate-contract.mjs",
|
|
34
34
|
"./validate-domain": "./dist/validate-domain.mjs",
|
|
35
35
|
"./package.json": "./package.json"
|
|
36
36
|
},
|
package/src/canonicalization.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { isArrayEqual } from '@prisma-next/utils/array-equal';
|
|
2
2
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
3
|
+
import type { JsonObject } from '@prisma-next/utils/json';
|
|
3
4
|
|
|
4
5
|
import type { Contract } from './contract-types';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Per-target contract serializer hook. The framework canonicalizer uses
|
|
9
|
+
* this to convert an in-memory contract (which may carry class-instance
|
|
10
|
+
* IR nodes whose runtime-only fields must not appear in the on-disk
|
|
11
|
+
* envelope) into a plain JsonObject before applying the family-agnostic
|
|
12
|
+
* canonical-key ordering / default-omission / sort steps. Targets whose
|
|
13
|
+
* contract is JSON-clean by construction return the contract unchanged.
|
|
14
|
+
*/
|
|
15
|
+
export type SerializeContract = (contract: Contract) => JsonObject;
|
|
16
|
+
|
|
6
17
|
const TOP_LEVEL_ORDER = [
|
|
7
18
|
'schemaVersion',
|
|
8
19
|
'canonicalVersion',
|
|
@@ -240,23 +251,44 @@ export function orderTopLevel(obj: Record<string, unknown>): Record<string, unkn
|
|
|
240
251
|
return ordered;
|
|
241
252
|
}
|
|
242
253
|
|
|
254
|
+
export interface CanonicalizeContractOptions {
|
|
255
|
+
readonly schemaVersion?: string;
|
|
256
|
+
/**
|
|
257
|
+
* Per-target hook that converts the in-memory contract (which may
|
|
258
|
+
* carry class-instance IR nodes) into a plain JsonObject before the
|
|
259
|
+
* family-agnostic canonicalization steps run.
|
|
260
|
+
*
|
|
261
|
+
* Routing through the hook is what lets each target decide which
|
|
262
|
+
* fields appear in the on-disk envelope; runtime-only class API
|
|
263
|
+
* fields stay invisible to the canonicalization walk by virtue of
|
|
264
|
+
* the per-target serializer not putting them in the JSON shape.
|
|
265
|
+
*/
|
|
266
|
+
readonly serializeContract: SerializeContract;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Object-form variant of {@link canonicalizeContract}. Exported because the
|
|
271
|
+
* emitter writes the canonical contract through a separate JSON-stringify
|
|
272
|
+
* pass and consumes the structured object directly.
|
|
273
|
+
*/
|
|
243
274
|
export function canonicalizeContractToObject(
|
|
244
275
|
contract: Contract,
|
|
245
|
-
options
|
|
276
|
+
options: CanonicalizeContractOptions,
|
|
246
277
|
): Record<string, unknown> {
|
|
278
|
+
const serialized = options.serializeContract(contract);
|
|
247
279
|
const normalized: Record<string, unknown> = {
|
|
248
|
-
...ifDefined('schemaVersion', options
|
|
249
|
-
targetFamily:
|
|
250
|
-
target:
|
|
251
|
-
profileHash:
|
|
252
|
-
roots:
|
|
253
|
-
models:
|
|
254
|
-
...ifDefined('valueObjects',
|
|
255
|
-
storage:
|
|
256
|
-
...ifDefined('execution',
|
|
257
|
-
extensionPacks:
|
|
258
|
-
capabilities:
|
|
259
|
-
meta:
|
|
280
|
+
...ifDefined('schemaVersion', options.schemaVersion),
|
|
281
|
+
targetFamily: serialized['targetFamily'],
|
|
282
|
+
target: serialized['target'],
|
|
283
|
+
profileHash: serialized['profileHash'],
|
|
284
|
+
roots: serialized['roots'],
|
|
285
|
+
models: serialized['models'],
|
|
286
|
+
...ifDefined('valueObjects', serialized['valueObjects']),
|
|
287
|
+
storage: serialized['storage'],
|
|
288
|
+
...ifDefined('execution', serialized['execution']),
|
|
289
|
+
extensionPacks: serialized['extensionPacks'],
|
|
290
|
+
capabilities: serialized['capabilities'],
|
|
291
|
+
meta: serialized['meta'],
|
|
260
292
|
};
|
|
261
293
|
const withDefaultsOmitted = omitDefaults(normalized, []) as Record<string, unknown>;
|
|
262
294
|
const withSortedIndexes = sortIndexesAndUniques(withDefaultsOmitted['storage']);
|
|
@@ -267,7 +299,7 @@ export function canonicalizeContractToObject(
|
|
|
267
299
|
|
|
268
300
|
export function canonicalizeContract(
|
|
269
301
|
contract: Contract,
|
|
270
|
-
options
|
|
302
|
+
options: CanonicalizeContractOptions,
|
|
271
303
|
): string {
|
|
272
304
|
return JSON.stringify(canonicalizeContractToObject(contract, options), null, 2);
|
|
273
305
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ContractValidationPhase = 'structural' | 'domain' | 'storage';
|
|
2
|
+
|
|
3
|
+
export class ContractValidationError extends Error {
|
|
4
|
+
readonly code = 'CONTRACT.VALIDATION_FAILED';
|
|
5
|
+
readonly phase: ContractValidationPhase;
|
|
6
|
+
|
|
7
|
+
constructor(message: string, phase: ContractValidationPhase) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'ContractValidationError';
|
|
10
|
+
this.phase = phase;
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/exports/hashing.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
type CanonicalizeContractOptions,
|
|
3
|
+
canonicalizeContract,
|
|
4
|
+
canonicalizeContractToObject,
|
|
5
|
+
type SerializeContract,
|
|
6
|
+
} from '../canonicalization';
|
|
2
7
|
export { computeExecutionHash, computeProfileHash, computeStorageHash } from '../hashing';
|
package/src/hashing.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { JsonObject } from '@prisma-next/utils/json';
|
|
2
3
|
import { canonicalizeContract } from './canonicalization';
|
|
3
4
|
import type { Contract } from './contract-types';
|
|
4
5
|
import type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from './types';
|
|
@@ -12,6 +13,12 @@ function sha256(content: string): string {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function hashContract(section: Record<string, unknown>): string {
|
|
16
|
+
// Blind cast: the synthesised object is a hash-only stand-in
|
|
17
|
+
// — never returned to callers, never executed as a Contract.
|
|
18
|
+
// `canonicalizeContract` only walks the storage / execution /
|
|
19
|
+
// capabilities slices, all of which are populated above, so the
|
|
20
|
+
// missing precise Contract typing on the other slots is
|
|
21
|
+
// immaterial for the hash result.
|
|
15
22
|
const contract = {
|
|
16
23
|
targetFamily: section['targetFamily'],
|
|
17
24
|
target: section['target'],
|
|
@@ -24,8 +31,11 @@ function hashContract(section: Record<string, unknown>): string {
|
|
|
24
31
|
meta: {},
|
|
25
32
|
profileHash: '',
|
|
26
33
|
...section,
|
|
27
|
-
} as Contract;
|
|
28
|
-
return canonicalizeContract(contract, {
|
|
34
|
+
} as unknown as Contract;
|
|
35
|
+
return canonicalizeContract(contract, {
|
|
36
|
+
schemaVersion: SCHEMA_VERSION,
|
|
37
|
+
serializeContract: (c) => JSON.parse(JSON.stringify(c)) as JsonObject,
|
|
38
|
+
});
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
export function computeStorageHash(args: {
|
package/src/validate-domain.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hashing-Dz-16T7H.mjs","names":[],"sources":["../src/canonicalization.ts","../src/hashing.ts"],"sourcesContent":["import { isArrayEqual } from '@prisma-next/utils/array-equal';\nimport { ifDefined } from '@prisma-next/utils/defined';\n\nimport type { Contract } from './contract-types';\n\nconst TOP_LEVEL_ORDER = [\n 'schemaVersion',\n 'canonicalVersion',\n 'targetFamily',\n 'target',\n 'profileHash',\n 'roots',\n 'models',\n 'valueObjects',\n 'storage',\n 'execution',\n 'capabilities',\n 'extensionPacks',\n 'meta',\n] as const;\n\nfunction isDefaultValue(value: unknown): boolean {\n if (value === false) return true;\n if (value === null) return false;\n if (Array.isArray(value) && value.length === 0) return true;\n if (typeof value === 'object' && value !== null) {\n const keys = Object.keys(value);\n return keys.length === 0;\n }\n return false;\n}\n\nfunction omitDefaults(obj: unknown, path: readonly string[]): unknown {\n if (obj === null || typeof obj !== 'object') {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => omitDefaults(item, path));\n }\n\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(obj)) {\n const currentPath = [...path, key];\n\n if (key === '_generated') {\n continue;\n }\n\n if (key === 'generated' && value === false) {\n continue;\n }\n\n if ((key === 'onDelete' || key === 'onUpdate') && value === 'noAction') {\n continue;\n }\n\n if (isDefaultValue(value)) {\n const isRequiredModels = isArrayEqual(currentPath, ['models']);\n const isRequiredTables = isArrayEqual(currentPath, ['storage', 'tables']);\n const isRequiredCollections = isArrayEqual(currentPath, ['storage', 'collections']);\n const isCollectionEntry =\n currentPath.length === 3 &&\n isArrayEqual([currentPath[0], currentPath[1]], ['storage', 'collections']);\n const isRequiredRoots = isArrayEqual(currentPath, ['roots']);\n const isRequiredExtensionPacks = isArrayEqual(currentPath, ['extensionPacks']);\n const isRequiredCapabilities = isArrayEqual(currentPath, ['capabilities']);\n const isRequiredMeta = isArrayEqual(currentPath, ['meta']);\n const isRequiredExecutionDefaults = isArrayEqual(currentPath, [\n 'execution',\n 'mutations',\n 'defaults',\n ]);\n const isExtensionNamespace = currentPath.length === 2 && currentPath[0] === 'extensionPacks';\n const isModelRelations =\n currentPath.length === 3 &&\n isArrayEqual([currentPath[0], currentPath[2]], ['models', 'relations']);\n const isModelStorage =\n currentPath.length === 3 &&\n isArrayEqual([currentPath[0], currentPath[2]], ['models', 'storage']);\n const isTableUniques =\n currentPath.length === 4 &&\n isArrayEqual(\n [currentPath[0], currentPath[1], currentPath[3]],\n ['storage', 'tables', 'uniques'],\n );\n const isTableIndexes =\n currentPath.length === 4 &&\n isArrayEqual(\n [currentPath[0], currentPath[1], currentPath[3]],\n ['storage', 'tables', 'indexes'],\n );\n const isTableForeignKeys =\n currentPath.length === 4 &&\n isArrayEqual(\n [currentPath[0], currentPath[1], currentPath[3]],\n ['storage', 'tables', 'foreignKeys'],\n );\n\n // `storage.types.<name>.typeParams` is part of the StorageTypeInstance\n // shape (validators require it). Preserve it even when empty so the\n // emitted contract.json remains structurally valid after a round-trip.\n const isStorageTypeTypeParams =\n currentPath.length === 4 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'types' &&\n key === 'typeParams';\n\n const isFkBooleanField =\n currentPath.length === 5 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'tables' &&\n currentPath[3] === 'foreignKeys' &&\n (key === 'constraint' || key === 'index');\n\n const isNullableField = key === 'nullable';\n\n if (\n !isRequiredModels &&\n !isRequiredTables &&\n !isRequiredCollections &&\n !isCollectionEntry &&\n !isRequiredRoots &&\n !isRequiredExtensionPacks &&\n !isRequiredCapabilities &&\n !isRequiredMeta &&\n !isRequiredExecutionDefaults &&\n !isExtensionNamespace &&\n !isModelRelations &&\n !isModelStorage &&\n !isTableUniques &&\n !isTableIndexes &&\n !isTableForeignKeys &&\n !isFkBooleanField &&\n !isNullableField &&\n !isStorageTypeTypeParams\n ) {\n continue;\n }\n }\n\n result[key] = omitDefaults(value, currentPath);\n }\n\n return result;\n}\n\nfunction sortObjectKeys(obj: unknown): unknown {\n if (obj === null || typeof obj !== 'object') {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => sortObjectKeys(item));\n }\n\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(obj).sort();\n for (const key of keys) {\n sorted[key] = sortObjectKeys((obj as Record<string, unknown>)[key]);\n }\n\n return sorted;\n}\n\ntype StorageObject = {\n tables?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\ntype TableObject = {\n indexes?: unknown[];\n uniques?: unknown[];\n [key: string]: unknown;\n};\n\nfunction sortIndexesAndUniques(storage: unknown): unknown {\n if (!storage || typeof storage !== 'object') {\n return storage;\n }\n\n const storageObj = storage as StorageObject;\n if (!storageObj.tables || typeof storageObj.tables !== 'object') {\n return storage;\n }\n\n const tables = storageObj.tables;\n const result: StorageObject = { ...storageObj };\n\n result.tables = {};\n const sortedTableNames = Object.keys(tables).sort();\n for (const tableName of sortedTableNames) {\n const table = tables[tableName];\n if (!table || typeof table !== 'object') {\n result.tables[tableName] = table;\n continue;\n }\n\n const tableObj = table as TableObject;\n const sortedTable: TableObject = { ...tableObj };\n\n if (Array.isArray(tableObj.indexes)) {\n sortedTable.indexes = [...tableObj.indexes].sort((a, b) => {\n const nameA = (a as { name?: string })?.name || '';\n const nameB = (b as { name?: string })?.name || '';\n return nameA.localeCompare(nameB);\n });\n }\n\n if (Array.isArray(tableObj.uniques)) {\n sortedTable.uniques = [...tableObj.uniques].sort((a, b) => {\n const nameA = (a as { name?: string })?.name || '';\n const nameB = (b as { name?: string })?.name || '';\n return nameA.localeCompare(nameB);\n });\n }\n\n result.tables[tableName] = sortedTable;\n }\n\n return result;\n}\n\nexport function orderTopLevel(obj: Record<string, unknown>): Record<string, unknown> {\n const ordered: Record<string, unknown> = {};\n const remaining = new Set(Object.keys(obj));\n\n for (const key of TOP_LEVEL_ORDER) {\n if (remaining.has(key)) {\n ordered[key] = obj[key];\n remaining.delete(key);\n }\n }\n\n for (const key of Array.from(remaining).sort()) {\n ordered[key] = obj[key];\n }\n\n return ordered;\n}\n\nexport function canonicalizeContractToObject(\n contract: Contract,\n options?: { schemaVersion?: string },\n): Record<string, unknown> {\n const normalized: Record<string, unknown> = {\n ...ifDefined('schemaVersion', options?.schemaVersion),\n targetFamily: contract.targetFamily,\n target: contract.target,\n profileHash: contract.profileHash,\n roots: contract.roots,\n models: contract.models,\n ...ifDefined('valueObjects', contract.valueObjects),\n storage: contract.storage,\n ...ifDefined('execution', contract.execution),\n extensionPacks: contract.extensionPacks,\n capabilities: contract.capabilities,\n meta: contract.meta,\n };\n const withDefaultsOmitted = omitDefaults(normalized, []) as Record<string, unknown>;\n const withSortedIndexes = sortIndexesAndUniques(withDefaultsOmitted['storage']);\n const withSortedStorage = { ...withDefaultsOmitted, storage: withSortedIndexes };\n const withSortedKeys = sortObjectKeys(withSortedStorage) as Record<string, unknown>;\n return orderTopLevel(withSortedKeys);\n}\n\nexport function canonicalizeContract(\n contract: Contract,\n options?: { schemaVersion?: string },\n): string {\n return JSON.stringify(canonicalizeContractToObject(contract, options), null, 2);\n}\n","import { createHash } from 'node:crypto';\nimport { canonicalizeContract } from './canonicalization';\nimport type { Contract } from './contract-types';\nimport type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from './types';\n\nconst SCHEMA_VERSION = '1';\n\nfunction sha256(content: string): string {\n const hash = createHash('sha256');\n hash.update(content);\n return `sha256:${hash.digest('hex')}`;\n}\n\nfunction hashContract(section: Record<string, unknown>): string {\n const contract = {\n targetFamily: section['targetFamily'],\n target: section['target'],\n roots: {},\n models: {},\n storage: section['storage'] ?? {},\n execution: section['execution'],\n extensionPacks: {},\n capabilities: section['capabilities'] ?? {},\n meta: {},\n profileHash: '',\n ...section,\n } as Contract;\n return canonicalizeContract(contract, { schemaVersion: SCHEMA_VERSION });\n}\n\nexport function computeStorageHash(args: {\n target: string;\n targetFamily: string;\n storage: Record<string, unknown>;\n}): StorageHashBase<string> {\n return sha256(hashContract(args)) as StorageHashBase<string>;\n}\n\nexport function computeExecutionHash(args: {\n target: string;\n targetFamily: string;\n execution: Record<string, unknown>;\n}): ExecutionHashBase<string> {\n return sha256(hashContract(args)) as ExecutionHashBase<string>;\n}\n\nexport function computeProfileHash(args: {\n target: string;\n targetFamily: string;\n capabilities: Record<string, Record<string, boolean>>;\n}): ProfileHashBase<string> {\n return sha256(hashContract(args)) as ProfileHashBase<string>;\n}\n"],"mappings":"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"}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { t as Contract } from "./contract-types-Kl86EaEa.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/validate-contract.d.ts
|
|
4
|
-
type ContractValidationPhase = 'structural' | 'domain' | 'storage';
|
|
5
|
-
declare class ContractValidationError extends Error {
|
|
6
|
-
readonly code = "CONTRACT.VALIDATION_FAILED";
|
|
7
|
-
readonly phase: ContractValidationPhase;
|
|
8
|
-
constructor(message: string, phase: ContractValidationPhase);
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Family-provided storage validator.
|
|
12
|
-
* SQL validates tables/columns/FKs; Mongo validates collections/embedding.
|
|
13
|
-
*/
|
|
14
|
-
type StorageValidator = (contract: Contract) => void;
|
|
15
|
-
/**
|
|
16
|
-
* Framework-level contract validation (ADR 182).
|
|
17
|
-
*
|
|
18
|
-
* Three-pass validation:
|
|
19
|
-
* 1. **Structural validation** (arktype): verifies required fields exist with
|
|
20
|
-
* correct base types.
|
|
21
|
-
* 2. **Domain validation** (framework-owned): roots, relation targets,
|
|
22
|
-
* variant/base consistency, discriminators, ownership, orphans.
|
|
23
|
-
* 3. **Storage validation** (family-provided): SQL validates tables/columns/FKs;
|
|
24
|
-
* Mongo validates collections/embedding.
|
|
25
|
-
*
|
|
26
|
-
* JSON persistence fields (`schemaVersion`, `_generated`) are stripped before
|
|
27
|
-
* validation — they are not part of the in-memory contract representation.
|
|
28
|
-
*
|
|
29
|
-
* @template TContract The fully-typed contract type (preserves literal types).
|
|
30
|
-
* @param value Raw contract value (e.g. parsed from JSON).
|
|
31
|
-
* @param storageValidator Family-specific storage validation function.
|
|
32
|
-
* @returns The validated contract with full literal types.
|
|
33
|
-
*/
|
|
34
|
-
declare function validateContract<TContract extends Contract>(value: unknown, storageValidator: StorageValidator): TContract;
|
|
35
|
-
//#endregion
|
|
36
|
-
export { ContractValidationError, type ContractValidationPhase, type StorageValidator, validateContract };
|
|
37
|
-
//# sourceMappingURL=validate-contract.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"validate-contract.d.mts","names":[],"sources":["../src/validate-contract.ts"],"mappings":";;;KAKY,uBAAA;AAAA,cAEC,uBAAA,SAAgC,KAAA;EAAA,SAClC,IAAA;EAAA,SACA,KAAA,EAAO,uBAAA;cAEJ,OAAA,UAAiB,KAAA,EAAO,uBAAA;AAAA;;AAJtC;;;KAeY,gBAAA,IAAoB,QAAA,EAAU,QAAA;;;;;;;;;;;;;;;AAA1C;;;;;iBAqDgB,gBAAA,mBAAmC,QAAA,CAAA,CACjD,KAAA,WACA,gBAAA,EAAkB,gBAAA,GACjB,SAAA"}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { type } from "arktype";
|
|
2
|
-
//#region src/validate-contract.ts
|
|
3
|
-
var ContractValidationError = class extends Error {
|
|
4
|
-
code = "CONTRACT.VALIDATION_FAILED";
|
|
5
|
-
phase;
|
|
6
|
-
constructor(message, phase) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = "ContractValidationError";
|
|
9
|
-
this.phase = phase;
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
const ContractSchema = type({
|
|
13
|
-
target: "string",
|
|
14
|
-
targetFamily: "string",
|
|
15
|
-
roots: "Record<string, string>",
|
|
16
|
-
models: "Record<string, unknown>",
|
|
17
|
-
"valueObjects?": "Record<string, unknown>",
|
|
18
|
-
storage: "Record<string, unknown>",
|
|
19
|
-
capabilities: "Record<string, Record<string, boolean>>",
|
|
20
|
-
extensionPacks: "Record<string, unknown>",
|
|
21
|
-
meta: "Record<string, unknown>",
|
|
22
|
-
"execution?": {
|
|
23
|
-
"executionHash?": "string",
|
|
24
|
-
mutations: { defaults: "unknown[]" }
|
|
25
|
-
},
|
|
26
|
-
profileHash: "string"
|
|
27
|
-
});
|
|
28
|
-
function stripPersistenceFields(raw) {
|
|
29
|
-
const { schemaVersion: _, _generated: _g, ...rest } = raw;
|
|
30
|
-
return rest;
|
|
31
|
-
}
|
|
32
|
-
function extractDomainShape(contract) {
|
|
33
|
-
return {
|
|
34
|
-
roots: contract.roots,
|
|
35
|
-
models: contract.models,
|
|
36
|
-
...contract.valueObjects ? { valueObjects: contract.valueObjects } : {}
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Framework-level contract validation (ADR 182).
|
|
41
|
-
*
|
|
42
|
-
* Three-pass validation:
|
|
43
|
-
* 1. **Structural validation** (arktype): verifies required fields exist with
|
|
44
|
-
* correct base types.
|
|
45
|
-
* 2. **Domain validation** (framework-owned): roots, relation targets,
|
|
46
|
-
* variant/base consistency, discriminators, ownership, orphans.
|
|
47
|
-
* 3. **Storage validation** (family-provided): SQL validates tables/columns/FKs;
|
|
48
|
-
* Mongo validates collections/embedding.
|
|
49
|
-
*
|
|
50
|
-
* JSON persistence fields (`schemaVersion`, `_generated`) are stripped before
|
|
51
|
-
* validation — they are not part of the in-memory contract representation.
|
|
52
|
-
*
|
|
53
|
-
* @template TContract The fully-typed contract type (preserves literal types).
|
|
54
|
-
* @param value Raw contract value (e.g. parsed from JSON).
|
|
55
|
-
* @param storageValidator Family-specific storage validation function.
|
|
56
|
-
* @returns The validated contract with full literal types.
|
|
57
|
-
*/
|
|
58
|
-
function validateContract(value, storageValidator) {
|
|
59
|
-
if (typeof value !== "object" || value === null) throw new ContractValidationError("Contract must be a non-null object", "structural");
|
|
60
|
-
const parsed = ContractSchema(stripPersistenceFields(value));
|
|
61
|
-
if (parsed instanceof type.errors) throw new ContractValidationError(`Invalid contract structure: ${parsed.summary}`, "structural");
|
|
62
|
-
const contract = parsed;
|
|
63
|
-
validateContractDomain(extractDomainShape(contract));
|
|
64
|
-
storageValidator(contract);
|
|
65
|
-
return contract;
|
|
66
|
-
}
|
|
67
|
-
//#endregion
|
|
68
|
-
//#region src/validate-domain.ts
|
|
69
|
-
function validateContractDomain(contract) {
|
|
70
|
-
const errors = [];
|
|
71
|
-
const modelNames = new Set(Object.keys(contract.models));
|
|
72
|
-
validateRoots(contract, modelNames, errors);
|
|
73
|
-
validateVariantsAndBases(contract, modelNames, errors);
|
|
74
|
-
validateRelationTargets(contract, modelNames, errors);
|
|
75
|
-
validateDiscriminators(contract, errors);
|
|
76
|
-
validateOwnership(contract, modelNames, errors);
|
|
77
|
-
validateValueObjectReferences(contract, errors);
|
|
78
|
-
validateFieldModifiers(contract, errors);
|
|
79
|
-
if (errors.length > 0) throw new ContractValidationError(`Contract domain validation failed:\n- ${errors.join("\n- ")}`, "domain");
|
|
80
|
-
}
|
|
81
|
-
function validateRoots(contract, modelNames, errors) {
|
|
82
|
-
const seenValues = /* @__PURE__ */ new Set();
|
|
83
|
-
for (const [rootKey, modelName] of Object.entries(contract.roots)) {
|
|
84
|
-
if (seenValues.has(modelName)) errors.push(`Duplicate root value: "${modelName}" is mapped by multiple root keys`);
|
|
85
|
-
seenValues.add(modelName);
|
|
86
|
-
if (!modelNames.has(modelName)) errors.push(`Root "${rootKey}" references model "${modelName}" which does not exist in models`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function validateVariantsAndBases(contract, modelNames, errors) {
|
|
90
|
-
const models = new Map(Object.entries(contract.models));
|
|
91
|
-
for (const [modelName, model] of models) {
|
|
92
|
-
if (model.variants) for (const variantName of Object.keys(model.variants)) {
|
|
93
|
-
if (!modelNames.has(variantName)) {
|
|
94
|
-
errors.push(`Model "${modelName}" lists variant "${variantName}" which does not exist in models`);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const variantModel = models.get(variantName);
|
|
98
|
-
if (!variantModel) continue;
|
|
99
|
-
if (variantModel.base !== modelName) errors.push(`Variant "${variantName}" has base "${variantModel.base ?? "(none)"}" but expected "${modelName}"`);
|
|
100
|
-
}
|
|
101
|
-
if (model.base) {
|
|
102
|
-
if (!modelNames.has(model.base)) {
|
|
103
|
-
errors.push(`Model "${modelName}" has base "${model.base}" which does not exist in models`);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const baseModel = models.get(model.base);
|
|
107
|
-
if (!baseModel) continue;
|
|
108
|
-
if (!baseModel.variants || !Object.hasOwn(baseModel.variants, modelName)) errors.push(`Model "${modelName}" has base "${model.base}" which does not list it as a variant`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function validateRelationTargets(contract, modelNames, errors) {
|
|
113
|
-
for (const [modelName, model] of Object.entries(contract.models)) for (const [relName, relation] of Object.entries(model.relations ?? {})) if (!modelNames.has(relation.to)) errors.push(`Relation "${relName}" on model "${modelName}" targets "${relation.to}" which does not exist in models`);
|
|
114
|
-
}
|
|
115
|
-
function validateDiscriminators(contract, errors) {
|
|
116
|
-
for (const [modelName, model] of Object.entries(contract.models)) {
|
|
117
|
-
if (model.discriminator) {
|
|
118
|
-
if (!model.variants || Object.keys(model.variants).length === 0) errors.push(`Model "${modelName}" has discriminator but no variants`);
|
|
119
|
-
if (!Object.hasOwn(model.fields, model.discriminator.field)) errors.push(`Discriminator field "${model.discriminator.field}" is not a field on model "${modelName}"`);
|
|
120
|
-
}
|
|
121
|
-
if (model.variants && Object.keys(model.variants).length > 0 && !model.discriminator) errors.push(`Model "${modelName}" has variants but no discriminator`);
|
|
122
|
-
if (model.base) {
|
|
123
|
-
if (model.discriminator) errors.push(`Model "${modelName}" has base and must not have discriminator`);
|
|
124
|
-
if (model.variants && Object.keys(model.variants).length > 0) errors.push(`Model "${modelName}" has base and must not have variants`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
function validateOwnership(contract, modelNames, errors) {
|
|
129
|
-
for (const [modelName, model] of Object.entries(contract.models)) {
|
|
130
|
-
if (!model.owner) continue;
|
|
131
|
-
if (model.owner === modelName) errors.push(`Model "${modelName}" cannot own itself`);
|
|
132
|
-
if (!modelNames.has(model.owner)) errors.push(`Model "${modelName}" has owner "${model.owner}" which does not exist in models`);
|
|
133
|
-
for (const [rootKey, rootModel] of Object.entries(contract.roots)) if (rootModel === modelName) errors.push(`Owned model "${modelName}" must not appear in roots (found as root "${rootKey}")`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function forEachContractField(contract, callback) {
|
|
137
|
-
for (const [modelName, model] of Object.entries(contract.models)) for (const [fieldName, field] of Object.entries(model.fields)) callback(field, `Model "${modelName}" field "${fieldName}"`);
|
|
138
|
-
for (const [voName, vo] of Object.entries(contract.valueObjects ?? {})) for (const [fieldName, field] of Object.entries(vo.fields)) callback(field, `Value object "${voName}" field "${fieldName}"`);
|
|
139
|
-
}
|
|
140
|
-
function validateValueObjectReferences(contract, errors) {
|
|
141
|
-
const voNames = new Set(Object.keys(contract.valueObjects ?? {}));
|
|
142
|
-
function checkType(type, location) {
|
|
143
|
-
if (!type) return;
|
|
144
|
-
if (type.kind === "valueObject" && type.name && !voNames.has(type.name)) {
|
|
145
|
-
errors.push(`${location} references value object "${type.name}" which does not exist in valueObjects`);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (type.kind === "union") for (const member of type.members ?? []) checkType(member, location);
|
|
149
|
-
}
|
|
150
|
-
forEachContractField(contract, (field, location) => {
|
|
151
|
-
checkType(field?.type, location);
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
function validateFieldModifiers(contract, errors) {
|
|
155
|
-
forEachContractField(contract, (field, location) => {
|
|
156
|
-
const f = field;
|
|
157
|
-
if (f?.many && f?.dict) errors.push(`${location} cannot have both "many" and "dict" modifiers`);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
//#endregion
|
|
161
|
-
export { ContractValidationError as n, validateContract as r, validateContractDomain as t };
|
|
162
|
-
|
|
163
|
-
//# sourceMappingURL=validate-domain-WtPdBLia.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"validate-domain-WtPdBLia.mjs","names":["f"],"sources":["../src/validate-contract.ts","../src/validate-domain.ts"],"sourcesContent":["import { type } from 'arktype';\nimport type { Contract } from './contract-types';\nimport type { DomainContractShape } from './validate-domain';\nimport { validateContractDomain } from './validate-domain';\n\nexport type ContractValidationPhase = 'structural' | 'domain' | 'storage';\n\nexport class ContractValidationError extends Error {\n readonly code = 'CONTRACT.VALIDATION_FAILED';\n readonly phase: ContractValidationPhase;\n\n constructor(message: string, phase: ContractValidationPhase) {\n super(message);\n this.name = 'ContractValidationError';\n this.phase = phase;\n }\n}\n\n/**\n * Family-provided storage validator.\n * SQL validates tables/columns/FKs; Mongo validates collections/embedding.\n */\nexport type StorageValidator = (contract: Contract) => void;\n\nconst ContractSchema = type({\n target: 'string',\n targetFamily: 'string',\n roots: 'Record<string, string>',\n models: 'Record<string, unknown>',\n 'valueObjects?': 'Record<string, unknown>',\n storage: 'Record<string, unknown>',\n capabilities: 'Record<string, Record<string, boolean>>',\n extensionPacks: 'Record<string, unknown>',\n meta: 'Record<string, unknown>',\n 'execution?': {\n 'executionHash?': 'string',\n mutations: {\n defaults: 'unknown[]',\n },\n },\n profileHash: 'string',\n});\n\nfunction stripPersistenceFields(raw: Record<string, unknown>): Record<string, unknown> {\n const { schemaVersion: _, _generated: _g, ...rest } = raw;\n return rest;\n}\n\nfunction extractDomainShape(contract: Contract): DomainContractShape {\n return {\n roots: contract.roots,\n models: contract.models,\n ...(contract.valueObjects ? { valueObjects: contract.valueObjects } : {}),\n };\n}\n\n/**\n * Framework-level contract validation (ADR 182).\n *\n * Three-pass validation:\n * 1. **Structural validation** (arktype): verifies required fields exist with\n * correct base types.\n * 2. **Domain validation** (framework-owned): roots, relation targets,\n * variant/base consistency, discriminators, ownership, orphans.\n * 3. **Storage validation** (family-provided): SQL validates tables/columns/FKs;\n * Mongo validates collections/embedding.\n *\n * JSON persistence fields (`schemaVersion`, `_generated`) are stripped before\n * validation — they are not part of the in-memory contract representation.\n *\n * @template TContract The fully-typed contract type (preserves literal types).\n * @param value Raw contract value (e.g. parsed from JSON).\n * @param storageValidator Family-specific storage validation function.\n * @returns The validated contract with full literal types.\n */\nexport function validateContract<TContract extends Contract>(\n value: unknown,\n storageValidator: StorageValidator,\n): TContract {\n if (typeof value !== 'object' || value === null) {\n throw new ContractValidationError('Contract must be a non-null object', 'structural');\n }\n\n const stripped = stripPersistenceFields(value as Record<string, unknown>);\n\n const parsed = ContractSchema(stripped);\n if (parsed instanceof type.errors) {\n throw new ContractValidationError(\n `Invalid contract structure: ${parsed.summary}`,\n 'structural',\n );\n }\n\n const contract = parsed as unknown as Contract;\n\n validateContractDomain(extractDomainShape(contract));\n\n storageValidator(contract);\n\n return contract as unknown as TContract;\n}\n","import { ContractValidationError } from './validate-contract';\n\nexport interface DomainModelShape {\n readonly fields: Record<string, unknown>;\n readonly relations?: Record<string, { readonly to: string }>;\n readonly discriminator?: { readonly field: string };\n readonly variants?: Record<string, unknown>;\n readonly base?: string;\n readonly owner?: string;\n}\n\nexport interface DomainContractShape {\n readonly roots: Record<string, string>;\n readonly models: Record<string, DomainModelShape>;\n readonly valueObjects?: Record<string, { readonly fields: Record<string, unknown> }>;\n}\n\nexport function validateContractDomain(contract: DomainContractShape): void {\n const errors: string[] = [];\n const modelNames = new Set(Object.keys(contract.models));\n\n validateRoots(contract, modelNames, errors);\n validateVariantsAndBases(contract, modelNames, errors);\n validateRelationTargets(contract, modelNames, errors);\n validateDiscriminators(contract, errors);\n validateOwnership(contract, modelNames, errors);\n validateValueObjectReferences(contract, errors);\n validateFieldModifiers(contract, errors);\n\n if (errors.length > 0) {\n throw new ContractValidationError(\n `Contract domain validation failed:\\n- ${errors.join('\\n- ')}`,\n 'domain',\n );\n }\n}\n\nfunction validateRoots(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n const seenValues = new Set<string>();\n for (const [rootKey, modelName] of Object.entries(contract.roots)) {\n if (seenValues.has(modelName)) {\n errors.push(`Duplicate root value: \"${modelName}\" is mapped by multiple root keys`);\n }\n seenValues.add(modelName);\n\n if (!modelNames.has(modelName)) {\n errors.push(\n `Root \"${rootKey}\" references model \"${modelName}\" which does not exist in models`,\n );\n }\n }\n}\n\nfunction validateVariantsAndBases(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n const models = new Map(Object.entries(contract.models));\n\n for (const [modelName, model] of models) {\n if (model.variants) {\n for (const variantName of Object.keys(model.variants)) {\n if (!modelNames.has(variantName)) {\n errors.push(\n `Model \"${modelName}\" lists variant \"${variantName}\" which does not exist in models`,\n );\n continue;\n }\n const variantModel = models.get(variantName);\n if (!variantModel) continue;\n if (variantModel.base !== modelName) {\n errors.push(\n `Variant \"${variantName}\" has base \"${variantModel.base ?? '(none)'}\" but expected \"${modelName}\"`,\n );\n }\n }\n }\n\n if (model.base) {\n if (!modelNames.has(model.base)) {\n errors.push(`Model \"${modelName}\" has base \"${model.base}\" which does not exist in models`);\n continue;\n }\n const baseModel = models.get(model.base);\n if (!baseModel) continue;\n if (!baseModel.variants || !Object.hasOwn(baseModel.variants, modelName)) {\n errors.push(\n `Model \"${modelName}\" has base \"${model.base}\" which does not list it as a variant`,\n );\n }\n }\n }\n}\n\nfunction validateRelationTargets(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n for (const [relName, relation] of Object.entries(model.relations ?? {})) {\n if (!modelNames.has(relation.to)) {\n errors.push(\n `Relation \"${relName}\" on model \"${modelName}\" targets \"${relation.to}\" which does not exist in models`,\n );\n }\n }\n }\n}\n\nfunction validateDiscriminators(contract: DomainContractShape, errors: string[]): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n if (model.discriminator) {\n if (!model.variants || Object.keys(model.variants).length === 0) {\n errors.push(`Model \"${modelName}\" has discriminator but no variants`);\n }\n if (!Object.hasOwn(model.fields, model.discriminator.field)) {\n errors.push(\n `Discriminator field \"${model.discriminator.field}\" is not a field on model \"${modelName}\"`,\n );\n }\n }\n\n if (model.variants && Object.keys(model.variants).length > 0 && !model.discriminator) {\n errors.push(`Model \"${modelName}\" has variants but no discriminator`);\n }\n\n if (model.base) {\n if (model.discriminator) {\n errors.push(`Model \"${modelName}\" has base and must not have discriminator`);\n }\n if (model.variants && Object.keys(model.variants).length > 0) {\n errors.push(`Model \"${modelName}\" has base and must not have variants`);\n }\n }\n }\n}\n\nfunction validateOwnership(\n contract: DomainContractShape,\n modelNames: Set<string>,\n errors: string[],\n): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n if (!model.owner) continue;\n\n if (model.owner === modelName) {\n errors.push(`Model \"${modelName}\" cannot own itself`);\n }\n\n if (!modelNames.has(model.owner)) {\n errors.push(`Model \"${modelName}\" has owner \"${model.owner}\" which does not exist in models`);\n }\n\n for (const [rootKey, rootModel] of Object.entries(contract.roots)) {\n if (rootModel === modelName) {\n errors.push(\n `Owned model \"${modelName}\" must not appear in roots (found as root \"${rootKey}\")`,\n );\n }\n }\n }\n}\n\ninterface FieldTypeLike {\n readonly kind?: string;\n readonly name?: string;\n readonly members?: readonly FieldTypeLike[];\n}\n\ninterface FieldLike {\n readonly type?: FieldTypeLike;\n readonly many?: boolean;\n readonly dict?: boolean;\n}\n\nfunction forEachContractField(\n contract: DomainContractShape,\n callback: (field: unknown, location: string) => void,\n): void {\n for (const [modelName, model] of Object.entries(contract.models)) {\n for (const [fieldName, field] of Object.entries(model.fields)) {\n callback(field, `Model \"${modelName}\" field \"${fieldName}\"`);\n }\n }\n for (const [voName, vo] of Object.entries(contract.valueObjects ?? {})) {\n for (const [fieldName, field] of Object.entries(vo.fields)) {\n callback(field, `Value object \"${voName}\" field \"${fieldName}\"`);\n }\n }\n}\n\nfunction validateValueObjectReferences(contract: DomainContractShape, errors: string[]): void {\n const voNames = new Set(Object.keys(contract.valueObjects ?? {}));\n\n function checkType(type: FieldTypeLike | undefined, location: string): void {\n if (!type) return;\n if (type.kind === 'valueObject' && type.name && !voNames.has(type.name)) {\n errors.push(\n `${location} references value object \"${type.name}\" which does not exist in valueObjects`,\n );\n return;\n }\n if (type.kind === 'union') {\n for (const member of type.members ?? []) checkType(member, location);\n }\n }\n\n forEachContractField(contract, (field, location) => {\n const f = field as FieldLike | undefined;\n checkType(f?.type, location);\n });\n}\n\nfunction validateFieldModifiers(contract: DomainContractShape, errors: string[]): void {\n forEachContractField(contract, (field, location) => {\n const f = field as FieldLike | undefined;\n if (f?.many && f?.dict) {\n errors.push(`${location} cannot have both \"many\" and \"dict\" modifiers`);\n }\n });\n}\n"],"mappings":";;AAOA,IAAa,0BAAb,cAA6C,MAAM;CACjD,OAAgB;CAChB;CAEA,YAAY,SAAiB,OAAgC;EAC3D,MAAM,QAAQ;EACd,KAAK,OAAO;EACZ,KAAK,QAAQ;;;AAUjB,MAAM,iBAAiB,KAAK;CAC1B,QAAQ;CACR,cAAc;CACd,OAAO;CACP,QAAQ;CACR,iBAAiB;CACjB,SAAS;CACT,cAAc;CACd,gBAAgB;CAChB,MAAM;CACN,cAAc;EACZ,kBAAkB;EAClB,WAAW,EACT,UAAU,aACX;EACF;CACD,aAAa;CACd,CAAC;AAEF,SAAS,uBAAuB,KAAuD;CACrF,MAAM,EAAE,eAAe,GAAG,YAAY,IAAI,GAAG,SAAS;CACtD,OAAO;;AAGT,SAAS,mBAAmB,UAAyC;CACnE,OAAO;EACL,OAAO,SAAS;EAChB,QAAQ,SAAS;EACjB,GAAI,SAAS,eAAe,EAAE,cAAc,SAAS,cAAc,GAAG,EAAE;EACzE;;;;;;;;;;;;;;;;;;;;;AAsBH,SAAgB,iBACd,OACA,kBACW;CACX,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,MAAM,IAAI,wBAAwB,sCAAsC,aAAa;CAKvF,MAAM,SAAS,eAFE,uBAAuB,MAEF,CAAC;CACvC,IAAI,kBAAkB,KAAK,QACzB,MAAM,IAAI,wBACR,+BAA+B,OAAO,WACtC,aACD;CAGH,MAAM,WAAW;CAEjB,uBAAuB,mBAAmB,SAAS,CAAC;CAEpD,iBAAiB,SAAS;CAE1B,OAAO;;;;AClFT,SAAgB,uBAAuB,UAAqC;CAC1E,MAAM,SAAmB,EAAE;CAC3B,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,SAAS,OAAO,CAAC;CAExD,cAAc,UAAU,YAAY,OAAO;CAC3C,yBAAyB,UAAU,YAAY,OAAO;CACtD,wBAAwB,UAAU,YAAY,OAAO;CACrD,uBAAuB,UAAU,OAAO;CACxC,kBAAkB,UAAU,YAAY,OAAO;CAC/C,8BAA8B,UAAU,OAAO;CAC/C,uBAAuB,UAAU,OAAO;CAExC,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,wBACR,yCAAyC,OAAO,KAAK,OAAO,IAC5D,SACD;;AAIL,SAAS,cACP,UACA,YACA,QACM;CACN,MAAM,6BAAa,IAAI,KAAa;CACpC,KAAK,MAAM,CAAC,SAAS,cAAc,OAAO,QAAQ,SAAS,MAAM,EAAE;EACjE,IAAI,WAAW,IAAI,UAAU,EAC3B,OAAO,KAAK,0BAA0B,UAAU,mCAAmC;EAErF,WAAW,IAAI,UAAU;EAEzB,IAAI,CAAC,WAAW,IAAI,UAAU,EAC5B,OAAO,KACL,SAAS,QAAQ,sBAAsB,UAAU,kCAClD;;;AAKP,SAAS,yBACP,UACA,YACA,QACM;CACN,MAAM,SAAS,IAAI,IAAI,OAAO,QAAQ,SAAS,OAAO,CAAC;CAEvD,KAAK,MAAM,CAAC,WAAW,UAAU,QAAQ;EACvC,IAAI,MAAM,UACR,KAAK,MAAM,eAAe,OAAO,KAAK,MAAM,SAAS,EAAE;GACrD,IAAI,CAAC,WAAW,IAAI,YAAY,EAAE;IAChC,OAAO,KACL,UAAU,UAAU,mBAAmB,YAAY,kCACpD;IACD;;GAEF,MAAM,eAAe,OAAO,IAAI,YAAY;GAC5C,IAAI,CAAC,cAAc;GACnB,IAAI,aAAa,SAAS,WACxB,OAAO,KACL,YAAY,YAAY,cAAc,aAAa,QAAQ,SAAS,kBAAkB,UAAU,GACjG;;EAKP,IAAI,MAAM,MAAM;GACd,IAAI,CAAC,WAAW,IAAI,MAAM,KAAK,EAAE;IAC/B,OAAO,KAAK,UAAU,UAAU,cAAc,MAAM,KAAK,kCAAkC;IAC3F;;GAEF,MAAM,YAAY,OAAO,IAAI,MAAM,KAAK;GACxC,IAAI,CAAC,WAAW;GAChB,IAAI,CAAC,UAAU,YAAY,CAAC,OAAO,OAAO,UAAU,UAAU,UAAU,EACtE,OAAO,KACL,UAAU,UAAU,cAAc,MAAM,KAAK,uCAC9C;;;;AAMT,SAAS,wBACP,UACA,YACA,QACM;CACN,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,OAAO,EAC9D,KAAK,MAAM,CAAC,SAAS,aAAa,OAAO,QAAQ,MAAM,aAAa,EAAE,CAAC,EACrE,IAAI,CAAC,WAAW,IAAI,SAAS,GAAG,EAC9B,OAAO,KACL,aAAa,QAAQ,cAAc,UAAU,aAAa,SAAS,GAAG,kCACvE;;AAMT,SAAS,uBAAuB,UAA+B,QAAwB;CACrF,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,OAAO,EAAE;EAChE,IAAI,MAAM,eAAe;GACvB,IAAI,CAAC,MAAM,YAAY,OAAO,KAAK,MAAM,SAAS,CAAC,WAAW,GAC5D,OAAO,KAAK,UAAU,UAAU,qCAAqC;GAEvE,IAAI,CAAC,OAAO,OAAO,MAAM,QAAQ,MAAM,cAAc,MAAM,EACzD,OAAO,KACL,wBAAwB,MAAM,cAAc,MAAM,6BAA6B,UAAU,GAC1F;;EAIL,IAAI,MAAM,YAAY,OAAO,KAAK,MAAM,SAAS,CAAC,SAAS,KAAK,CAAC,MAAM,eACrE,OAAO,KAAK,UAAU,UAAU,qCAAqC;EAGvE,IAAI,MAAM,MAAM;GACd,IAAI,MAAM,eACR,OAAO,KAAK,UAAU,UAAU,4CAA4C;GAE9E,IAAI,MAAM,YAAY,OAAO,KAAK,MAAM,SAAS,CAAC,SAAS,GACzD,OAAO,KAAK,UAAU,UAAU,uCAAuC;;;;AAM/E,SAAS,kBACP,UACA,YACA,QACM;CACN,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,OAAO,EAAE;EAChE,IAAI,CAAC,MAAM,OAAO;EAElB,IAAI,MAAM,UAAU,WAClB,OAAO,KAAK,UAAU,UAAU,qBAAqB;EAGvD,IAAI,CAAC,WAAW,IAAI,MAAM,MAAM,EAC9B,OAAO,KAAK,UAAU,UAAU,eAAe,MAAM,MAAM,kCAAkC;EAG/F,KAAK,MAAM,CAAC,SAAS,cAAc,OAAO,QAAQ,SAAS,MAAM,EAC/D,IAAI,cAAc,WAChB,OAAO,KACL,gBAAgB,UAAU,6CAA6C,QAAQ,IAChF;;;AAkBT,SAAS,qBACP,UACA,UACM;CACN,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,SAAS,OAAO,EAC9D,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,MAAM,OAAO,EAC3D,SAAS,OAAO,UAAU,UAAU,WAAW,UAAU,GAAG;CAGhE,KAAK,MAAM,CAAC,QAAQ,OAAO,OAAO,QAAQ,SAAS,gBAAgB,EAAE,CAAC,EACpE,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,GAAG,OAAO,EACxD,SAAS,OAAO,iBAAiB,OAAO,WAAW,UAAU,GAAG;;AAKtE,SAAS,8BAA8B,UAA+B,QAAwB;CAC5F,MAAM,UAAU,IAAI,IAAI,OAAO,KAAK,SAAS,gBAAgB,EAAE,CAAC,CAAC;CAEjE,SAAS,UAAU,MAAiC,UAAwB;EAC1E,IAAI,CAAC,MAAM;EACX,IAAI,KAAK,SAAS,iBAAiB,KAAK,QAAQ,CAAC,QAAQ,IAAI,KAAK,KAAK,EAAE;GACvE,OAAO,KACL,GAAG,SAAS,4BAA4B,KAAK,KAAK,wCACnD;GACD;;EAEF,IAAI,KAAK,SAAS,SAChB,KAAK,MAAM,UAAU,KAAK,WAAW,EAAE,EAAE,UAAU,QAAQ,SAAS;;CAIxE,qBAAqB,WAAW,OAAO,aAAa;EAElD,UAAUA,OAAG,MAAM,SAAS;GAC5B;;AAGJ,SAAS,uBAAuB,UAA+B,QAAwB;CACrF,qBAAqB,WAAW,OAAO,aAAa;EAClD,MAAM,IAAI;EACV,IAAI,GAAG,QAAQ,GAAG,MAChB,OAAO,KAAK,GAAG,SAAS,+CAA+C;GAEzE"}
|
package/src/validate-contract.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { type } from 'arktype';
|
|
2
|
-
import type { Contract } from './contract-types';
|
|
3
|
-
import type { DomainContractShape } from './validate-domain';
|
|
4
|
-
import { validateContractDomain } from './validate-domain';
|
|
5
|
-
|
|
6
|
-
export type ContractValidationPhase = 'structural' | 'domain' | 'storage';
|
|
7
|
-
|
|
8
|
-
export class ContractValidationError extends Error {
|
|
9
|
-
readonly code = 'CONTRACT.VALIDATION_FAILED';
|
|
10
|
-
readonly phase: ContractValidationPhase;
|
|
11
|
-
|
|
12
|
-
constructor(message: string, phase: ContractValidationPhase) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.name = 'ContractValidationError';
|
|
15
|
-
this.phase = phase;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Family-provided storage validator.
|
|
21
|
-
* SQL validates tables/columns/FKs; Mongo validates collections/embedding.
|
|
22
|
-
*/
|
|
23
|
-
export type StorageValidator = (contract: Contract) => void;
|
|
24
|
-
|
|
25
|
-
const ContractSchema = type({
|
|
26
|
-
target: 'string',
|
|
27
|
-
targetFamily: 'string',
|
|
28
|
-
roots: 'Record<string, string>',
|
|
29
|
-
models: 'Record<string, unknown>',
|
|
30
|
-
'valueObjects?': 'Record<string, unknown>',
|
|
31
|
-
storage: 'Record<string, unknown>',
|
|
32
|
-
capabilities: 'Record<string, Record<string, boolean>>',
|
|
33
|
-
extensionPacks: 'Record<string, unknown>',
|
|
34
|
-
meta: 'Record<string, unknown>',
|
|
35
|
-
'execution?': {
|
|
36
|
-
'executionHash?': 'string',
|
|
37
|
-
mutations: {
|
|
38
|
-
defaults: 'unknown[]',
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
profileHash: 'string',
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
function stripPersistenceFields(raw: Record<string, unknown>): Record<string, unknown> {
|
|
45
|
-
const { schemaVersion: _, _generated: _g, ...rest } = raw;
|
|
46
|
-
return rest;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function extractDomainShape(contract: Contract): DomainContractShape {
|
|
50
|
-
return {
|
|
51
|
-
roots: contract.roots,
|
|
52
|
-
models: contract.models,
|
|
53
|
-
...(contract.valueObjects ? { valueObjects: contract.valueObjects } : {}),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Framework-level contract validation (ADR 182).
|
|
59
|
-
*
|
|
60
|
-
* Three-pass validation:
|
|
61
|
-
* 1. **Structural validation** (arktype): verifies required fields exist with
|
|
62
|
-
* correct base types.
|
|
63
|
-
* 2. **Domain validation** (framework-owned): roots, relation targets,
|
|
64
|
-
* variant/base consistency, discriminators, ownership, orphans.
|
|
65
|
-
* 3. **Storage validation** (family-provided): SQL validates tables/columns/FKs;
|
|
66
|
-
* Mongo validates collections/embedding.
|
|
67
|
-
*
|
|
68
|
-
* JSON persistence fields (`schemaVersion`, `_generated`) are stripped before
|
|
69
|
-
* validation — they are not part of the in-memory contract representation.
|
|
70
|
-
*
|
|
71
|
-
* @template TContract The fully-typed contract type (preserves literal types).
|
|
72
|
-
* @param value Raw contract value (e.g. parsed from JSON).
|
|
73
|
-
* @param storageValidator Family-specific storage validation function.
|
|
74
|
-
* @returns The validated contract with full literal types.
|
|
75
|
-
*/
|
|
76
|
-
export function validateContract<TContract extends Contract>(
|
|
77
|
-
value: unknown,
|
|
78
|
-
storageValidator: StorageValidator,
|
|
79
|
-
): TContract {
|
|
80
|
-
if (typeof value !== 'object' || value === null) {
|
|
81
|
-
throw new ContractValidationError('Contract must be a non-null object', 'structural');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const stripped = stripPersistenceFields(value as Record<string, unknown>);
|
|
85
|
-
|
|
86
|
-
const parsed = ContractSchema(stripped);
|
|
87
|
-
if (parsed instanceof type.errors) {
|
|
88
|
-
throw new ContractValidationError(
|
|
89
|
-
`Invalid contract structure: ${parsed.summary}`,
|
|
90
|
-
'structural',
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const contract = parsed as unknown as Contract;
|
|
95
|
-
|
|
96
|
-
validateContractDomain(extractDomainShape(contract));
|
|
97
|
-
|
|
98
|
-
storageValidator(contract);
|
|
99
|
-
|
|
100
|
-
return contract as unknown as TContract;
|
|
101
|
-
}
|