@prisma-next/contract 0.3.0-dev.15 → 0.3.0-dev.150
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/LICENSE +201 -0
- package/README.md +43 -254
- package/dist/contract-types-MYdoYIIh.d.mts +314 -0
- package/dist/contract-types-MYdoYIIh.d.mts.map +1 -0
- package/dist/hashing-CyaA_Qvf.mjs +196 -0
- package/dist/hashing-CyaA_Qvf.mjs.map +1 -0
- package/dist/hashing.d.mts +29 -0
- package/dist/hashing.d.mts.map +1 -0
- package/dist/hashing.mjs +3 -0
- package/dist/testing.d.mts +29 -0
- package/dist/testing.d.mts.map +1 -0
- package/dist/testing.mjs +58 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-aMyNgejf.mjs +14 -0
- package/dist/types-aMyNgejf.mjs.map +1 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +3 -0
- package/dist/validate-contract.d.mts +37 -0
- package/dist/validate-contract.d.mts.map +1 -0
- package/dist/validate-contract.mjs +3 -0
- package/dist/validate-domain-CpCcTlqJ.mjs +165 -0
- package/dist/validate-domain-CpCcTlqJ.mjs.map +1 -0
- package/dist/validate-domain.d.mts +24 -0
- package/dist/validate-domain.d.mts.map +1 -0
- package/dist/validate-domain.mjs +3 -0
- package/package.json +24 -25
- package/schemas/data-contract-document-v1.json +5 -5
- package/src/canonicalization.ts +263 -0
- package/src/contract-types.ts +55 -0
- package/src/domain-types.ts +95 -0
- package/src/exports/hashing.ts +2 -0
- package/src/exports/testing.ts +1 -0
- package/src/exports/types.ts +36 -13
- package/src/exports/validate-contract.ts +6 -0
- package/src/exports/validate-domain.ts +5 -0
- package/src/hashing.ts +53 -0
- package/src/testing-factories.ts +101 -0
- package/src/types.ts +102 -121
- package/src/validate-contract.ts +101 -0
- package/src/validate-domain.ts +227 -0
- package/dist/exports/framework-components.d.ts +0 -3
- package/dist/exports/framework-components.d.ts.map +0 -1
- package/dist/exports/framework-components.js +0 -24
- package/dist/exports/framework-components.js.map +0 -1
- package/dist/exports/ir.d.ts +0 -2
- package/dist/exports/ir.d.ts.map +0 -1
- package/dist/exports/ir.js +0 -35
- package/dist/exports/ir.js.map +0 -1
- package/dist/exports/pack-manifest-types.d.ts +0 -2
- package/dist/exports/pack-manifest-types.d.ts.map +0 -1
- package/dist/exports/pack-manifest-types.js +0 -1
- package/dist/exports/pack-manifest-types.js.map +0 -1
- package/dist/exports/types.d.ts +0 -3
- package/dist/exports/types.d.ts.map +0 -1
- package/dist/exports/types.js +0 -8
- package/dist/exports/types.js.map +0 -1
- package/dist/framework-components.d.ts +0 -408
- package/dist/framework-components.d.ts.map +0 -1
- package/dist/ir.d.ts +0 -76
- package/dist/ir.d.ts.map +0 -1
- package/dist/types.d.ts +0 -222
- package/dist/types.d.ts.map +0 -1
- package/src/exports/framework-components.ts +0 -26
- package/src/exports/ir.ts +0 -1
- package/src/exports/pack-manifest-types.ts +0 -6
- package/src/framework-components.ts +0 -525
- package/src/ir.ts +0 -113
package/src/types.ts
CHANGED
|
@@ -1,16 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Unique symbol used as the key for branding types.
|
|
3
|
+
*/
|
|
4
|
+
export const $: unique symbol = Symbol('__prisma_next_brand__');
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
/**
|
|
7
|
+
* A helper type to brand a given type with a unique identifier.
|
|
8
|
+
*
|
|
9
|
+
* @template TKey Text used as the brand key.
|
|
10
|
+
* @template TValue Optional value associated with the brand key. Defaults to `true`.
|
|
11
|
+
*/
|
|
12
|
+
export type Brand<TKey extends string | number | symbol, TValue = true> = {
|
|
13
|
+
[$]: {
|
|
14
|
+
[K in TKey]: TValue;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Base type for storage contract hashes.
|
|
20
|
+
* Emitted contract.d.ts files use this with the hash value as a type parameter:
|
|
21
|
+
* `type StorageHash = StorageHashBase<'sha256:abc123...'>`
|
|
22
|
+
*/
|
|
23
|
+
export type StorageHashBase<THash extends string> = THash & Brand<'StorageHash'>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Base type for execution contract hashes.
|
|
27
|
+
* Emitted contract.d.ts files use this with the hash value as a type parameter:
|
|
28
|
+
* `type ExecutionHash = ExecutionHashBase<'sha256:def456...'>`
|
|
29
|
+
*/
|
|
30
|
+
export type ExecutionHashBase<THash extends string> = THash & Brand<'ExecutionHash'>;
|
|
31
|
+
|
|
32
|
+
export function executionHash<const T extends string>(value: T): ExecutionHashBase<T> {
|
|
33
|
+
return value as ExecutionHashBase<T>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function coreHash<const T extends string>(value: T): StorageHashBase<T> {
|
|
37
|
+
return value as StorageHashBase<T>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Base type for profile contract hashes.
|
|
42
|
+
* Emitted contract.d.ts files use this with the hash value as a type parameter:
|
|
43
|
+
* `type ProfileHash = ProfileHashBase<'sha256:def456...'>`
|
|
44
|
+
*/
|
|
45
|
+
export type ProfileHashBase<THash extends string> = THash & Brand<'ProfileHash'>;
|
|
46
|
+
|
|
47
|
+
export function profileHash<const T extends string>(value: T): ProfileHashBase<T> {
|
|
48
|
+
return value as ProfileHashBase<T>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Base type for family-specific storage blocks.
|
|
53
|
+
* Family storage types (SqlStorage, MongoStorage, etc.) extend this to carry the
|
|
54
|
+
* storage hash alongside family-specific data (tables, collections, etc.).
|
|
55
|
+
*/
|
|
56
|
+
export interface StorageBase<THash extends string = string> {
|
|
57
|
+
readonly storageHash: StorageHashBase<THash>;
|
|
14
58
|
}
|
|
15
59
|
|
|
16
60
|
export interface FieldType {
|
|
@@ -20,6 +64,48 @@ export interface FieldType {
|
|
|
20
64
|
readonly properties?: Record<string, FieldType>;
|
|
21
65
|
}
|
|
22
66
|
|
|
67
|
+
export type GeneratedValueSpec = {
|
|
68
|
+
readonly id: string;
|
|
69
|
+
readonly params?: Record<string, unknown>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
73
|
+
|
|
74
|
+
export type JsonValue =
|
|
75
|
+
| JsonPrimitive
|
|
76
|
+
| { readonly [key: string]: JsonValue }
|
|
77
|
+
| readonly JsonValue[];
|
|
78
|
+
|
|
79
|
+
export type ColumnDefaultLiteralValue = JsonValue;
|
|
80
|
+
|
|
81
|
+
export type ColumnDefaultLiteralInputValue = ColumnDefaultLiteralValue | Date;
|
|
82
|
+
|
|
83
|
+
export type ColumnDefault =
|
|
84
|
+
| {
|
|
85
|
+
readonly kind: 'literal';
|
|
86
|
+
readonly value: ColumnDefaultLiteralInputValue;
|
|
87
|
+
}
|
|
88
|
+
| { readonly kind: 'function'; readonly expression: string };
|
|
89
|
+
|
|
90
|
+
export type ExecutionMutationDefaultValue = {
|
|
91
|
+
readonly kind: 'generator';
|
|
92
|
+
readonly id: GeneratedValueSpec['id'];
|
|
93
|
+
readonly params?: Record<string, unknown>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type ExecutionMutationDefault = {
|
|
97
|
+
readonly ref: { readonly table: string; readonly column: string };
|
|
98
|
+
readonly onCreate?: ExecutionMutationDefaultValue;
|
|
99
|
+
readonly onUpdate?: ExecutionMutationDefaultValue;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type ExecutionSection<THash extends string = string> = {
|
|
103
|
+
readonly executionHash: ExecutionHashBase<THash>;
|
|
104
|
+
readonly mutations: {
|
|
105
|
+
readonly defaults: ReadonlyArray<ExecutionMutationDefault>;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
23
109
|
export interface Source {
|
|
24
110
|
readonly readOnly: boolean;
|
|
25
111
|
readonly projection: Record<string, FieldType>;
|
|
@@ -42,25 +128,13 @@ export type Expr =
|
|
|
42
128
|
export interface DocCollection {
|
|
43
129
|
readonly name: string;
|
|
44
130
|
readonly id?: {
|
|
45
|
-
readonly strategy: 'auto' | 'client' | 'uuid' | '
|
|
131
|
+
readonly strategy: 'auto' | 'client' | 'uuid' | 'objectId';
|
|
46
132
|
};
|
|
47
133
|
readonly fields: Record<string, FieldType>;
|
|
48
134
|
readonly indexes?: ReadonlyArray<DocIndex>;
|
|
49
135
|
readonly readOnly?: boolean;
|
|
50
136
|
}
|
|
51
137
|
|
|
52
|
-
export interface DocumentStorage {
|
|
53
|
-
readonly document: {
|
|
54
|
-
readonly collections: Record<string, DocCollection>;
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface DocumentContract extends ContractBase {
|
|
59
|
-
// Accept string to work with JSON imports; runtime validation ensures 'document'
|
|
60
|
-
readonly targetFamily: string;
|
|
61
|
-
readonly storage: DocumentStorage;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
138
|
// Plan types - target-family agnostic execution types
|
|
65
139
|
export interface ParamDescriptor {
|
|
66
140
|
readonly index?: number;
|
|
@@ -68,7 +142,7 @@ export interface ParamDescriptor {
|
|
|
68
142
|
readonly codecId?: string;
|
|
69
143
|
readonly nativeType?: string;
|
|
70
144
|
readonly nullable?: boolean;
|
|
71
|
-
readonly source: 'dsl' | 'raw';
|
|
145
|
+
readonly source: 'dsl' | 'raw' | 'lane';
|
|
72
146
|
readonly refs?: { table: string; column: string };
|
|
73
147
|
}
|
|
74
148
|
|
|
@@ -85,7 +159,7 @@ export interface PlanRefs {
|
|
|
85
159
|
export interface PlanMeta {
|
|
86
160
|
readonly target: string;
|
|
87
161
|
readonly targetFamily?: string;
|
|
88
|
-
readonly
|
|
162
|
+
readonly storageHash: string;
|
|
89
163
|
readonly profileHash?: string;
|
|
90
164
|
readonly lane: string;
|
|
91
165
|
readonly annotations?: {
|
|
@@ -134,24 +208,12 @@ export interface ExecutionPlan<Row = unknown, Ast = unknown> {
|
|
|
134
208
|
export type ResultType<P> =
|
|
135
209
|
P extends ExecutionPlan<infer R, unknown> ? R : P extends { readonly _Row?: infer R } ? R : never;
|
|
136
210
|
|
|
137
|
-
/**
|
|
138
|
-
* Type guard to check if a contract is a Document contract
|
|
139
|
-
*/
|
|
140
|
-
export function isDocumentContract(contract: unknown): contract is DocumentContract {
|
|
141
|
-
return (
|
|
142
|
-
typeof contract === 'object' &&
|
|
143
|
-
contract !== null &&
|
|
144
|
-
'targetFamily' in contract &&
|
|
145
|
-
contract.targetFamily === 'document'
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
211
|
/**
|
|
150
212
|
* Contract marker record stored in the database.
|
|
151
213
|
* Represents the current contract identity for a database.
|
|
152
214
|
*/
|
|
153
215
|
export interface ContractMarkerRecord {
|
|
154
|
-
readonly
|
|
216
|
+
readonly storageHash: string;
|
|
155
217
|
readonly profileHash: string;
|
|
156
218
|
readonly contractJson: unknown | null;
|
|
157
219
|
readonly canonicalVersion: number | null;
|
|
@@ -159,84 +221,3 @@ export interface ContractMarkerRecord {
|
|
|
159
221
|
readonly appTag: string | null;
|
|
160
222
|
readonly meta: Record<string, unknown>;
|
|
161
223
|
}
|
|
162
|
-
|
|
163
|
-
// Emitter types - moved from @prisma-next/emitter to shared location
|
|
164
|
-
/**
|
|
165
|
-
* Specifies how to import TypeScript types from a package.
|
|
166
|
-
* Used in extension pack manifests to declare codec and operation type imports.
|
|
167
|
-
*/
|
|
168
|
-
export interface TypesImportSpec {
|
|
169
|
-
readonly package: string;
|
|
170
|
-
readonly named: string;
|
|
171
|
-
readonly alias: string;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Validation context passed to TargetFamilyHook.validateTypes().
|
|
176
|
-
* Contains pre-assembled operation registry, type imports, and extension IDs.
|
|
177
|
-
*/
|
|
178
|
-
export interface ValidationContext {
|
|
179
|
-
readonly operationRegistry?: OperationRegistry;
|
|
180
|
-
readonly codecTypeImports?: ReadonlyArray<TypesImportSpec>;
|
|
181
|
-
readonly operationTypeImports?: ReadonlyArray<TypesImportSpec>;
|
|
182
|
-
readonly extensionIds?: ReadonlyArray<string>;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* SPI interface for target family hooks that extend emission behavior.
|
|
187
|
-
* Implemented by family-specific emitter hooks (e.g., SQL family).
|
|
188
|
-
*/
|
|
189
|
-
export interface TargetFamilyHook {
|
|
190
|
-
readonly id: string;
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Validates that all type IDs in the contract come from referenced extension packs.
|
|
194
|
-
* @param ir - Contract IR to validate
|
|
195
|
-
* @param ctx - Validation context with operation registry and extension IDs
|
|
196
|
-
*/
|
|
197
|
-
validateTypes(ir: ContractIR, ctx: ValidationContext): void;
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Validates family-specific contract structure.
|
|
201
|
-
* @param ir - Contract IR to validate
|
|
202
|
-
*/
|
|
203
|
-
validateStructure(ir: ContractIR): void;
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Generates contract.d.ts file content.
|
|
207
|
-
* @param ir - Contract IR
|
|
208
|
-
* @param codecTypeImports - Array of codec type import specs
|
|
209
|
-
* @param operationTypeImports - Array of operation type import specs
|
|
210
|
-
* @returns Generated TypeScript type definitions as string
|
|
211
|
-
*/
|
|
212
|
-
generateContractTypes(
|
|
213
|
-
ir: ContractIR,
|
|
214
|
-
codecTypeImports: ReadonlyArray<TypesImportSpec>,
|
|
215
|
-
operationTypeImports: ReadonlyArray<TypesImportSpec>,
|
|
216
|
-
): string;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Extension pack manifest types - moved from @prisma-next/core-control-plane to shared location
|
|
220
|
-
export type ArgSpecManifest =
|
|
221
|
-
| { readonly kind: 'typeId'; readonly type: string }
|
|
222
|
-
| { readonly kind: 'param' }
|
|
223
|
-
| { readonly kind: 'literal' };
|
|
224
|
-
|
|
225
|
-
export type ReturnSpecManifest =
|
|
226
|
-
| { readonly kind: 'typeId'; readonly type: string }
|
|
227
|
-
| { readonly kind: 'builtin'; readonly type: 'number' | 'boolean' | 'string' };
|
|
228
|
-
|
|
229
|
-
export interface LoweringSpecManifest {
|
|
230
|
-
readonly targetFamily: 'sql';
|
|
231
|
-
readonly strategy: 'infix' | 'function';
|
|
232
|
-
readonly template: string;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export interface OperationManifest {
|
|
236
|
-
readonly for: string;
|
|
237
|
-
readonly method: string;
|
|
238
|
-
readonly args: ReadonlyArray<ArgSpecManifest>;
|
|
239
|
-
readonly returns: ReturnSpecManifest;
|
|
240
|
-
readonly lowering: LoweringSpecManifest;
|
|
241
|
-
readonly capabilities?: ReadonlyArray<string>;
|
|
242
|
-
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { ContractValidationError } from './validate-contract';
|
|
2
|
+
|
|
3
|
+
export interface DomainModelShape {
|
|
4
|
+
readonly fields: Record<string, unknown>;
|
|
5
|
+
readonly relations?: Record<string, { readonly to: string }>;
|
|
6
|
+
readonly discriminator?: { readonly field: string };
|
|
7
|
+
readonly variants?: Record<string, unknown>;
|
|
8
|
+
readonly base?: string;
|
|
9
|
+
readonly owner?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DomainContractShape {
|
|
13
|
+
readonly roots: Record<string, string>;
|
|
14
|
+
readonly models: Record<string, DomainModelShape>;
|
|
15
|
+
readonly valueObjects?: Record<string, { readonly fields: Record<string, unknown> }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateContractDomain(contract: DomainContractShape): void {
|
|
19
|
+
const errors: string[] = [];
|
|
20
|
+
const modelNames = new Set(Object.keys(contract.models));
|
|
21
|
+
|
|
22
|
+
validateRoots(contract, modelNames, errors);
|
|
23
|
+
validateVariantsAndBases(contract, modelNames, errors);
|
|
24
|
+
validateRelationTargets(contract, modelNames, errors);
|
|
25
|
+
validateDiscriminators(contract, errors);
|
|
26
|
+
validateOwnership(contract, modelNames, errors);
|
|
27
|
+
validateValueObjectReferences(contract, errors);
|
|
28
|
+
validateFieldModifiers(contract, errors);
|
|
29
|
+
|
|
30
|
+
if (errors.length > 0) {
|
|
31
|
+
throw new ContractValidationError(
|
|
32
|
+
`Contract domain validation failed:\n- ${errors.join('\n- ')}`,
|
|
33
|
+
'domain',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateRoots(
|
|
39
|
+
contract: DomainContractShape,
|
|
40
|
+
modelNames: Set<string>,
|
|
41
|
+
errors: string[],
|
|
42
|
+
): void {
|
|
43
|
+
const seenValues = new Set<string>();
|
|
44
|
+
for (const [rootKey, modelName] of Object.entries(contract.roots)) {
|
|
45
|
+
if (seenValues.has(modelName)) {
|
|
46
|
+
errors.push(`Duplicate root value: "${modelName}" is mapped by multiple root keys`);
|
|
47
|
+
}
|
|
48
|
+
seenValues.add(modelName);
|
|
49
|
+
|
|
50
|
+
if (!modelNames.has(modelName)) {
|
|
51
|
+
errors.push(
|
|
52
|
+
`Root "${rootKey}" references model "${modelName}" which does not exist in models`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateVariantsAndBases(
|
|
59
|
+
contract: DomainContractShape,
|
|
60
|
+
modelNames: Set<string>,
|
|
61
|
+
errors: string[],
|
|
62
|
+
): void {
|
|
63
|
+
const models = new Map(Object.entries(contract.models));
|
|
64
|
+
|
|
65
|
+
for (const [modelName, model] of models) {
|
|
66
|
+
if (model.variants) {
|
|
67
|
+
for (const variantName of Object.keys(model.variants)) {
|
|
68
|
+
if (!modelNames.has(variantName)) {
|
|
69
|
+
errors.push(
|
|
70
|
+
`Model "${modelName}" lists variant "${variantName}" which does not exist in models`,
|
|
71
|
+
);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const variantModel = models.get(variantName);
|
|
75
|
+
if (!variantModel) continue;
|
|
76
|
+
if (variantModel.base !== modelName) {
|
|
77
|
+
errors.push(
|
|
78
|
+
`Variant "${variantName}" has base "${variantModel.base ?? '(none)'}" but expected "${modelName}"`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (model.base) {
|
|
85
|
+
if (!modelNames.has(model.base)) {
|
|
86
|
+
errors.push(`Model "${modelName}" has base "${model.base}" which does not exist in models`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const baseModel = models.get(model.base);
|
|
90
|
+
if (!baseModel) continue;
|
|
91
|
+
if (!baseModel.variants || !Object.hasOwn(baseModel.variants, modelName)) {
|
|
92
|
+
errors.push(
|
|
93
|
+
`Model "${modelName}" has base "${model.base}" which does not list it as a variant`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateRelationTargets(
|
|
101
|
+
contract: DomainContractShape,
|
|
102
|
+
modelNames: Set<string>,
|
|
103
|
+
errors: string[],
|
|
104
|
+
): void {
|
|
105
|
+
for (const [modelName, model] of Object.entries(contract.models)) {
|
|
106
|
+
for (const [relName, relation] of Object.entries(model.relations ?? {})) {
|
|
107
|
+
if (!modelNames.has(relation.to)) {
|
|
108
|
+
errors.push(
|
|
109
|
+
`Relation "${relName}" on model "${modelName}" targets "${relation.to}" which does not exist in models`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function validateDiscriminators(contract: DomainContractShape, errors: string[]): void {
|
|
117
|
+
for (const [modelName, model] of Object.entries(contract.models)) {
|
|
118
|
+
if (model.discriminator) {
|
|
119
|
+
if (!model.variants || Object.keys(model.variants).length === 0) {
|
|
120
|
+
errors.push(`Model "${modelName}" has discriminator but no variants`);
|
|
121
|
+
}
|
|
122
|
+
if (!Object.hasOwn(model.fields, model.discriminator.field)) {
|
|
123
|
+
errors.push(
|
|
124
|
+
`Discriminator field "${model.discriminator.field}" is not a field on model "${modelName}"`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (model.variants && Object.keys(model.variants).length > 0 && !model.discriminator) {
|
|
130
|
+
errors.push(`Model "${modelName}" has variants but no discriminator`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (model.base) {
|
|
134
|
+
if (model.discriminator) {
|
|
135
|
+
errors.push(`Model "${modelName}" has base and must not have discriminator`);
|
|
136
|
+
}
|
|
137
|
+
if (model.variants && Object.keys(model.variants).length > 0) {
|
|
138
|
+
errors.push(`Model "${modelName}" has base and must not have variants`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateOwnership(
|
|
145
|
+
contract: DomainContractShape,
|
|
146
|
+
modelNames: Set<string>,
|
|
147
|
+
errors: string[],
|
|
148
|
+
): void {
|
|
149
|
+
for (const [modelName, model] of Object.entries(contract.models)) {
|
|
150
|
+
if (!model.owner) continue;
|
|
151
|
+
|
|
152
|
+
if (model.owner === modelName) {
|
|
153
|
+
errors.push(`Model "${modelName}" cannot own itself`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!modelNames.has(model.owner)) {
|
|
157
|
+
errors.push(`Model "${modelName}" has owner "${model.owner}" which does not exist in models`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const [rootKey, rootModel] of Object.entries(contract.roots)) {
|
|
161
|
+
if (rootModel === modelName) {
|
|
162
|
+
errors.push(
|
|
163
|
+
`Owned model "${modelName}" must not appear in roots (found as root "${rootKey}")`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface FieldTypeLike {
|
|
171
|
+
readonly kind?: string;
|
|
172
|
+
readonly name?: string;
|
|
173
|
+
readonly members?: readonly FieldTypeLike[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface FieldLike {
|
|
177
|
+
readonly type?: FieldTypeLike;
|
|
178
|
+
readonly many?: boolean;
|
|
179
|
+
readonly dict?: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function forEachContractField(
|
|
183
|
+
contract: DomainContractShape,
|
|
184
|
+
callback: (field: unknown, location: string) => void,
|
|
185
|
+
): void {
|
|
186
|
+
for (const [modelName, model] of Object.entries(contract.models)) {
|
|
187
|
+
for (const [fieldName, field] of Object.entries(model.fields)) {
|
|
188
|
+
callback(field, `Model "${modelName}" field "${fieldName}"`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const [voName, vo] of Object.entries(contract.valueObjects ?? {})) {
|
|
192
|
+
for (const [fieldName, field] of Object.entries(vo.fields)) {
|
|
193
|
+
callback(field, `Value object "${voName}" field "${fieldName}"`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateValueObjectReferences(contract: DomainContractShape, errors: string[]): void {
|
|
199
|
+
const voNames = new Set(Object.keys(contract.valueObjects ?? {}));
|
|
200
|
+
|
|
201
|
+
function checkType(type: FieldTypeLike | undefined, location: string): void {
|
|
202
|
+
if (!type) return;
|
|
203
|
+
if (type.kind === 'valueObject' && type.name && !voNames.has(type.name)) {
|
|
204
|
+
errors.push(
|
|
205
|
+
`${location} references value object "${type.name}" which does not exist in valueObjects`,
|
|
206
|
+
);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (type.kind === 'union') {
|
|
210
|
+
for (const member of type.members ?? []) checkType(member, location);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
forEachContractField(contract, (field, location) => {
|
|
215
|
+
const f = field as FieldLike | undefined;
|
|
216
|
+
checkType(f?.type, location);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function validateFieldModifiers(contract: DomainContractShape, errors: string[]): void {
|
|
221
|
+
forEachContractField(contract, (field, location) => {
|
|
222
|
+
const f = field as FieldLike | undefined;
|
|
223
|
+
if (f?.many && f?.dict) {
|
|
224
|
+
errors.push(`${location} cannot have both "many" and "dict" modifiers`);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
export type { AdapterDescriptor, AdapterInstance, AdapterPackRef, ComponentDescriptor, ComponentMetadata, ContractComponentRequirementsCheckInput, ContractComponentRequirementsCheckResult, DriverDescriptor, DriverInstance, DriverPackRef, ExtensionDescriptor, ExtensionInstance, ExtensionPackRef, FamilyDescriptor, FamilyInstance, PackRefBase, TargetBoundComponentDescriptor, TargetDescriptor, TargetInstance, TargetPackRef, } from '../framework-components';
|
|
2
|
-
export { checkContractComponentRequirements } from '../framework-components';
|
|
3
|
-
//# sourceMappingURL=framework-components.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"framework-components.d.ts","sourceRoot":"","sources":["../../src/exports/framework-components.ts"],"names":[],"mappings":"AAAA,YAAY,EAEV,iBAAiB,EAEjB,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,uCAAuC,EACvC,wCAAwC,EACxC,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,WAAW,EACX,8BAA8B,EAC9B,gBAAgB,EAChB,cAAc,EACd,aAAa,GACd,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,kCAAkC,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// src/framework-components.ts
|
|
2
|
-
function checkContractComponentRequirements(input) {
|
|
3
|
-
const providedIds = /* @__PURE__ */ new Set();
|
|
4
|
-
for (const id of input.providedComponentIds) {
|
|
5
|
-
providedIds.add(id);
|
|
6
|
-
}
|
|
7
|
-
const requiredExtensionPackIds = input.contract.extensionPacks ? Object.keys(input.contract.extensionPacks) : [];
|
|
8
|
-
const missingExtensionPackIds = requiredExtensionPackIds.filter((id) => !providedIds.has(id));
|
|
9
|
-
const expectedTargetFamily = input.expectedTargetFamily;
|
|
10
|
-
const contractTargetFamily = input.contract.targetFamily;
|
|
11
|
-
const familyMismatch = expectedTargetFamily !== void 0 && contractTargetFamily !== void 0 && contractTargetFamily !== expectedTargetFamily ? { expected: expectedTargetFamily, actual: contractTargetFamily } : void 0;
|
|
12
|
-
const expectedTargetId = input.expectedTargetId;
|
|
13
|
-
const contractTargetId = input.contract.target;
|
|
14
|
-
const targetMismatch = expectedTargetId !== void 0 && contractTargetId !== expectedTargetId ? { expected: expectedTargetId, actual: contractTargetId } : void 0;
|
|
15
|
-
return {
|
|
16
|
-
...familyMismatch ? { familyMismatch } : {},
|
|
17
|
-
...targetMismatch ? { targetMismatch } : {},
|
|
18
|
-
missingExtensionPackIds
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
export {
|
|
22
|
-
checkContractComponentRequirements
|
|
23
|
-
};
|
|
24
|
-
//# sourceMappingURL=framework-components.js.map
|