@prisma-next/contract 0.3.0-dev.143 → 0.3.0-dev.145
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/{types-D6WADlh6.d.mts → contract-types-Cd9ic3zL.d.mts} +46 -46
- package/dist/contract-types-Cd9ic3zL.d.mts.map +1 -0
- package/dist/{hashing-vJBMVN8S.mjs → hashing-BHjb-IPM.mjs} +36 -59
- package/dist/hashing-BHjb-IPM.mjs.map +1 -0
- package/dist/hashing.d.mts +8 -19
- package/dist/hashing.d.mts.map +1 -1
- package/dist/hashing.mjs +1 -1
- package/dist/testing.d.mts +1 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +2 -2
- package/dist/{types-DYikGC04.mjs → types-BP4dqfP-.mjs} +2 -8
- package/dist/types-BP4dqfP-.mjs.map +1 -0
- package/dist/types.d.mts +2 -3
- package/dist/types.mjs +2 -2
- package/dist/validate-contract.d.mts +10 -8
- package/dist/validate-contract.d.mts.map +1 -1
- package/dist/validate-contract.mjs +2 -60
- package/dist/{validate-domain-CTQiBiei.mjs → validate-domain-BlwWyv01.mjs} +70 -17
- package/dist/validate-domain-BlwWyv01.mjs.map +1 -0
- package/dist/validate-domain.d.mts +2 -5
- package/dist/validate-domain.d.mts.map +1 -1
- package/dist/validate-domain.mjs +1 -1
- package/package.json +3 -3
- package/src/canonicalization.ts +24 -49
- package/src/domain-types.ts +0 -17
- package/src/exports/hashing.ts +1 -5
- package/src/exports/types.ts +0 -12
- package/src/exports/validate-contract.ts +2 -1
- package/src/exports/validate-domain.ts +0 -1
- package/src/hashing.ts +21 -37
- package/src/types.ts +0 -50
- package/src/validate-contract.ts +25 -19
- package/src/validate-domain.ts +7 -47
- package/dist/contract-types-d-PtSVGD.d.mts +0 -49
- package/dist/contract-types-d-PtSVGD.d.mts.map +0 -1
- package/dist/hashing-vJBMVN8S.mjs.map +0 -1
- package/dist/types-D6WADlh6.d.mts.map +0 -1
- package/dist/types-DYikGC04.mjs.map +0 -1
- package/dist/validate-contract.mjs.map +0 -1
- package/dist/validate-domain-CTQiBiei.mjs.map +0 -1
|
@@ -1,16 +1,80 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
//#region src/validate-contract.ts
|
|
4
|
+
var ContractValidationError = class extends Error {
|
|
5
|
+
code = "CONTRACT.VALIDATION_FAILED";
|
|
6
|
+
phase;
|
|
7
|
+
constructor(message, phase) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ContractValidationError";
|
|
10
|
+
this.phase = phase;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const ContractSchema = type({
|
|
14
|
+
target: "string",
|
|
15
|
+
targetFamily: "string",
|
|
16
|
+
roots: "Record<string, string>",
|
|
17
|
+
models: "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
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Framework-level contract validation (ADR 182).
|
|
40
|
+
*
|
|
41
|
+
* Three-pass validation:
|
|
42
|
+
* 1. **Structural validation** (arktype): verifies required fields exist with
|
|
43
|
+
* correct base types.
|
|
44
|
+
* 2. **Domain validation** (framework-owned): roots, relation targets,
|
|
45
|
+
* variant/base consistency, discriminators, ownership, orphans.
|
|
46
|
+
* 3. **Storage validation** (family-provided): SQL validates tables/columns/FKs;
|
|
47
|
+
* Mongo validates collections/embedding.
|
|
48
|
+
*
|
|
49
|
+
* JSON persistence fields (`schemaVersion`, `_generated`) are stripped before
|
|
50
|
+
* validation — they are not part of the in-memory contract representation.
|
|
51
|
+
*
|
|
52
|
+
* @template TContract The fully-typed contract type (preserves literal types).
|
|
53
|
+
* @param value Raw contract value (e.g. parsed from JSON).
|
|
54
|
+
* @param storageValidator Family-specific storage validation function.
|
|
55
|
+
* @returns The validated contract with full literal types.
|
|
56
|
+
*/
|
|
57
|
+
function validateContract(value, storageValidator) {
|
|
58
|
+
if (typeof value !== "object" || value === null) throw new ContractValidationError("Contract must be a non-null object", "structural");
|
|
59
|
+
const parsed = ContractSchema(stripPersistenceFields(value));
|
|
60
|
+
if (parsed instanceof type.errors) throw new ContractValidationError(`Invalid contract structure: ${parsed.summary}`, "structural");
|
|
61
|
+
const contract = parsed;
|
|
62
|
+
validateContractDomain(extractDomainShape(contract));
|
|
63
|
+
storageValidator(contract);
|
|
64
|
+
return contract;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
1
68
|
//#region src/validate-domain.ts
|
|
2
69
|
function validateContractDomain(contract) {
|
|
3
70
|
const errors = [];
|
|
4
|
-
const warnings = [];
|
|
5
71
|
const modelNames = new Set(Object.keys(contract.models));
|
|
6
72
|
validateRoots(contract, modelNames, errors);
|
|
7
73
|
validateVariantsAndBases(contract, modelNames, errors);
|
|
8
74
|
validateRelationTargets(contract, modelNames, errors);
|
|
9
75
|
validateDiscriminators(contract, errors);
|
|
10
76
|
validateOwnership(contract, modelNames, errors);
|
|
11
|
-
|
|
12
|
-
if (errors.length > 0) throw new Error(`Contract domain validation failed:\n- ${errors.join("\n- ")}`);
|
|
13
|
-
return { warnings };
|
|
77
|
+
if (errors.length > 0) throw new ContractValidationError(`Contract domain validation failed:\n- ${errors.join("\n- ")}`, "domain");
|
|
14
78
|
}
|
|
15
79
|
function validateRoots(contract, modelNames, errors) {
|
|
16
80
|
const seenValues = /* @__PURE__ */ new Set();
|
|
@@ -67,18 +131,7 @@ function validateOwnership(contract, modelNames, errors) {
|
|
|
67
131
|
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
132
|
}
|
|
69
133
|
}
|
|
70
|
-
function detectOrphanedModels(contract, modelNames, warnings) {
|
|
71
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
72
|
-
for (const modelName of Object.values(contract.roots)) referenced.add(modelName);
|
|
73
|
-
for (const [modelName, model] of Object.entries(contract.models)) {
|
|
74
|
-
for (const relation of Object.values(model.relations ?? {})) referenced.add(relation.to);
|
|
75
|
-
if (model.variants) for (const variantName of Object.keys(model.variants)) referenced.add(variantName);
|
|
76
|
-
if (model.base) referenced.add(model.base);
|
|
77
|
-
if (model.owner) referenced.add(modelName);
|
|
78
|
-
}
|
|
79
|
-
for (const modelName of modelNames) if (!referenced.has(modelName)) warnings.push(`Orphaned model: "${modelName}" is not referenced by any root, relation, or variant`);
|
|
80
|
-
}
|
|
81
134
|
|
|
82
135
|
//#endregion
|
|
83
|
-
export { validateContractDomain as t };
|
|
84
|
-
//# sourceMappingURL=validate-domain-
|
|
136
|
+
export { ContractValidationError as n, validateContract as r, validateContractDomain as t };
|
|
137
|
+
//# sourceMappingURL=validate-domain-BlwWyv01.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-domain-BlwWyv01.mjs","names":["errors: string[]"],"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 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 };\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}\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\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"],"mappings":"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"}
|
|
@@ -15,10 +15,7 @@ interface DomainContractShape {
|
|
|
15
15
|
readonly roots: Record<string, string>;
|
|
16
16
|
readonly models: Record<string, DomainModelShape>;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
readonly warnings: string[];
|
|
20
|
-
}
|
|
21
|
-
declare function validateContractDomain(contract: DomainContractShape): DomainValidationResult;
|
|
18
|
+
declare function validateContractDomain(contract: DomainContractShape): void;
|
|
22
19
|
//#endregion
|
|
23
|
-
export { type DomainContractShape, type DomainModelShape,
|
|
20
|
+
export { type DomainContractShape, type DomainModelShape, validateContractDomain };
|
|
24
21
|
//# sourceMappingURL=validate-domain.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-domain.d.mts","names":[],"sources":["../src/validate-domain.ts"],"sourcesContent":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"validate-domain.d.mts","names":[],"sources":["../src/validate-domain.ts"],"sourcesContent":[],"mappings":";UAEiB,gBAAA;EAAA,SAAA,MAAA,EACE,MADc,CAAA,MAAA,EAAA,OAAA,CAAA;EACd,SAAA,SAAA,CAAA,EACI,MADJ,CAAA,MAAA,EAAA;IACI,SAAA,EAAA,EAAA,MAAA;EAED,CAAA,CAAA;EAAM,SAAA,aAAA,CAAA,EAAA;IAKX,SAAA,KAAA,EAAA,MAAmB;EAClB,CAAA;EACgB,SAAA,QAAA,CAAA,EAPZ,MAOY,CAAA,MAAA,EAAA,OAAA,CAAA;EAAf,SAAA,IAAA,CAAA,EAAA,MAAA;EAAM,SAAA,KAAA,CAAA,EAAA,MAAA;AAGzB;UALiB,mBAAA;kBACC;mBACC,eAAe;;iBAGlB,sBAAA,WAAiC"}
|
package/dist/validate-domain.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/contract",
|
|
3
|
-
"version": "0.3.0-dev.
|
|
3
|
+
"version": "0.3.0-dev.145",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Data contract type definitions and JSON schema for Prisma Next",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"arktype": "^2.1.29",
|
|
9
|
-
"@prisma-next/utils": "0.3.0-dev.
|
|
9
|
+
"@prisma-next/utils": "0.3.0-dev.145"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"tsdown": "0.18.4",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
37
|
"url": "https://github.com/prisma/prisma-next.git",
|
|
38
|
-
"directory": "packages/1-framework/0-foundation/
|
|
38
|
+
"directory": "packages/1-framework/0-foundation/contract"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsdown",
|
package/src/canonicalization.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isArrayEqual } from '@prisma-next/utils/array-equal';
|
|
2
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
3
|
|
|
3
|
-
import type {
|
|
4
|
+
import type { Contract } from './contract-types';
|
|
4
5
|
import { bigintJsonReplacer } from './types';
|
|
5
6
|
|
|
6
7
|
const TOP_LEVEL_ORDER = [
|
|
@@ -8,8 +9,6 @@ const TOP_LEVEL_ORDER = [
|
|
|
8
9
|
'canonicalVersion',
|
|
9
10
|
'targetFamily',
|
|
10
11
|
'target',
|
|
11
|
-
'storageHash',
|
|
12
|
-
'executionHash',
|
|
13
12
|
'profileHash',
|
|
14
13
|
'roots',
|
|
15
14
|
'models',
|
|
@@ -54,10 +53,6 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
54
53
|
continue;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
if (key === 'nullable' && value === false) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
56
|
if (key === 'generated' && value === false) {
|
|
62
57
|
continue;
|
|
63
58
|
}
|
|
@@ -115,6 +110,8 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
115
110
|
currentPath[3] === 'foreignKeys' &&
|
|
116
111
|
(key === 'constraint' || key === 'index');
|
|
117
112
|
|
|
113
|
+
const isNullableField = key === 'nullable';
|
|
114
|
+
|
|
118
115
|
if (
|
|
119
116
|
!isRequiredModels &&
|
|
120
117
|
!isRequiredTables &&
|
|
@@ -131,7 +128,8 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
131
128
|
!isTableUniques &&
|
|
132
129
|
!isTableIndexes &&
|
|
133
130
|
!isTableForeignKeys &&
|
|
134
|
-
!isFkBooleanField
|
|
131
|
+
!isFkBooleanField &&
|
|
132
|
+
!isNullableField
|
|
135
133
|
) {
|
|
136
134
|
continue;
|
|
137
135
|
}
|
|
@@ -241,49 +239,23 @@ export function orderTopLevel(obj: Record<string, unknown>): Record<string, unkn
|
|
|
241
239
|
return ordered;
|
|
242
240
|
}
|
|
243
241
|
|
|
244
|
-
export type CanonicalContractInput = {
|
|
245
|
-
readonly schemaVersion?: string | undefined;
|
|
246
|
-
readonly targetFamily: string;
|
|
247
|
-
readonly target: string;
|
|
248
|
-
readonly roots: Record<string, string>;
|
|
249
|
-
readonly models: Record<string, unknown>;
|
|
250
|
-
readonly storage: StorageBase | Record<string, unknown>;
|
|
251
|
-
readonly execution?: Record<string, unknown> | undefined;
|
|
252
|
-
readonly extensionPacks: Record<string, unknown>;
|
|
253
|
-
readonly capabilities: Record<string, Record<string, boolean>>;
|
|
254
|
-
readonly meta: Record<string, unknown>;
|
|
255
|
-
readonly storageHash?: string | undefined;
|
|
256
|
-
readonly executionHash?: string | undefined;
|
|
257
|
-
readonly profileHash?: string | undefined;
|
|
258
|
-
};
|
|
259
|
-
|
|
260
242
|
export function canonicalizeContractToObject(
|
|
261
|
-
|
|
243
|
+
contract: Contract,
|
|
244
|
+
options?: { schemaVersion?: string },
|
|
262
245
|
): Record<string, unknown> {
|
|
263
|
-
const i = input as Record<string, unknown>;
|
|
264
246
|
const normalized: Record<string, unknown> = {
|
|
265
|
-
...(
|
|
266
|
-
targetFamily:
|
|
267
|
-
target:
|
|
268
|
-
|
|
269
|
-
roots:
|
|
270
|
-
models:
|
|
271
|
-
storage:
|
|
272
|
-
...(
|
|
273
|
-
extensionPacks:
|
|
274
|
-
capabilities:
|
|
275
|
-
meta:
|
|
247
|
+
...ifDefined('schemaVersion', options?.schemaVersion),
|
|
248
|
+
targetFamily: contract.targetFamily,
|
|
249
|
+
target: contract.target,
|
|
250
|
+
profileHash: contract.profileHash,
|
|
251
|
+
roots: contract.roots,
|
|
252
|
+
models: contract.models,
|
|
253
|
+
storage: contract.storage,
|
|
254
|
+
...ifDefined('execution', contract.execution),
|
|
255
|
+
extensionPacks: contract.extensionPacks,
|
|
256
|
+
capabilities: contract.capabilities,
|
|
257
|
+
meta: contract.meta,
|
|
276
258
|
};
|
|
277
|
-
if (i['storageHash'] !== undefined) {
|
|
278
|
-
normalized['storageHash'] = i['storageHash'];
|
|
279
|
-
}
|
|
280
|
-
if (i['executionHash'] !== undefined) {
|
|
281
|
-
normalized['executionHash'] = i['executionHash'];
|
|
282
|
-
}
|
|
283
|
-
if (i['profileHash'] !== undefined) {
|
|
284
|
-
normalized['profileHash'] = i['profileHash'];
|
|
285
|
-
}
|
|
286
|
-
|
|
287
259
|
const withDefaultsOmitted = omitDefaults(normalized, []) as Record<string, unknown>;
|
|
288
260
|
const withSortedIndexes = sortIndexesAndUniques(withDefaultsOmitted['storage']);
|
|
289
261
|
const withSortedStorage = { ...withDefaultsOmitted, storage: withSortedIndexes };
|
|
@@ -291,6 +263,9 @@ export function canonicalizeContractToObject(
|
|
|
291
263
|
return orderTopLevel(withSortedKeys);
|
|
292
264
|
}
|
|
293
265
|
|
|
294
|
-
export function canonicalizeContract(
|
|
295
|
-
|
|
266
|
+
export function canonicalizeContract(
|
|
267
|
+
contract: Contract,
|
|
268
|
+
options?: { schemaVersion?: string },
|
|
269
|
+
): string {
|
|
270
|
+
return JSON.stringify(canonicalizeContractToObject(contract, options), bigintJsonReplacer, 2);
|
|
296
271
|
}
|
package/src/domain-types.ts
CHANGED
|
@@ -43,23 +43,6 @@ export interface ContractModel<TModelStorage extends ModelStorageBase = ModelSto
|
|
|
43
43
|
|
|
44
44
|
// ── Backward-compatible aliases ──────────────────────────────────────────────
|
|
45
45
|
|
|
46
|
-
/** @deprecated Use {@link ContractField} */
|
|
47
|
-
export type DomainField = ContractField;
|
|
48
|
-
/** @deprecated Use {@link ContractRelationOn} */
|
|
49
|
-
export type DomainRelationOn = ContractRelationOn;
|
|
50
|
-
/** @deprecated Use {@link ContractReferenceRelation} */
|
|
51
|
-
export type DomainReferenceRelation = ContractReferenceRelation;
|
|
52
|
-
/** @deprecated Use {@link ContractEmbedRelation} */
|
|
53
|
-
export type DomainEmbedRelation = ContractEmbedRelation;
|
|
54
|
-
/** @deprecated Use {@link ContractRelation} */
|
|
55
|
-
export type DomainRelation = ContractRelation;
|
|
56
|
-
/** @deprecated Use {@link ContractDiscriminator} */
|
|
57
|
-
export type DomainDiscriminator = ContractDiscriminator;
|
|
58
|
-
/** @deprecated Use {@link ContractVariantEntry} */
|
|
59
|
-
export type DomainVariantEntry = ContractVariantEntry;
|
|
60
|
-
/** @deprecated Use {@link ContractModel} */
|
|
61
|
-
export type DomainModel = ContractModel;
|
|
62
|
-
|
|
63
46
|
// ── Relation key helpers ─────────────────────────────────────────────────────
|
|
64
47
|
|
|
65
48
|
type HasModelsWithRelations = {
|
package/src/exports/hashing.ts
CHANGED
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
type CanonicalContractInput,
|
|
3
|
-
canonicalizeContract,
|
|
4
|
-
canonicalizeContractToObject,
|
|
5
|
-
} from '../canonicalization';
|
|
1
|
+
export { canonicalizeContract, canonicalizeContractToObject } from '../canonicalization';
|
|
6
2
|
export { computeExecutionHash, computeProfileHash, computeStorageHash } from '../hashing';
|
package/src/exports/types.ts
CHANGED
|
@@ -8,14 +8,6 @@ export type {
|
|
|
8
8
|
ContractRelation,
|
|
9
9
|
ContractRelationOn,
|
|
10
10
|
ContractVariantEntry,
|
|
11
|
-
DomainDiscriminator,
|
|
12
|
-
DomainEmbedRelation,
|
|
13
|
-
DomainField,
|
|
14
|
-
DomainModel,
|
|
15
|
-
DomainReferenceRelation,
|
|
16
|
-
DomainRelation,
|
|
17
|
-
DomainRelationOn,
|
|
18
|
-
DomainVariantEntry,
|
|
19
11
|
EmbedRelationKeys,
|
|
20
12
|
ModelStorageBase,
|
|
21
13
|
ReferenceRelationKeys,
|
|
@@ -26,12 +18,9 @@ export type {
|
|
|
26
18
|
ColumnDefault,
|
|
27
19
|
ColumnDefaultLiteralInputValue,
|
|
28
20
|
ColumnDefaultLiteralValue,
|
|
29
|
-
ContractBase,
|
|
30
21
|
ContractMarkerRecord,
|
|
31
22
|
DocCollection,
|
|
32
23
|
DocIndex,
|
|
33
|
-
DocumentContract,
|
|
34
|
-
DocumentStorage,
|
|
35
24
|
ExecutionHashBase,
|
|
36
25
|
ExecutionMutationDefault,
|
|
37
26
|
ExecutionMutationDefaultValue,
|
|
@@ -58,7 +47,6 @@ export {
|
|
|
58
47
|
bigintJsonReplacer,
|
|
59
48
|
coreHash,
|
|
60
49
|
executionHash,
|
|
61
|
-
isDocumentContract,
|
|
62
50
|
isTaggedBigInt,
|
|
63
51
|
isTaggedRaw,
|
|
64
52
|
profileHash,
|
package/src/hashing.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canonicalizeContract } from './canonicalization';
|
|
3
|
+
import type { Contract } from './contract-types';
|
|
3
4
|
import type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from './types';
|
|
4
5
|
|
|
5
6
|
const SCHEMA_VERSION = '1';
|
|
@@ -10,23 +11,29 @@ function sha256(content: string): string {
|
|
|
10
11
|
return `sha256:${hash.digest('hex')}`;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
function hashContract(section: Record<string, unknown>): string {
|
|
15
|
+
const contract = {
|
|
16
|
+
targetFamily: section['targetFamily'],
|
|
17
|
+
target: section['target'],
|
|
18
|
+
roots: {},
|
|
19
|
+
models: {},
|
|
20
|
+
storage: section['storage'] ?? {},
|
|
21
|
+
execution: section['execution'],
|
|
22
|
+
extensionPacks: {},
|
|
23
|
+
capabilities: section['capabilities'] ?? {},
|
|
24
|
+
meta: {},
|
|
25
|
+
profileHash: '',
|
|
26
|
+
...section,
|
|
27
|
+
} as Contract;
|
|
28
|
+
return canonicalizeContract(contract, { schemaVersion: SCHEMA_VERSION });
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
export function computeStorageHash(args: {
|
|
14
32
|
target: string;
|
|
15
33
|
targetFamily: string;
|
|
16
34
|
storage: Record<string, unknown>;
|
|
17
35
|
}): StorageHashBase<string> {
|
|
18
|
-
|
|
19
|
-
schemaVersion: SCHEMA_VERSION,
|
|
20
|
-
targetFamily: args.targetFamily,
|
|
21
|
-
target: args.target,
|
|
22
|
-
storage: args.storage,
|
|
23
|
-
roots: {},
|
|
24
|
-
models: {},
|
|
25
|
-
extensionPacks: {},
|
|
26
|
-
capabilities: {},
|
|
27
|
-
meta: {},
|
|
28
|
-
});
|
|
29
|
-
return sha256(canonical) as StorageHashBase<string>;
|
|
36
|
+
return sha256(hashContract(args)) as StorageHashBase<string>;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
export function computeExecutionHash(args: {
|
|
@@ -34,19 +41,7 @@ export function computeExecutionHash(args: {
|
|
|
34
41
|
targetFamily: string;
|
|
35
42
|
execution: Record<string, unknown>;
|
|
36
43
|
}): ExecutionHashBase<string> {
|
|
37
|
-
|
|
38
|
-
schemaVersion: SCHEMA_VERSION,
|
|
39
|
-
targetFamily: args.targetFamily,
|
|
40
|
-
target: args.target,
|
|
41
|
-
execution: args.execution,
|
|
42
|
-
roots: {},
|
|
43
|
-
models: {},
|
|
44
|
-
storage: {},
|
|
45
|
-
extensionPacks: {},
|
|
46
|
-
capabilities: {},
|
|
47
|
-
meta: {},
|
|
48
|
-
});
|
|
49
|
-
return sha256(canonical) as ExecutionHashBase<string>;
|
|
44
|
+
return sha256(hashContract(args)) as ExecutionHashBase<string>;
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
export function computeProfileHash(args: {
|
|
@@ -54,16 +49,5 @@ export function computeProfileHash(args: {
|
|
|
54
49
|
targetFamily: string;
|
|
55
50
|
capabilities: Record<string, Record<string, boolean>>;
|
|
56
51
|
}): ProfileHashBase<string> {
|
|
57
|
-
|
|
58
|
-
schemaVersion: SCHEMA_VERSION,
|
|
59
|
-
targetFamily: args.targetFamily,
|
|
60
|
-
target: args.target,
|
|
61
|
-
capabilities: args.capabilities,
|
|
62
|
-
roots: {},
|
|
63
|
-
models: {},
|
|
64
|
-
storage: {},
|
|
65
|
-
extensionPacks: {},
|
|
66
|
-
meta: {},
|
|
67
|
-
});
|
|
68
|
-
return sha256(canonical) as ProfileHashBase<string>;
|
|
52
|
+
return sha256(hashContract(args)) as ProfileHashBase<string>;
|
|
69
53
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { DomainModel } from './domain-types';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Unique symbol used as the key for branding types.
|
|
5
3
|
*/
|
|
@@ -59,26 +57,6 @@ export interface StorageBase<THash extends string = string> {
|
|
|
59
57
|
readonly storageHash: StorageHashBase<THash>;
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
export interface ContractBase<
|
|
63
|
-
TStorageHash extends StorageHashBase<string> = StorageHashBase<string>,
|
|
64
|
-
TExecutionHash extends ExecutionHashBase<string> = ExecutionHashBase<string>,
|
|
65
|
-
TProfileHash extends ProfileHashBase<string> = ProfileHashBase<string>,
|
|
66
|
-
> {
|
|
67
|
-
readonly schemaVersion: string;
|
|
68
|
-
readonly target: string;
|
|
69
|
-
readonly targetFamily: string;
|
|
70
|
-
readonly storageHash: TStorageHash;
|
|
71
|
-
readonly executionHash?: TExecutionHash;
|
|
72
|
-
readonly profileHash?: TProfileHash;
|
|
73
|
-
readonly capabilities: Record<string, Record<string, boolean>>;
|
|
74
|
-
readonly extensionPacks: Record<string, unknown>;
|
|
75
|
-
readonly meta: Record<string, unknown>;
|
|
76
|
-
readonly sources: Record<string, Source>;
|
|
77
|
-
readonly execution?: ExecutionSection;
|
|
78
|
-
readonly roots: Record<string, string>;
|
|
79
|
-
readonly models: Record<string, DomainModel>;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
60
|
export interface FieldType {
|
|
83
61
|
readonly type: string;
|
|
84
62
|
readonly nullable: boolean;
|
|
@@ -188,22 +166,6 @@ export interface DocCollection {
|
|
|
188
166
|
readonly readOnly?: boolean;
|
|
189
167
|
}
|
|
190
168
|
|
|
191
|
-
export interface DocumentStorage {
|
|
192
|
-
readonly document: {
|
|
193
|
-
readonly collections: Record<string, DocCollection>;
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export interface DocumentContract<
|
|
198
|
-
TStorageHash extends StorageHashBase<string> = StorageHashBase<string>,
|
|
199
|
-
TExecutionHash extends ExecutionHashBase<string> = ExecutionHashBase<string>,
|
|
200
|
-
TProfileHash extends ProfileHashBase<string> = ProfileHashBase<string>,
|
|
201
|
-
> extends ContractBase<TStorageHash, TExecutionHash, TProfileHash> {
|
|
202
|
-
// Accept string to work with JSON imports; runtime validation ensures 'document'
|
|
203
|
-
readonly targetFamily: string;
|
|
204
|
-
readonly storage: DocumentStorage;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
169
|
// Plan types - target-family agnostic execution types
|
|
208
170
|
export interface ParamDescriptor {
|
|
209
171
|
readonly index?: number;
|
|
@@ -277,18 +239,6 @@ export interface ExecutionPlan<Row = unknown, Ast = unknown> {
|
|
|
277
239
|
export type ResultType<P> =
|
|
278
240
|
P extends ExecutionPlan<infer R, unknown> ? R : P extends { readonly _Row?: infer R } ? R : never;
|
|
279
241
|
|
|
280
|
-
/**
|
|
281
|
-
* Type guard to check if a contract is a Document contract
|
|
282
|
-
*/
|
|
283
|
-
export function isDocumentContract(contract: unknown): contract is DocumentContract {
|
|
284
|
-
return (
|
|
285
|
-
typeof contract === 'object' &&
|
|
286
|
-
contract !== null &&
|
|
287
|
-
'targetFamily' in contract &&
|
|
288
|
-
contract.targetFamily === 'document'
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
242
|
/**
|
|
293
243
|
* Contract marker record stored in the database.
|
|
294
244
|
* Represents the current contract identity for a database.
|
package/src/validate-contract.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import { type } from 'arktype';
|
|
2
2
|
import type { Contract } from './contract-types';
|
|
3
|
-
import type { DomainContractShape
|
|
3
|
+
import type { DomainContractShape } from './validate-domain';
|
|
4
4
|
import { validateContractDomain } from './validate-domain';
|
|
5
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
|
+
|
|
6
19
|
/**
|
|
7
20
|
* Family-provided storage validator.
|
|
8
21
|
* SQL validates tables/columns/FKs; Mongo validates collections/embedding.
|
|
9
22
|
*/
|
|
10
23
|
export type StorageValidator = (contract: Contract) => void;
|
|
11
24
|
|
|
12
|
-
export interface ValidateContractResult {
|
|
13
|
-
readonly warnings: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
25
|
const ContractSchema = type({
|
|
17
26
|
target: 'string',
|
|
18
27
|
targetFamily: 'string',
|
|
@@ -28,11 +37,11 @@ const ContractSchema = type({
|
|
|
28
37
|
defaults: 'unknown[]',
|
|
29
38
|
},
|
|
30
39
|
},
|
|
31
|
-
|
|
40
|
+
profileHash: 'string',
|
|
32
41
|
});
|
|
33
42
|
|
|
34
43
|
function stripPersistenceFields(raw: Record<string, unknown>): Record<string, unknown> {
|
|
35
|
-
const { schemaVersion: _,
|
|
44
|
+
const { schemaVersion: _, _generated: _g, ...rest } = raw;
|
|
36
45
|
return rest;
|
|
37
46
|
}
|
|
38
47
|
|
|
@@ -54,7 +63,7 @@ function extractDomainShape(contract: Contract): DomainContractShape {
|
|
|
54
63
|
* 3. **Storage validation** (family-provided): SQL validates tables/columns/FKs;
|
|
55
64
|
* Mongo validates collections/embedding.
|
|
56
65
|
*
|
|
57
|
-
* JSON persistence fields (`schemaVersion`, `
|
|
66
|
+
* JSON persistence fields (`schemaVersion`, `_generated`) are stripped before
|
|
58
67
|
* validation — they are not part of the in-memory contract representation.
|
|
59
68
|
*
|
|
60
69
|
* @template TContract The fully-typed contract type (preserves literal types).
|
|
@@ -65,29 +74,26 @@ function extractDomainShape(contract: Contract): DomainContractShape {
|
|
|
65
74
|
export function validateContract<TContract extends Contract>(
|
|
66
75
|
value: unknown,
|
|
67
76
|
storageValidator: StorageValidator,
|
|
68
|
-
): TContract
|
|
77
|
+
): TContract {
|
|
69
78
|
if (typeof value !== 'object' || value === null) {
|
|
70
|
-
throw new
|
|
79
|
+
throw new ContractValidationError('Contract must be a non-null object', 'structural');
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
const stripped = stripPersistenceFields(value as Record<string, unknown>);
|
|
74
83
|
|
|
75
84
|
const parsed = ContractSchema(stripped);
|
|
76
85
|
if (parsed instanceof type.errors) {
|
|
77
|
-
throw new
|
|
86
|
+
throw new ContractValidationError(
|
|
87
|
+
`Invalid contract structure: ${parsed.summary}`,
|
|
88
|
+
'structural',
|
|
89
|
+
);
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
// Arktype verified the structural shape; Contract adds branded hash types and
|
|
81
|
-
// ContractModel generics that can't be expressed in the schema.
|
|
82
92
|
const contract = parsed as unknown as Contract;
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
validateContractDomain(extractDomainShape(contract));
|
|
85
95
|
|
|
86
96
|
storageValidator(contract);
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
// the runtime object is the same — the cast preserves the caller's type parameter.
|
|
90
|
-
return Object.assign(contract as unknown as TContract, {
|
|
91
|
-
warnings: domainResult.warnings,
|
|
92
|
-
});
|
|
98
|
+
return contract as unknown as TContract;
|
|
93
99
|
}
|