@prisma-next/contract 0.3.0-dev.14 → 0.3.0-dev.141
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-x89nqTli.d.mts +49 -0
- package/dist/contract-types-x89nqTli.d.mts.map +1 -0
- package/dist/hashing-D1EPxYRl.mjs +215 -0
- package/dist/hashing-D1EPxYRl.mjs.map +1 -0
- package/dist/hashing.d.mts +38 -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 +56 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-DYikGC04.mjs +33 -0
- package/dist/types-DYikGC04.mjs.map +1 -0
- package/dist/types-DmKtoEd-.d.mts +303 -0
- package/dist/types-DmKtoEd-.d.mts.map +1 -0
- package/dist/types.d.mts +3 -0
- package/dist/types.mjs +3 -0
- package/dist/validate-contract.d.mts +35 -0
- package/dist/validate-contract.d.mts.map +1 -0
- package/dist/validate-contract.mjs +61 -0
- package/dist/validate-contract.mjs.map +1 -0
- package/dist/validate-domain-CTQiBiei.mjs +84 -0
- package/dist/validate-domain-CTQiBiei.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 +286 -0
- package/src/contract-types.ts +54 -0
- package/src/domain-types.ts +85 -0
- package/src/exports/hashing.ts +6 -0
- package/src/exports/testing.ts +1 -0
- package/src/exports/types.ts +49 -10
- package/src/exports/validate-contract.ts +5 -0
- package/src/exports/validate-domain.ts +6 -0
- package/src/hashing.ts +69 -0
- package/src/testing-factories.ts +93 -0
- package/src/types.ts +153 -91
- package/src/validate-contract.ts +93 -0
- package/src/validate-domain.ts +205 -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/hashing.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalizeContract } from './canonicalization';
|
|
3
|
+
import type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from './types';
|
|
4
|
+
|
|
5
|
+
const SCHEMA_VERSION = '1';
|
|
6
|
+
|
|
7
|
+
function sha256(content: string): string {
|
|
8
|
+
const hash = createHash('sha256');
|
|
9
|
+
hash.update(content);
|
|
10
|
+
return `sha256:${hash.digest('hex')}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function computeStorageHash(args: {
|
|
14
|
+
target: string;
|
|
15
|
+
targetFamily: string;
|
|
16
|
+
storage: Record<string, unknown>;
|
|
17
|
+
}): StorageHashBase<string> {
|
|
18
|
+
const canonical = canonicalizeContract({
|
|
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>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function computeExecutionHash(args: {
|
|
33
|
+
target: string;
|
|
34
|
+
targetFamily: string;
|
|
35
|
+
execution: Record<string, unknown>;
|
|
36
|
+
}): ExecutionHashBase<string> {
|
|
37
|
+
const canonical = canonicalizeContract({
|
|
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>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function computeProfileHash(args: {
|
|
53
|
+
target: string;
|
|
54
|
+
targetFamily: string;
|
|
55
|
+
capabilities: Record<string, Record<string, boolean>>;
|
|
56
|
+
}): ProfileHashBase<string> {
|
|
57
|
+
const canonical = canonicalizeContract({
|
|
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>;
|
|
69
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Contract } from './contract-types';
|
|
2
|
+
import type { ContractModel, ModelStorageBase } from './domain-types';
|
|
3
|
+
import { computeExecutionHash, computeProfileHash, computeStorageHash } from './hashing';
|
|
4
|
+
import type { ExecutionSection, ProfileHashBase, StorageBase } from './types';
|
|
5
|
+
import { coreHash } from './types';
|
|
6
|
+
|
|
7
|
+
type ContractOverrides<
|
|
8
|
+
TStorage extends StorageBase = StorageBase,
|
|
9
|
+
TModels extends Record<string, ContractModel> = Record<string, ContractModel>,
|
|
10
|
+
> = {
|
|
11
|
+
target?: string;
|
|
12
|
+
targetFamily?: string;
|
|
13
|
+
roots?: Record<string, string>;
|
|
14
|
+
models?: TModels;
|
|
15
|
+
storage?: Omit<TStorage, 'storageHash'>;
|
|
16
|
+
capabilities?: Record<string, Record<string, boolean>>;
|
|
17
|
+
extensionPacks?: Record<string, unknown>;
|
|
18
|
+
execution?: Omit<ExecutionSection, 'executionHash'>;
|
|
19
|
+
profileHash?: ProfileHashBase<string>;
|
|
20
|
+
meta?: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DUMMY_HASH = coreHash('sha256:test');
|
|
24
|
+
|
|
25
|
+
export function createContract<
|
|
26
|
+
TStorage extends StorageBase = StorageBase,
|
|
27
|
+
TModels extends Record<string, ContractModel> = Record<string, ContractModel>,
|
|
28
|
+
>(overrides: ContractOverrides<TStorage, TModels> = {}): Contract<TStorage, TModels> {
|
|
29
|
+
const target = overrides.target ?? 'postgres';
|
|
30
|
+
const targetFamily = overrides.targetFamily ?? 'sql';
|
|
31
|
+
const capabilities = overrides.capabilities ?? {};
|
|
32
|
+
|
|
33
|
+
const rawStorage =
|
|
34
|
+
overrides.storage ?? ({ tables: {} } as unknown as Omit<TStorage, 'storageHash'>);
|
|
35
|
+
|
|
36
|
+
const storageHash = computeStorageHash({
|
|
37
|
+
target,
|
|
38
|
+
targetFamily,
|
|
39
|
+
storage: rawStorage as Record<string, unknown>,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const storage = {
|
|
43
|
+
...rawStorage,
|
|
44
|
+
storageHash,
|
|
45
|
+
} as TStorage;
|
|
46
|
+
|
|
47
|
+
const computedProfileHash =
|
|
48
|
+
overrides.profileHash ?? computeProfileHash({ target, targetFamily, capabilities });
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
target,
|
|
52
|
+
targetFamily,
|
|
53
|
+
roots: overrides.roots ?? {},
|
|
54
|
+
models: (overrides.models ?? {}) as TModels,
|
|
55
|
+
storage,
|
|
56
|
+
capabilities,
|
|
57
|
+
extensionPacks: overrides.extensionPacks ?? {},
|
|
58
|
+
...(overrides.execution !== undefined
|
|
59
|
+
? {
|
|
60
|
+
execution: {
|
|
61
|
+
...overrides.execution,
|
|
62
|
+
executionHash: computeExecutionHash({
|
|
63
|
+
target,
|
|
64
|
+
targetFamily,
|
|
65
|
+
execution: overrides.execution,
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
: {}),
|
|
70
|
+
profileHash: computedProfileHash,
|
|
71
|
+
meta: overrides.meta ?? {},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type SqlStorageLike = StorageBase & {
|
|
76
|
+
readonly tables: Record<string, unknown>;
|
|
77
|
+
readonly types?: Record<string, unknown>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type SqlModelLike = ContractModel<ModelStorageBase & { table: string }>;
|
|
81
|
+
|
|
82
|
+
export function createSqlContract(
|
|
83
|
+
overrides: ContractOverrides<SqlStorageLike, Record<string, SqlModelLike>> = {},
|
|
84
|
+
): Contract<SqlStorageLike, Record<string, SqlModelLike>> {
|
|
85
|
+
return createContract<SqlStorageLike, Record<string, SqlModelLike>>({
|
|
86
|
+
target: 'postgres',
|
|
87
|
+
targetFamily: 'sql',
|
|
88
|
+
storage: overrides.storage ?? { tables: {} },
|
|
89
|
+
...overrides,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { DUMMY_HASH };
|
package/src/types.ts
CHANGED
|
@@ -1,16 +1,82 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { ContractIR } from './ir';
|
|
1
|
+
import type { DomainModel } from './domain-types';
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Unique symbol used as the key for branding types.
|
|
5
|
+
*/
|
|
6
|
+
export const $: unique symbol = Symbol('__prisma_next_brand__');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A helper type to brand a given type with a unique identifier.
|
|
10
|
+
*
|
|
11
|
+
* @template TKey Text used as the brand key.
|
|
12
|
+
* @template TValue Optional value associated with the brand key. Defaults to `true`.
|
|
13
|
+
*/
|
|
14
|
+
export type Brand<TKey extends string | number | symbol, TValue = true> = {
|
|
15
|
+
[$]: {
|
|
16
|
+
[K in TKey]: TValue;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Base type for storage contract hashes.
|
|
22
|
+
* Emitted contract.d.ts files use this with the hash value as a type parameter:
|
|
23
|
+
* `type StorageHash = StorageHashBase<'sha256:abc123...'>`
|
|
24
|
+
*/
|
|
25
|
+
export type StorageHashBase<THash extends string> = THash & Brand<'StorageHash'>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Base type for execution contract hashes.
|
|
29
|
+
* Emitted contract.d.ts files use this with the hash value as a type parameter:
|
|
30
|
+
* `type ExecutionHash = ExecutionHashBase<'sha256:def456...'>`
|
|
31
|
+
*/
|
|
32
|
+
export type ExecutionHashBase<THash extends string> = THash & Brand<'ExecutionHash'>;
|
|
33
|
+
|
|
34
|
+
export function executionHash<const T extends string>(value: T): ExecutionHashBase<T> {
|
|
35
|
+
return value as ExecutionHashBase<T>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function coreHash<const T extends string>(value: T): StorageHashBase<T> {
|
|
39
|
+
return value as StorageHashBase<T>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Base type for profile contract hashes.
|
|
44
|
+
* Emitted contract.d.ts files use this with the hash value as a type parameter:
|
|
45
|
+
* `type ProfileHash = ProfileHashBase<'sha256:def456...'>`
|
|
46
|
+
*/
|
|
47
|
+
export type ProfileHashBase<THash extends string> = THash & Brand<'ProfileHash'>;
|
|
48
|
+
|
|
49
|
+
export function profileHash<const T extends string>(value: T): ProfileHashBase<T> {
|
|
50
|
+
return value as ProfileHashBase<T>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Base type for family-specific storage blocks.
|
|
55
|
+
* Family storage types (SqlStorage, MongoStorage, etc.) extend this to carry the
|
|
56
|
+
* storage hash alongside family-specific data (tables, collections, etc.).
|
|
57
|
+
*/
|
|
58
|
+
export interface StorageBase<THash extends string = string> {
|
|
59
|
+
readonly storageHash: StorageHashBase<THash>;
|
|
60
|
+
}
|
|
61
|
+
|
|
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
|
+
> {
|
|
5
67
|
readonly schemaVersion: string;
|
|
6
68
|
readonly target: string;
|
|
7
69
|
readonly targetFamily: string;
|
|
8
|
-
readonly
|
|
9
|
-
readonly
|
|
70
|
+
readonly storageHash: TStorageHash;
|
|
71
|
+
readonly executionHash?: TExecutionHash | undefined;
|
|
72
|
+
readonly profileHash?: TProfileHash | undefined;
|
|
10
73
|
readonly capabilities: Record<string, Record<string, boolean>>;
|
|
11
74
|
readonly extensionPacks: Record<string, unknown>;
|
|
12
75
|
readonly meta: Record<string, unknown>;
|
|
13
76
|
readonly sources: Record<string, Source>;
|
|
77
|
+
readonly execution?: ExecutionSection;
|
|
78
|
+
readonly roots: Record<string, string>;
|
|
79
|
+
readonly models: Record<string, DomainModel>;
|
|
14
80
|
}
|
|
15
81
|
|
|
16
82
|
export interface FieldType {
|
|
@@ -20,6 +86,79 @@ export interface FieldType {
|
|
|
20
86
|
readonly properties?: Record<string, FieldType>;
|
|
21
87
|
}
|
|
22
88
|
|
|
89
|
+
export type GeneratedValueSpec = {
|
|
90
|
+
readonly id: string;
|
|
91
|
+
readonly params?: Record<string, unknown>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
95
|
+
|
|
96
|
+
export type JsonValue =
|
|
97
|
+
| JsonPrimitive
|
|
98
|
+
| { readonly [key: string]: JsonValue }
|
|
99
|
+
| readonly JsonValue[];
|
|
100
|
+
|
|
101
|
+
export type TaggedBigInt = { readonly $type: 'bigint'; readonly value: string };
|
|
102
|
+
|
|
103
|
+
export function isTaggedBigInt(value: unknown): value is TaggedBigInt {
|
|
104
|
+
return (
|
|
105
|
+
typeof value === 'object' &&
|
|
106
|
+
value !== null &&
|
|
107
|
+
(value as { $type?: unknown }).$type === 'bigint' &&
|
|
108
|
+
typeof (value as { value?: unknown }).value === 'string'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function bigintJsonReplacer(_key: string, value: unknown): unknown {
|
|
113
|
+
if (typeof value === 'bigint') {
|
|
114
|
+
return { $type: 'bigint', value: value.toString() } satisfies TaggedBigInt;
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type TaggedRaw = { readonly $type: 'raw'; readonly value: JsonValue };
|
|
120
|
+
|
|
121
|
+
export function isTaggedRaw(value: unknown): value is TaggedRaw {
|
|
122
|
+
return (
|
|
123
|
+
typeof value === 'object' &&
|
|
124
|
+
value !== null &&
|
|
125
|
+
(value as { $type?: unknown }).$type === 'raw' &&
|
|
126
|
+
'value' in (value as object)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type TaggedLiteralValue = TaggedBigInt | TaggedRaw;
|
|
131
|
+
|
|
132
|
+
export type ColumnDefaultLiteralValue = JsonValue | TaggedLiteralValue;
|
|
133
|
+
|
|
134
|
+
export type ColumnDefaultLiteralInputValue = ColumnDefaultLiteralValue | bigint | Date;
|
|
135
|
+
|
|
136
|
+
export type ColumnDefault =
|
|
137
|
+
| {
|
|
138
|
+
readonly kind: 'literal';
|
|
139
|
+
readonly value: ColumnDefaultLiteralInputValue;
|
|
140
|
+
}
|
|
141
|
+
| { readonly kind: 'function'; readonly expression: string };
|
|
142
|
+
|
|
143
|
+
export type ExecutionMutationDefaultValue = {
|
|
144
|
+
readonly kind: 'generator';
|
|
145
|
+
readonly id: GeneratedValueSpec['id'];
|
|
146
|
+
readonly params?: Record<string, unknown>;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type ExecutionMutationDefault = {
|
|
150
|
+
readonly ref: { readonly table: string; readonly column: string };
|
|
151
|
+
readonly onCreate?: ExecutionMutationDefaultValue;
|
|
152
|
+
readonly onUpdate?: ExecutionMutationDefaultValue;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export type ExecutionSection<THash extends string = string> = {
|
|
156
|
+
readonly executionHash: ExecutionHashBase<THash>;
|
|
157
|
+
readonly mutations: {
|
|
158
|
+
readonly defaults: ReadonlyArray<ExecutionMutationDefault>;
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
23
162
|
export interface Source {
|
|
24
163
|
readonly readOnly: boolean;
|
|
25
164
|
readonly projection: Record<string, FieldType>;
|
|
@@ -42,7 +181,7 @@ export type Expr =
|
|
|
42
181
|
export interface DocCollection {
|
|
43
182
|
readonly name: string;
|
|
44
183
|
readonly id?: {
|
|
45
|
-
readonly strategy: 'auto' | 'client' | 'uuid' | '
|
|
184
|
+
readonly strategy: 'auto' | 'client' | 'uuid' | 'objectId';
|
|
46
185
|
};
|
|
47
186
|
readonly fields: Record<string, FieldType>;
|
|
48
187
|
readonly indexes?: ReadonlyArray<DocIndex>;
|
|
@@ -55,7 +194,11 @@ export interface DocumentStorage {
|
|
|
55
194
|
};
|
|
56
195
|
}
|
|
57
196
|
|
|
58
|
-
export interface DocumentContract
|
|
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> {
|
|
59
202
|
// Accept string to work with JSON imports; runtime validation ensures 'document'
|
|
60
203
|
readonly targetFamily: string;
|
|
61
204
|
readonly storage: DocumentStorage;
|
|
@@ -68,7 +211,7 @@ export interface ParamDescriptor {
|
|
|
68
211
|
readonly codecId?: string;
|
|
69
212
|
readonly nativeType?: string;
|
|
70
213
|
readonly nullable?: boolean;
|
|
71
|
-
readonly source: 'dsl' | 'raw';
|
|
214
|
+
readonly source: 'dsl' | 'raw' | 'lane';
|
|
72
215
|
readonly refs?: { table: string; column: string };
|
|
73
216
|
}
|
|
74
217
|
|
|
@@ -85,7 +228,7 @@ export interface PlanRefs {
|
|
|
85
228
|
export interface PlanMeta {
|
|
86
229
|
readonly target: string;
|
|
87
230
|
readonly targetFamily?: string;
|
|
88
|
-
readonly
|
|
231
|
+
readonly storageHash: string;
|
|
89
232
|
readonly profileHash?: string;
|
|
90
233
|
readonly lane: string;
|
|
91
234
|
readonly annotations?: {
|
|
@@ -151,7 +294,7 @@ export function isDocumentContract(contract: unknown): contract is DocumentContr
|
|
|
151
294
|
* Represents the current contract identity for a database.
|
|
152
295
|
*/
|
|
153
296
|
export interface ContractMarkerRecord {
|
|
154
|
-
readonly
|
|
297
|
+
readonly storageHash: string;
|
|
155
298
|
readonly profileHash: string;
|
|
156
299
|
readonly contractJson: unknown | null;
|
|
157
300
|
readonly canonicalVersion: number | null;
|
|
@@ -159,84 +302,3 @@ export interface ContractMarkerRecord {
|
|
|
159
302
|
readonly appTag: string | null;
|
|
160
303
|
readonly meta: Record<string, unknown>;
|
|
161
304
|
}
|
|
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,93 @@
|
|
|
1
|
+
import { type } from 'arktype';
|
|
2
|
+
import type { Contract } from './contract-types';
|
|
3
|
+
import type { DomainContractShape, DomainValidationResult } from './validate-domain';
|
|
4
|
+
import { validateContractDomain } from './validate-domain';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Family-provided storage validator.
|
|
8
|
+
* SQL validates tables/columns/FKs; Mongo validates collections/embedding.
|
|
9
|
+
*/
|
|
10
|
+
export type StorageValidator = (contract: Contract) => void;
|
|
11
|
+
|
|
12
|
+
export interface ValidateContractResult {
|
|
13
|
+
readonly warnings: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ContractSchema = type({
|
|
17
|
+
target: 'string',
|
|
18
|
+
targetFamily: 'string',
|
|
19
|
+
roots: 'Record<string, string>',
|
|
20
|
+
models: 'Record<string, unknown>',
|
|
21
|
+
storage: 'Record<string, unknown>',
|
|
22
|
+
capabilities: 'Record<string, Record<string, boolean>>',
|
|
23
|
+
extensionPacks: 'Record<string, unknown>',
|
|
24
|
+
meta: 'Record<string, unknown>',
|
|
25
|
+
'execution?': {
|
|
26
|
+
'executionHash?': 'string',
|
|
27
|
+
mutations: {
|
|
28
|
+
defaults: 'unknown[]',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
'profileHash?': 'string',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function stripPersistenceFields(raw: Record<string, unknown>): Record<string, unknown> {
|
|
35
|
+
const { schemaVersion: _, sources: _s, ...rest } = raw;
|
|
36
|
+
return rest;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractDomainShape(contract: Contract): DomainContractShape {
|
|
40
|
+
return {
|
|
41
|
+
roots: contract.roots,
|
|
42
|
+
models: contract.models,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Framework-level contract validation (ADR 182).
|
|
48
|
+
*
|
|
49
|
+
* Three-pass validation:
|
|
50
|
+
* 1. **Structural validation** (arktype): verifies required fields exist with
|
|
51
|
+
* correct base types.
|
|
52
|
+
* 2. **Domain validation** (framework-owned): roots, relation targets,
|
|
53
|
+
* variant/base consistency, discriminators, ownership, orphans.
|
|
54
|
+
* 3. **Storage validation** (family-provided): SQL validates tables/columns/FKs;
|
|
55
|
+
* Mongo validates collections/embedding.
|
|
56
|
+
*
|
|
57
|
+
* JSON persistence fields (`schemaVersion`, `sources`) are stripped before
|
|
58
|
+
* validation — they are not part of the in-memory contract representation.
|
|
59
|
+
*
|
|
60
|
+
* @template TContract The fully-typed contract type (preserves literal types).
|
|
61
|
+
* @param value Raw contract value (e.g. parsed from JSON).
|
|
62
|
+
* @param storageValidator Family-specific storage validation function.
|
|
63
|
+
* @returns The validated contract with full literal types.
|
|
64
|
+
*/
|
|
65
|
+
export function validateContract<TContract extends Contract>(
|
|
66
|
+
value: unknown,
|
|
67
|
+
storageValidator: StorageValidator,
|
|
68
|
+
): TContract & ValidateContractResult {
|
|
69
|
+
if (typeof value !== 'object' || value === null) {
|
|
70
|
+
throw new Error('Contract must be a non-null object');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const stripped = stripPersistenceFields(value as Record<string, unknown>);
|
|
74
|
+
|
|
75
|
+
const parsed = ContractSchema(stripped);
|
|
76
|
+
if (parsed instanceof type.errors) {
|
|
77
|
+
throw new Error(`Invalid contract structure: ${parsed.summary}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Arktype verified the structural shape; Contract adds branded hash types and
|
|
81
|
+
// ContractModel generics that can't be expressed in the schema.
|
|
82
|
+
const contract = parsed as unknown as Contract;
|
|
83
|
+
|
|
84
|
+
const domainResult: DomainValidationResult = validateContractDomain(extractDomainShape(contract));
|
|
85
|
+
|
|
86
|
+
storageValidator(contract);
|
|
87
|
+
|
|
88
|
+
// TContract narrows Contract with literal types from the caller's contract.d.ts;
|
|
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
|
+
});
|
|
93
|
+
}
|