@prisma-next/contract 0.11.0-dev.5 → 0.11.0-dev.50
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/canonicalization-j__G4o7F.d.mts +69 -0
- package/dist/canonicalization-j__G4o7F.d.mts.map +1 -0
- package/dist/{contract-types-Bt2uyqs3.d.mts → contract-types-OSHtK6P5.d.mts} +7 -5
- package/dist/contract-types-OSHtK6P5.d.mts.map +1 -0
- package/dist/contract-validation-error-Dp2vHZt5.mjs.map +1 -1
- package/dist/cross-reference-t0TDbBTP.d.mts +18 -0
- package/dist/cross-reference-t0TDbBTP.d.mts.map +1 -0
- package/dist/{hashing-rZiqFOlc.mjs → hashing-C25nwocN.mjs} +23 -76
- package/dist/hashing-C25nwocN.mjs.map +1 -0
- package/dist/hashing-utils.d.mts +19 -0
- package/dist/hashing-utils.d.mts.map +1 -0
- package/dist/hashing-utils.mjs +71 -0
- package/dist/hashing-utils.mjs.map +1 -0
- package/dist/hashing.d.mts +8 -37
- package/dist/hashing.d.mts.map +1 -1
- package/dist/hashing.mjs +1 -1
- package/dist/testing.d.mts +6 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +4 -2
- package/dist/testing.mjs.map +1 -1
- package/dist/types-CVGwkRLa.mjs.map +1 -1
- package/dist/types.d.mts +3 -2
- package/dist/types.mjs +24 -1
- package/dist/types.mjs.map +1 -0
- package/dist/validate-domain.d.mts +5 -3
- package/dist/validate-domain.d.mts.map +1 -1
- package/dist/validate-domain.mjs +16 -10
- package/dist/validate-domain.mjs.map +1 -1
- package/package.json +6 -5
- package/src/canonicalization-path-match.ts +44 -0
- package/src/canonicalization-storage-sort.ts +88 -0
- package/src/canonicalization.ts +57 -142
- package/src/contract-types.ts +2 -1
- package/src/cross-reference.ts +28 -0
- package/src/domain-types.ts +5 -3
- package/src/exports/hashing-utils.ts +12 -0
- package/src/exports/hashing.ts +2 -0
- package/src/exports/types.ts +4 -0
- package/src/hashing.ts +27 -10
- package/src/namespace-id.ts +10 -0
- package/src/testing-factories.ts +7 -1
- package/src/validate-domain.ts +23 -16
- package/dist/contract-types-Bt2uyqs3.d.mts.map +0 -1
- package/dist/hashing-rZiqFOlc.mjs.map +0 -1
package/src/canonicalization.ts
CHANGED
|
@@ -14,6 +14,25 @@ import type { Contract } from './contract-types';
|
|
|
14
14
|
*/
|
|
15
15
|
export type SerializeContract = (contract: Contract) => JsonObject;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Family-contributed predicate for the default-omission walk. Called when
|
|
19
|
+
* a value at `path` is a default (empty object/array or `false`); if this
|
|
20
|
+
* returns `true` the value is kept rather than stripped.
|
|
21
|
+
*
|
|
22
|
+
* The framework only calls the predicate inside the `isDefaultValue` branch,
|
|
23
|
+
* so there is no need to guard against non-default values.
|
|
24
|
+
*/
|
|
25
|
+
export type PreserveEmptyPredicate = (path: readonly string[]) => boolean;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Family-contributed storage sort. Applied to the serialized `storage`
|
|
29
|
+
* subtree after the default-omission walk; the result replaces the
|
|
30
|
+
* `storage` field before the final key sort. Use to establish a
|
|
31
|
+
* deterministic order for storage arrays (indexes, uniques) that the
|
|
32
|
+
* family-agnostic `sortObjectKeys` pass cannot handle.
|
|
33
|
+
*/
|
|
34
|
+
export type StorageSort = (storage: unknown) => unknown;
|
|
35
|
+
|
|
17
36
|
const TOP_LEVEL_ORDER = [
|
|
18
37
|
'schemaVersion',
|
|
19
38
|
'canonicalVersion',
|
|
@@ -42,13 +61,17 @@ function isDefaultValue(value: unknown): boolean {
|
|
|
42
61
|
return false;
|
|
43
62
|
}
|
|
44
63
|
|
|
45
|
-
function omitDefaults(
|
|
64
|
+
function omitDefaults(
|
|
65
|
+
obj: unknown,
|
|
66
|
+
path: readonly string[],
|
|
67
|
+
shouldPreserveEmpty: PreserveEmptyPredicate | undefined,
|
|
68
|
+
): unknown {
|
|
46
69
|
if (obj === null || typeof obj !== 'object') {
|
|
47
70
|
return obj;
|
|
48
71
|
}
|
|
49
72
|
|
|
50
73
|
if (Array.isArray(obj)) {
|
|
51
|
-
return obj.map((item) => omitDefaults(item, path));
|
|
74
|
+
return obj.map((item) => omitDefaults(item, path, shouldPreserveEmpty));
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
const result: Record<string, unknown> = {};
|
|
@@ -74,20 +97,6 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
74
97
|
const isNamespaceSlot =
|
|
75
98
|
currentPath.length === 3 &&
|
|
76
99
|
isArrayEqual([currentPath[0], currentPath[1]], ['storage', 'namespaces']);
|
|
77
|
-
const isRequiredNamespaceTables =
|
|
78
|
-
currentPath.length === 4 &&
|
|
79
|
-
currentPath[0] === 'storage' &&
|
|
80
|
-
currentPath[1] === 'namespaces' &&
|
|
81
|
-
currentPath[3] === 'tables';
|
|
82
|
-
// Preserve per-table payloads even when empty. SQL tables are never
|
|
83
|
-
// emitted empty; Mongo collections legitimately are (a declared
|
|
84
|
-
// collection with no schema is a valid representation), and the
|
|
85
|
-
// family-agnostic canonicalizer must not strip them.
|
|
86
|
-
const isNamespaceTableEntry =
|
|
87
|
-
currentPath.length === 5 &&
|
|
88
|
-
currentPath[0] === 'storage' &&
|
|
89
|
-
currentPath[1] === 'namespaces' &&
|
|
90
|
-
currentPath[3] === 'tables';
|
|
91
100
|
const isRequiredRoots = isArrayEqual(currentPath, ['roots']);
|
|
92
101
|
const isRequiredExtensionPacks = isArrayEqual(currentPath, ['extensionPacks']);
|
|
93
102
|
const isRequiredCapabilities = isArrayEqual(currentPath, ['capabilities']);
|
|
@@ -104,50 +113,21 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
104
113
|
const isModelStorage =
|
|
105
114
|
currentPath.length === 3 &&
|
|
106
115
|
isArrayEqual([currentPath[0], currentPath[2]], ['models', 'storage']);
|
|
107
|
-
const isNamespaceTableUniques =
|
|
108
|
-
currentPath.length === 6 &&
|
|
109
|
-
currentPath[0] === 'storage' &&
|
|
110
|
-
currentPath[1] === 'namespaces' &&
|
|
111
|
-
currentPath[3] === 'tables' &&
|
|
112
|
-
currentPath[5] === 'uniques';
|
|
113
|
-
const isNamespaceTableIndexes =
|
|
114
|
-
currentPath.length === 6 &&
|
|
115
|
-
currentPath[0] === 'storage' &&
|
|
116
|
-
currentPath[1] === 'namespaces' &&
|
|
117
|
-
currentPath[3] === 'tables' &&
|
|
118
|
-
currentPath[5] === 'indexes';
|
|
119
|
-
const isNamespaceTableForeignKeys =
|
|
120
|
-
currentPath.length === 6 &&
|
|
121
|
-
currentPath[0] === 'storage' &&
|
|
122
|
-
currentPath[1] === 'namespaces' &&
|
|
123
|
-
currentPath[3] === 'tables' &&
|
|
124
|
-
currentPath[5] === 'foreignKeys';
|
|
125
116
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
currentPath.length === 4 &&
|
|
131
|
-
currentPath[0] === 'storage' &&
|
|
132
|
-
currentPath[1] === 'types' &&
|
|
117
|
+
const isDomainUnboundTypeParams =
|
|
118
|
+
currentPath.length === 5 &&
|
|
119
|
+
currentPath[0] === 'domain' &&
|
|
120
|
+
currentPath[2] === 'types' &&
|
|
133
121
|
key === 'typeParams';
|
|
134
122
|
|
|
135
|
-
const isFkBooleanField =
|
|
136
|
-
currentPath.length === 7 &&
|
|
137
|
-
currentPath[0] === 'storage' &&
|
|
138
|
-
currentPath[1] === 'namespaces' &&
|
|
139
|
-
currentPath[3] === 'tables' &&
|
|
140
|
-
currentPath[5] === 'foreignKeys' &&
|
|
141
|
-
(key === 'constraint' || key === 'index');
|
|
142
|
-
|
|
143
123
|
const isNullableField = key === 'nullable';
|
|
144
124
|
|
|
125
|
+
const isFamilyPreserved = shouldPreserveEmpty?.(currentPath) ?? false;
|
|
126
|
+
|
|
145
127
|
if (
|
|
146
128
|
!isRequiredModels &&
|
|
147
129
|
!isRequiredNamespaces &&
|
|
148
130
|
!isNamespaceSlot &&
|
|
149
|
-
!isRequiredNamespaceTables &&
|
|
150
|
-
!isNamespaceTableEntry &&
|
|
151
131
|
!isRequiredRoots &&
|
|
152
132
|
!isRequiredExtensionPacks &&
|
|
153
133
|
!isRequiredCapabilities &&
|
|
@@ -156,18 +136,15 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
156
136
|
!isExtensionNamespace &&
|
|
157
137
|
!isModelRelations &&
|
|
158
138
|
!isModelStorage &&
|
|
159
|
-
!isNamespaceTableUniques &&
|
|
160
|
-
!isNamespaceTableIndexes &&
|
|
161
|
-
!isNamespaceTableForeignKeys &&
|
|
162
|
-
!isFkBooleanField &&
|
|
163
139
|
!isNullableField &&
|
|
164
|
-
!
|
|
140
|
+
!isDomainUnboundTypeParams &&
|
|
141
|
+
!isFamilyPreserved
|
|
165
142
|
) {
|
|
166
143
|
continue;
|
|
167
144
|
}
|
|
168
145
|
}
|
|
169
146
|
|
|
170
|
-
result[key] = omitDefaults(value, currentPath);
|
|
147
|
+
result[key] = omitDefaults(value, currentPath, shouldPreserveEmpty);
|
|
171
148
|
}
|
|
172
149
|
|
|
173
150
|
return result;
|
|
@@ -191,88 +168,6 @@ function sortObjectKeys(obj: unknown): unknown {
|
|
|
191
168
|
return sorted;
|
|
192
169
|
}
|
|
193
170
|
|
|
194
|
-
type NamespaceObject = {
|
|
195
|
-
tables?: Record<string, unknown>;
|
|
196
|
-
[key: string]: unknown;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
type StorageObject = {
|
|
200
|
-
namespaces?: Record<string, unknown>;
|
|
201
|
-
[key: string]: unknown;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
type TableObject = {
|
|
205
|
-
indexes?: unknown[];
|
|
206
|
-
uniques?: unknown[];
|
|
207
|
-
[key: string]: unknown;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
function sortTableArrays(tableObj: TableObject): TableObject {
|
|
211
|
-
const sortedTable: TableObject = { ...tableObj };
|
|
212
|
-
|
|
213
|
-
if (Array.isArray(tableObj.indexes)) {
|
|
214
|
-
sortedTable.indexes = [...tableObj.indexes].sort((a, b) => {
|
|
215
|
-
const nameA = (a as { name?: string })?.name || '';
|
|
216
|
-
const nameB = (b as { name?: string })?.name || '';
|
|
217
|
-
return nameA.localeCompare(nameB);
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (Array.isArray(tableObj.uniques)) {
|
|
222
|
-
sortedTable.uniques = [...tableObj.uniques].sort((a, b) => {
|
|
223
|
-
const nameA = (a as { name?: string })?.name || '';
|
|
224
|
-
const nameB = (b as { name?: string })?.name || '';
|
|
225
|
-
return nameA.localeCompare(nameB);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return sortedTable;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function sortIndexesAndUniques(storage: unknown): unknown {
|
|
233
|
-
if (!storage || typeof storage !== 'object') {
|
|
234
|
-
return storage;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const storageObj = storage as StorageObject;
|
|
238
|
-
if (!storageObj.namespaces || typeof storageObj.namespaces !== 'object') {
|
|
239
|
-
return storage;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const namespaces = storageObj.namespaces;
|
|
243
|
-
const result: StorageObject = { ...storageObj, namespaces: {} };
|
|
244
|
-
const resultNamespaces = result.namespaces as Record<string, unknown>;
|
|
245
|
-
|
|
246
|
-
for (const nsId of Object.keys(namespaces)) {
|
|
247
|
-
const ns = namespaces[nsId];
|
|
248
|
-
if (!ns || typeof ns !== 'object') {
|
|
249
|
-
resultNamespaces[nsId] = ns;
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const nsObj = ns as NamespaceObject;
|
|
254
|
-
if (!nsObj.tables || typeof nsObj.tables !== 'object') {
|
|
255
|
-
resultNamespaces[nsId] = ns;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const sortedTables: Record<string, unknown> = {};
|
|
260
|
-
const sortedTableNames = Object.keys(nsObj.tables).sort();
|
|
261
|
-
for (const tableName of sortedTableNames) {
|
|
262
|
-
const table = nsObj.tables[tableName];
|
|
263
|
-
if (!table || typeof table !== 'object') {
|
|
264
|
-
sortedTables[tableName] = table;
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
sortedTables[tableName] = sortTableArrays(table as TableObject);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
resultNamespaces[nsId] = { ...nsObj, tables: sortedTables };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return result;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
171
|
export function orderTopLevel(obj: Record<string, unknown>): Record<string, unknown> {
|
|
277
172
|
const ordered: Record<string, unknown> = {};
|
|
278
173
|
const remaining = new Set(Object.keys(obj));
|
|
@@ -304,6 +199,21 @@ export interface CanonicalizeContractOptions {
|
|
|
304
199
|
* the per-target serializer not putting them in the JSON shape.
|
|
305
200
|
*/
|
|
306
201
|
readonly serializeContract: SerializeContract;
|
|
202
|
+
/**
|
|
203
|
+
* Family-contributed preserve-empty predicate. When the walk encounters a
|
|
204
|
+
* default value (empty object/array or `false`) at `path`, calling this
|
|
205
|
+
* with the full path allows the family to veto the omission. If absent,
|
|
206
|
+
* only the framework's family-agnostic required-slot rules apply.
|
|
207
|
+
*/
|
|
208
|
+
readonly shouldPreserveEmpty?: PreserveEmptyPredicate;
|
|
209
|
+
/**
|
|
210
|
+
* Family-contributed storage sort. Applied to the serialized `storage`
|
|
211
|
+
* subtree after the default-omission walk, before the final key sort.
|
|
212
|
+
* SQL family uses this to impose a deterministic order on `indexes` and
|
|
213
|
+
* `uniques` arrays within each namespace table. Families that require no
|
|
214
|
+
* special storage ordering omit this hook.
|
|
215
|
+
*/
|
|
216
|
+
readonly sortStorage?: StorageSort;
|
|
307
217
|
}
|
|
308
218
|
|
|
309
219
|
/**
|
|
@@ -324,15 +234,20 @@ export function canonicalizeContractToObject(
|
|
|
324
234
|
roots: serialized['roots'],
|
|
325
235
|
models: serialized['models'],
|
|
326
236
|
...ifDefined('valueObjects', serialized['valueObjects']),
|
|
237
|
+
...ifDefined('domain', serialized['domain']),
|
|
327
238
|
storage: serialized['storage'],
|
|
328
239
|
...ifDefined('execution', serialized['execution']),
|
|
329
240
|
extensionPacks: serialized['extensionPacks'],
|
|
330
241
|
capabilities: serialized['capabilities'],
|
|
331
242
|
meta: serialized['meta'],
|
|
332
243
|
};
|
|
333
|
-
const withDefaultsOmitted = omitDefaults(normalized, []) as Record<
|
|
334
|
-
|
|
335
|
-
|
|
244
|
+
const withDefaultsOmitted = omitDefaults(normalized, [], options.shouldPreserveEmpty) as Record<
|
|
245
|
+
string,
|
|
246
|
+
unknown
|
|
247
|
+
>;
|
|
248
|
+
const withSortedStorage = options.sortStorage
|
|
249
|
+
? { ...withDefaultsOmitted, storage: options.sortStorage(withDefaultsOmitted['storage']) }
|
|
250
|
+
: withDefaultsOmitted;
|
|
336
251
|
const withSortedKeys = sortObjectKeys(withSortedStorage) as Record<string, unknown>;
|
|
337
252
|
return orderTopLevel(withSortedKeys);
|
|
338
253
|
}
|
package/src/contract-types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CrossReference } from './cross-reference';
|
|
1
2
|
import type { ContractModelBase, ContractValueObject } from './domain-types';
|
|
2
3
|
import type {
|
|
3
4
|
ExecutionHashBase,
|
|
@@ -43,7 +44,7 @@ export interface Contract<
|
|
|
43
44
|
> {
|
|
44
45
|
readonly target: string;
|
|
45
46
|
readonly targetFamily: string;
|
|
46
|
-
readonly roots: Record<string,
|
|
47
|
+
readonly roots: Record<string, CrossReference>;
|
|
47
48
|
readonly models: TModels;
|
|
48
49
|
readonly valueObjects?: Record<string, ContractValueObject>;
|
|
49
50
|
/**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
2
|
+
import { type Type, type } from 'arktype';
|
|
3
|
+
import { asNamespaceId, type NamespaceId } from './namespace-id';
|
|
4
|
+
|
|
5
|
+
export interface CrossReference {
|
|
6
|
+
readonly namespace: NamespaceId;
|
|
7
|
+
readonly model: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const CrossReferenceSchema = blindCast<
|
|
11
|
+
Type<CrossReference>,
|
|
12
|
+
'namespace is validated as string at runtime and branded to NamespaceId by asNamespaceId in crossRef(); the schema accepts plain strings but the public type reflects the branded shape'
|
|
13
|
+
>(
|
|
14
|
+
type({
|
|
15
|
+
'+': 'reject',
|
|
16
|
+
namespace: 'string',
|
|
17
|
+
model: 'string',
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const DEFAULT_CROSS_REF_NAMESPACE = '__unbound__';
|
|
22
|
+
|
|
23
|
+
export function crossRef(
|
|
24
|
+
model: string,
|
|
25
|
+
namespace: string = DEFAULT_CROSS_REF_NAMESPACE,
|
|
26
|
+
): CrossReference {
|
|
27
|
+
return { namespace: asNamespaceId(namespace), model };
|
|
28
|
+
}
|
package/src/domain-types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CrossReference } from './cross-reference';
|
|
2
|
+
|
|
1
3
|
export type ScalarFieldType = {
|
|
2
4
|
readonly kind: 'scalar';
|
|
3
5
|
readonly codecId: string;
|
|
@@ -29,13 +31,13 @@ export type ContractRelationOn = {
|
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export type ContractReferenceRelation = {
|
|
32
|
-
readonly to:
|
|
34
|
+
readonly to: CrossReference;
|
|
33
35
|
readonly cardinality: '1:1' | '1:N' | 'N:1';
|
|
34
36
|
readonly on: ContractRelationOn;
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
export type ContractEmbedRelation = {
|
|
38
|
-
readonly to:
|
|
40
|
+
readonly to: CrossReference;
|
|
39
41
|
readonly cardinality: '1:1' | '1:N';
|
|
40
42
|
};
|
|
41
43
|
|
|
@@ -61,7 +63,7 @@ export interface ContractModelBase<TModelStorage extends ModelStorageBase = Mode
|
|
|
61
63
|
readonly storage: TModelStorage;
|
|
62
64
|
readonly discriminator?: ContractDiscriminator;
|
|
63
65
|
readonly variants?: Record<string, ContractVariantEntry>;
|
|
64
|
-
readonly base?:
|
|
66
|
+
readonly base?: CrossReference;
|
|
65
67
|
readonly owner?: string;
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createPreserveEmptyPredicate,
|
|
3
|
+
matchesPathPattern,
|
|
4
|
+
type PathPattern,
|
|
5
|
+
type PathSegment as PreserveEmptyPathSegment,
|
|
6
|
+
} from '../canonicalization-path-match';
|
|
7
|
+
export {
|
|
8
|
+
compareByNameProperty,
|
|
9
|
+
createStorageSort,
|
|
10
|
+
type NamedArraySortTarget,
|
|
11
|
+
type PathSegment as StorageSortPathSegment,
|
|
12
|
+
} from '../canonicalization-storage-sort';
|
package/src/exports/hashing.ts
CHANGED
|
@@ -2,6 +2,8 @@ export {
|
|
|
2
2
|
type CanonicalizeContractOptions,
|
|
3
3
|
canonicalizeContract,
|
|
4
4
|
canonicalizeContractToObject,
|
|
5
|
+
type PreserveEmptyPredicate,
|
|
5
6
|
type SerializeContract,
|
|
7
|
+
type StorageSort,
|
|
6
8
|
} from '../canonicalization';
|
|
7
9
|
export { computeExecutionHash, computeProfileHash, computeStorageHash } from '../hashing';
|
package/src/exports/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type { Contract, ContractExecutionSection } from '../contract-types';
|
|
2
|
+
export type { CrossReference } from '../cross-reference';
|
|
3
|
+
export { CrossReferenceSchema, crossRef } from '../cross-reference';
|
|
2
4
|
export type {
|
|
3
5
|
ContractDiscriminator,
|
|
4
6
|
ContractEmbedRelation,
|
|
@@ -18,6 +20,8 @@ export type {
|
|
|
18
20
|
UnionFieldType,
|
|
19
21
|
ValueObjectFieldType,
|
|
20
22
|
} from '../domain-types';
|
|
23
|
+
export type { NamespaceId } from '../namespace-id';
|
|
24
|
+
export { asNamespaceId } from '../namespace-id';
|
|
21
25
|
export type {
|
|
22
26
|
$,
|
|
23
27
|
Brand,
|
package/src/hashing.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
3
|
import type { JsonObject } from '@prisma-next/utils/json';
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
canonicalizeContract,
|
|
6
|
+
type PreserveEmptyPredicate,
|
|
7
|
+
type StorageSort,
|
|
8
|
+
} from './canonicalization';
|
|
4
9
|
import type { Contract } from './contract-types';
|
|
5
10
|
import type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from './types';
|
|
6
11
|
|
|
@@ -12,7 +17,13 @@ function sha256(content: string): string {
|
|
|
12
17
|
return `sha256:${hash.digest('hex')}`;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
type HashContractSection = Record<string, unknown> & {
|
|
21
|
+
readonly shouldPreserveEmpty?: PreserveEmptyPredicate;
|
|
22
|
+
readonly sortStorage?: StorageSort;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function hashContract(section: HashContractSection): string {
|
|
26
|
+
const { shouldPreserveEmpty, sortStorage, ...sectionData } = section;
|
|
16
27
|
// Blind cast: the synthesised object is a hash-only stand-in
|
|
17
28
|
// — never returned to callers, never executed as a Contract.
|
|
18
29
|
// `canonicalizeContract` only walks the storage / execution /
|
|
@@ -20,29 +31,35 @@ function hashContract(section: Record<string, unknown>): string {
|
|
|
20
31
|
// missing precise Contract typing on the other slots is
|
|
21
32
|
// immaterial for the hash result.
|
|
22
33
|
const contract = {
|
|
23
|
-
targetFamily:
|
|
24
|
-
target:
|
|
34
|
+
targetFamily: sectionData['targetFamily'],
|
|
35
|
+
target: sectionData['target'],
|
|
25
36
|
roots: {},
|
|
26
37
|
models: {},
|
|
27
|
-
storage:
|
|
28
|
-
execution:
|
|
38
|
+
storage: sectionData['storage'] ?? {},
|
|
39
|
+
execution: sectionData['execution'],
|
|
29
40
|
extensionPacks: {},
|
|
30
|
-
capabilities:
|
|
41
|
+
capabilities: sectionData['capabilities'] ?? {},
|
|
31
42
|
meta: {},
|
|
32
43
|
profileHash: '',
|
|
33
|
-
...
|
|
44
|
+
...sectionData,
|
|
34
45
|
} as unknown as Contract;
|
|
35
46
|
return canonicalizeContract(contract, {
|
|
36
47
|
schemaVersion: SCHEMA_VERSION,
|
|
37
48
|
serializeContract: (c) => JSON.parse(JSON.stringify(c)) as JsonObject,
|
|
49
|
+
...ifDefined('shouldPreserveEmpty', shouldPreserveEmpty),
|
|
50
|
+
...ifDefined('sortStorage', sortStorage),
|
|
38
51
|
});
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
export
|
|
54
|
+
export type ComputeStorageHashArgs = {
|
|
42
55
|
target: string;
|
|
43
56
|
targetFamily: string;
|
|
44
57
|
storage: Record<string, unknown>;
|
|
45
|
-
|
|
58
|
+
readonly shouldPreserveEmpty?: PreserveEmptyPredicate;
|
|
59
|
+
readonly sortStorage?: StorageSort;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function computeStorageHash(args: ComputeStorageHashArgs): StorageHashBase<string> {
|
|
46
63
|
return sha256(hashContract(args)) as StorageHashBase<string>;
|
|
47
64
|
}
|
|
48
65
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
2
|
+
|
|
3
|
+
export type NamespaceId = string & { readonly __brand: 'NamespaceId' };
|
|
4
|
+
|
|
5
|
+
export function asNamespaceId(value: string): NamespaceId {
|
|
6
|
+
return blindCast<
|
|
7
|
+
NamespaceId,
|
|
8
|
+
'NamespaceId is a compile-time-only brand on string; this factory is the sole assertion site'
|
|
9
|
+
>(value);
|
|
10
|
+
}
|
package/src/testing-factories.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
|
+
import type { PreserveEmptyPredicate, StorageSort } from './canonicalization';
|
|
2
3
|
import type { Contract } from './contract-types';
|
|
4
|
+
import type { CrossReference } from './cross-reference';
|
|
3
5
|
import type {
|
|
4
6
|
ContractModel,
|
|
5
7
|
ContractModelBase,
|
|
@@ -16,7 +18,7 @@ type ContractOverrides<
|
|
|
16
18
|
> = {
|
|
17
19
|
target?: string;
|
|
18
20
|
targetFamily?: string;
|
|
19
|
-
roots?: Record<string,
|
|
21
|
+
roots?: Record<string, CrossReference>;
|
|
20
22
|
models?: TModels;
|
|
21
23
|
storage?: Omit<TStorage, 'storageHash'>;
|
|
22
24
|
valueObjects?: Record<string, ContractValueObject>;
|
|
@@ -25,6 +27,8 @@ type ContractOverrides<
|
|
|
25
27
|
execution?: Omit<ExecutionSection, 'executionHash'>;
|
|
26
28
|
profileHash?: ProfileHashBase<string>;
|
|
27
29
|
meta?: Record<string, unknown>;
|
|
30
|
+
shouldPreserveEmpty?: PreserveEmptyPredicate;
|
|
31
|
+
sortStorage?: StorageSort;
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
const DUMMY_HASH = coreHash('sha256:test');
|
|
@@ -56,6 +60,8 @@ export function createContract<
|
|
|
56
60
|
target,
|
|
57
61
|
targetFamily,
|
|
58
62
|
storage: rawStorage as Record<string, unknown>,
|
|
63
|
+
...ifDefined('shouldPreserveEmpty', overrides.shouldPreserveEmpty),
|
|
64
|
+
...ifDefined('sortStorage', overrides.sortStorage),
|
|
59
65
|
});
|
|
60
66
|
|
|
61
67
|
const storage = {
|
package/src/validate-domain.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { ContractValidationError } from './contract-validation-error';
|
|
2
|
+
import type { CrossReference } from './cross-reference';
|
|
2
3
|
|
|
3
4
|
export interface DomainModelShape {
|
|
4
5
|
readonly fields: Record<string, unknown>;
|
|
5
|
-
readonly relations?: Record<string, { readonly to:
|
|
6
|
+
readonly relations?: Record<string, { readonly to: CrossReference }>;
|
|
6
7
|
readonly discriminator?: { readonly field: string };
|
|
7
8
|
readonly variants?: Record<string, unknown>;
|
|
8
|
-
readonly base?:
|
|
9
|
+
readonly base?: CrossReference;
|
|
9
10
|
readonly owner?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface DomainContractShape {
|
|
13
|
-
readonly roots: Record<string,
|
|
14
|
+
readonly roots: Record<string, CrossReference>;
|
|
14
15
|
readonly models: Record<string, DomainModelShape>;
|
|
15
16
|
readonly valueObjects?: Record<string, { readonly fields: Record<string, unknown> }>;
|
|
16
17
|
}
|
|
@@ -41,11 +42,13 @@ function validateRoots(
|
|
|
41
42
|
errors: string[],
|
|
42
43
|
): void {
|
|
43
44
|
const seenValues = new Set<string>();
|
|
44
|
-
for (const [rootKey,
|
|
45
|
-
|
|
45
|
+
for (const [rootKey, crossRef] of Object.entries(contract.roots)) {
|
|
46
|
+
const modelName = crossRef.model;
|
|
47
|
+
const dedupeKey = `${crossRef.namespace}:${modelName}`;
|
|
48
|
+
if (seenValues.has(dedupeKey)) {
|
|
46
49
|
errors.push(`Duplicate root value: "${modelName}" is mapped by multiple root keys`);
|
|
47
50
|
}
|
|
48
|
-
seenValues.add(
|
|
51
|
+
seenValues.add(dedupeKey);
|
|
49
52
|
|
|
50
53
|
if (!modelNames.has(modelName)) {
|
|
51
54
|
errors.push(
|
|
@@ -73,24 +76,27 @@ function validateVariantsAndBases(
|
|
|
73
76
|
}
|
|
74
77
|
const variantModel = models.get(variantName);
|
|
75
78
|
if (!variantModel) continue;
|
|
76
|
-
if (variantModel.base !== modelName) {
|
|
79
|
+
if (variantModel.base?.model !== modelName) {
|
|
77
80
|
errors.push(
|
|
78
|
-
`Variant "${variantName}" has base "${variantModel.base ?? '(none)'}" but expected "${modelName}"`,
|
|
81
|
+
`Variant "${variantName}" has base "${variantModel.base?.model ?? '(none)'}" but expected "${modelName}"`,
|
|
79
82
|
);
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
if (model.base) {
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
const baseModelName = model.base.model;
|
|
89
|
+
if (!modelNames.has(baseModelName)) {
|
|
90
|
+
errors.push(
|
|
91
|
+
`Model "${modelName}" has base "${baseModelName}" which does not exist in models`,
|
|
92
|
+
);
|
|
87
93
|
continue;
|
|
88
94
|
}
|
|
89
|
-
const baseModel = models.get(
|
|
95
|
+
const baseModel = models.get(baseModelName);
|
|
90
96
|
if (!baseModel) continue;
|
|
91
97
|
if (!baseModel.variants || !Object.hasOwn(baseModel.variants, modelName)) {
|
|
92
98
|
errors.push(
|
|
93
|
-
`Model "${modelName}" has base "${
|
|
99
|
+
`Model "${modelName}" has base "${baseModelName}" which does not list it as a variant`,
|
|
94
100
|
);
|
|
95
101
|
}
|
|
96
102
|
}
|
|
@@ -104,9 +110,10 @@ function validateRelationTargets(
|
|
|
104
110
|
): void {
|
|
105
111
|
for (const [modelName, model] of Object.entries(contract.models)) {
|
|
106
112
|
for (const [relName, relation] of Object.entries(model.relations ?? {})) {
|
|
107
|
-
|
|
113
|
+
const targetModelName = relation.to.model;
|
|
114
|
+
if (!modelNames.has(targetModelName)) {
|
|
108
115
|
errors.push(
|
|
109
|
-
`Relation "${relName}" on model "${modelName}" targets "${
|
|
116
|
+
`Relation "${relName}" on model "${modelName}" targets "${targetModelName}" which does not exist in models`,
|
|
110
117
|
);
|
|
111
118
|
}
|
|
112
119
|
}
|
|
@@ -157,8 +164,8 @@ function validateOwnership(
|
|
|
157
164
|
errors.push(`Model "${modelName}" has owner "${model.owner}" which does not exist in models`);
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
for (const [rootKey,
|
|
161
|
-
if (
|
|
167
|
+
for (const [rootKey, rootRef] of Object.entries(contract.roots)) {
|
|
168
|
+
if (rootRef.model === modelName) {
|
|
162
169
|
errors.push(
|
|
163
170
|
`Owned model "${modelName}" must not appear in roots (found as root "${rootKey}")`,
|
|
164
171
|
);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"contract-types-Bt2uyqs3.d.mts","names":[],"sources":["../src/domain-types.ts","../src/types.ts","../src/contract-types.ts"],"mappings":";KAAY,eAAA;EAAA,SACD,IAAA;EAAA,SACA,OAAA;EAAA,SACA,UAAA,GAAa,MAAA;AAAA;AAAA,KAGZ,oBAAA;EAAA,SACD,IAAA;EAAA,SACA,IAAA;AAAA;AAAA,KAGC,cAAA;EAAA,SACD,IAAA;EAAA,SACA,OAAA,EAAS,aAAA,CAAc,eAAA,GAAkB,oBAAA;AAAA;AAAA,KAGxC,iBAAA,GAAoB,eAAA,GAAkB,oBAAA,GAAuB,cAAA;AAAA,KAE7D,aAAA;EAAA,SACD,QAAA;EAAA,SACA,IAAA,EAAM,iBAAA;EAAA,SACN,IAAA;EAAA,SACA,IAAA;AAAA;AAAA,KAGC,kBAAA;EAAA,SACD,WAAA;EAAA,SACA,YAAA;AAAA;AAAA,KAGC,yBAAA;EAAA,SACD,EAAA;EAAA,SACA,WAAA;EAAA,SACA,EAAA,EAAI,kBAAA;AAAA;AAAA,KAGH,qBAAA;EAAA,SACD,EAAA;EAAA,SACA,WAAA;AAAA;AAAA,KAGC,gBAAA,GAAmB,yBAAA,GAA4B,qBAAA;AAAA,KAE/C,qBAAA;EAAA,SACD,KAAA;AAAA;AAAA,KAGC,oBAAA;EAAA,SACD,KAAA;AAAA;AAAA,KAGC,mBAAA;EAAA,SACD,MAAA,EAAQ,MAAA,SAAe,aAAA;AAAA;AAAA,KAGtB,gBAAA,GAAmB,QAAA,CAAS,MAAA;AAAA,UAEvB,iBAAA,uBAAwC,gBAAA,GAAmB,gBAAA;EAAA,SACjE,MAAA,EAAQ,MAAA,SAAe,aAAA;EAAA,SACvB,SAAA,EAAW,MAAA,SAAe,gBAAA;EAAA,SAC1B,OAAA,EAAS,aAAA;EAAA,SACT,aAAA,GAAgB,qBAAA;EAAA,SAChB,QAAA,GAAW,MAAA,SAAe,oBAAA;EAAA,SAC1B,IAAA;EAAA,SACA,KAAA;AAAA;AAAA,UAGM,aAAA,uBAAoC,gBAAA,GAAmB,gBAAA,UAC9D,iBAAA,CAAkB,aAAA;EAAA,SACjB,MAAA,EAAQ,MAAA,SAAe,aAAA;AAAA;AAAA,KAK7B,sBAAA;EAAA,SACM,MAAA,EAAQ,MAAA;IAAA,SAA0B,SAAA,EAAW,MAAA,SAAe,gBAAA;EAAA;AAAA;AAAA,KAG3D,qBAAA,mBACQ,sBAAA,mCACe,SAAA,4BAErB,SAAA,WAAoB,SAAA,iBAA0B,SAAA,WAAoB,SAAA,eAAwB,CAAA,UAAW,yBAAA,GAC7G,CAAA,iBAEE,SAAA,WAAoB,SAAA;AAAA,KAEhB,iBAAA,mBACQ,sBAAA,mCACe,SAAA,4BAErB,SAAA,WAAoB,SAAA,iBAA0B,SAAA,WAAoB,SAAA,eAAwB,CAAA,UAAW,yBAAA,WAE7G,CAAA,SACE,SAAA,WAAoB,SAAA;;;;AA9F5B;;cCGa,CAAA;;;;;;;KAQD,KAAA;EAAA,CACT,CAAA,WACO,IAAA,GAAO,MAAA;AAAA;;ADFjB;;;;KCWY,eAAA,yBAAwC,KAAA,GAAQ,KAAA;;;;;;KAOhD,iBAAA,yBAA0C,KAAA,GAAQ,KAAA;AAAA,iBAE9C,aAAA,wBAAA,CAAsC,KAAA,EAAO,CAAA,GAAI,iBAAA,CAAkB,CAAA;AAAA,iBAInE,QAAA,wBAAA,CAAiC,KAAA,EAAO,CAAA,GAAI,eAAA,CAAgB,CAAA;;;ADnB5E;;;KC4BY,eAAA,yBAAwC,KAAA,GAAQ,KAAA;AAAA,iBAE5C,WAAA,wBAAA,CAAoC,KAAA,EAAO,CAAA,GAAI,eAAA,CAAgB,CAAA;;;;;;UAS9D,WAAA;EAAA,SACN,WAAA,EAAa,eAAA,CAAgB,KAAA;AAAA;AAAA,UAGvB,SAAA;EAAA,SACN,IAAA;EAAA,SACA,QAAA;EAAA,SACA,KAAA,GAAQ,SAAA;EAAA,SACR,UAAA,GAAa,MAAA,SAAe,SAAA;AAAA;AAAA,KAG3B,kBAAA;EAAA,SACD,EAAA;EAAA,SACA,MAAA,GAAS,MAAA;AAAA;AAAA,KAGR,aAAA;AAAA,KAEA,SAAA,GACR,aAAA;EAAA,UACY,GAAA,WAAc,SAAA;AAAA,aACjB,SAAA;AAAA,KAED,yBAAA,GAA4B,SAAA;AAAA,KAE5B,8BAAA,GAAiC,yBAAA,GAA4B,IAAA;ADlDzE;;;;;;;;AAAA,iBC4DgB,gCAAA,CACd,KAAA,YACC,KAAA,IAAS,8BAAA;AAAA,KAYA,aAAA;EAAA,SAEG,IAAA;EAAA,SACA,KAAA,EAAO,8BAAA;AAAA;EAAA,SAEP,IAAA;EAAA,SAA2B,UAAA;AAAA;AAAA,iBAE1B,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAA;AAAA,KAY9C,6BAAA;EAAA,SACD,IAAA;EAAA,SACA,EAAA,EAAI,kBAAA;EAAA,SACJ,MAAA,GAAS,MAAA;AAAA;AAAA,iBAGJ,+BAAA,CACd,KAAA,YACC,KAAA,IAAS,6BAAA;AAAA,KAoBA,wBAAA;EAAA,SACD,GAAA;IAAA,SAAgB,KAAA;IAAA,SAAwB,MAAA;EAAA;EAAA,SACxC,QAAA,GAAW,6BAAA;EAAA,SACX,QAAA,GAAW,6BAAA;AAAA;;;;;KAOV,8BAAA,GAAiC,IAAA,CAAK,wBAAA;AAAA,KAEtC,gBAAA;EAAA,SACD,aAAA,EAAe,iBAAA,CAAkB,KAAA;EAAA,SACjC,SAAA;IAAA,SACE,QAAA,EAAU,aAAA,CAAc,wBAAA;EAAA;AAAA;AAAA,UAIpB,MAAA;EAAA,SACN,QAAA;EAAA,SACA,UAAA,EAAY,MAAA,SAAe,SAAA;EAAA,SAC3B,MAAA,GAAS,MAAA;EAAA,SACT,YAAA,GAAe,MAAA;AAAA;AAAA,UAIT,QAAA;EAAA,SACN,IAAA;EAAA,SACA,IAAA,EAAM,MAAA;EAAA,SACN,MAAA;EAAA,SACA,KAAA,GAAQ,IAAA;AAAA;AAAA,KAGP,IAAA;EAAA,SACG,IAAA;EAAA,SAAqB,IAAA,EAAM,aAAA;EAAA,SAAgC,KAAA;AAAA;EAAA,SAC3D,IAAA;EAAA,SAAyB,IAAA,EAAM,aAAA;AAAA;AAAA,UAE7B,aAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAA;IAAA,SACE,QAAA;EAAA;EAAA,SAEF,MAAA,EAAQ,MAAA,SAAe,SAAA;EAAA,SACvB,OAAA,GAAU,aAAA,CAAc,QAAA;EAAA,SACxB,QAAA;AAAA;AAAA,UAGM,QAAA;EAAA,SACN,MAAA;EAAA,SACA,YAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA;EAAA,SACA,IAAA;EAAA,SACA,WAAA;IAAA,UACG,GAAA;EAAA;AAAA;;;;;UAQG,oBAAA;EAAA,SACN,WAAA;EAAA,SACA,WAAA;EAAA,SACA,YAAA;EAAA,SACA,gBAAA;EAAA,SACA,SAAA,EAAW,IAAA;EAAA,SACX,MAAA;EAAA,SACA,IAAA,EAAM,MAAA;EAAA,SACN,UAAA;AAAA;;;;;;;;;;;;KC7MC,wBAAA;EAAA,SACD,aAAA,EAAe,iBAAA,CAAkB,KAAA;EAAA,SACjC,SAAA;IAAA,SACE,QAAA,EAAU,aAAA,CAAc,wBAAA;EAAA;AAAA;;;;;;;;;;;;;;;AFJrC;UEuBiB,QAAA,kBACE,WAAA,GAAc,WAAA,kBACf,MAAA,SAAe,iBAAA,IAAqB,MAAA,SAAe,iBAAA;EAAA,SAE1D,MAAA;EAAA,SACA,YAAA;EAAA,SACA,KAAA,EAAO,MAAA;EAAA,SACP,MAAA,EAAQ,OAAA;EAAA,SACR,YAAA,GAAe,MAAA,SAAe,mBAAA;EF/B8C;;;;;EAAA,SEqC5E,MAAA,GAAS,MAAA,SAAe,MAAA,SAAe,MAAA;EAAA,SACvC,OAAA,EAAS,QAAA;EAAA,SACT,YAAA,EAAc,MAAA,SAAe,MAAA;EAAA,SAC7B,cAAA,EAAgB,MAAA;EAAA,SAChB,SAAA,GAAY,wBAAA;EAAA,SACZ,WAAA,EAAa,eAAA;EAAA,SACb,IAAA,EAAM,MAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hashing-rZiqFOlc.mjs","names":[],"sources":["../src/canonicalization.ts","../src/hashing.ts"],"sourcesContent":["import { isArrayEqual } from '@prisma-next/utils/array-equal';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport type { JsonObject } from '@prisma-next/utils/json';\n\nimport type { Contract } from './contract-types';\n\n/**\n * Per-target contract serializer hook. The framework canonicalizer uses\n * this to convert an in-memory contract (which may carry class-instance\n * IR nodes whose runtime-only fields must not appear in the on-disk\n * envelope) into a plain JsonObject before applying the family-agnostic\n * canonical-key ordering / default-omission / sort steps. Targets whose\n * contract is JSON-clean by construction return the contract unchanged.\n */\nexport type SerializeContract = (contract: Contract) => JsonObject;\n\nconst TOP_LEVEL_ORDER = [\n 'schemaVersion',\n 'canonicalVersion',\n 'targetFamily',\n 'target',\n 'profileHash',\n 'roots',\n 'models',\n 'valueObjects',\n 'domain',\n 'storage',\n 'execution',\n 'capabilities',\n 'extensionPacks',\n 'meta',\n] as const;\n\nfunction isDefaultValue(value: unknown): boolean {\n if (value === false) return true;\n if (value === null) return false;\n if (Array.isArray(value) && value.length === 0) return true;\n if (typeof value === 'object' && value !== null) {\n const keys = Object.keys(value);\n return keys.length === 0;\n }\n return false;\n}\n\nfunction omitDefaults(obj: unknown, path: readonly string[]): unknown {\n if (obj === null || typeof obj !== 'object') {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => omitDefaults(item, path));\n }\n\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(obj)) {\n const currentPath = [...path, key];\n\n if (key === '_generated') {\n continue;\n }\n\n if (key === 'generated' && value === false) {\n continue;\n }\n\n if ((key === 'onDelete' || key === 'onUpdate') && value === 'noAction') {\n continue;\n }\n\n if (isDefaultValue(value)) {\n const isRequiredModels = isArrayEqual(currentPath, ['models']);\n const isRequiredNamespaces = isArrayEqual(currentPath, ['storage', 'namespaces']);\n const isNamespaceSlot =\n currentPath.length === 3 &&\n isArrayEqual([currentPath[0], currentPath[1]], ['storage', 'namespaces']);\n const isRequiredNamespaceTables =\n currentPath.length === 4 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'namespaces' &&\n currentPath[3] === 'tables';\n // Preserve per-table payloads even when empty. SQL tables are never\n // emitted empty; Mongo collections legitimately are (a declared\n // collection with no schema is a valid representation), and the\n // family-agnostic canonicalizer must not strip them.\n const isNamespaceTableEntry =\n currentPath.length === 5 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'namespaces' &&\n currentPath[3] === 'tables';\n const isRequiredRoots = isArrayEqual(currentPath, ['roots']);\n const isRequiredExtensionPacks = isArrayEqual(currentPath, ['extensionPacks']);\n const isRequiredCapabilities = isArrayEqual(currentPath, ['capabilities']);\n const isRequiredMeta = isArrayEqual(currentPath, ['meta']);\n const isRequiredExecutionDefaults = isArrayEqual(currentPath, [\n 'execution',\n 'mutations',\n 'defaults',\n ]);\n const isExtensionNamespace = currentPath.length === 2 && currentPath[0] === 'extensionPacks';\n const isModelRelations =\n currentPath.length === 3 &&\n isArrayEqual([currentPath[0], currentPath[2]], ['models', 'relations']);\n const isModelStorage =\n currentPath.length === 3 &&\n isArrayEqual([currentPath[0], currentPath[2]], ['models', 'storage']);\n const isNamespaceTableUniques =\n currentPath.length === 6 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'namespaces' &&\n currentPath[3] === 'tables' &&\n currentPath[5] === 'uniques';\n const isNamespaceTableIndexes =\n currentPath.length === 6 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'namespaces' &&\n currentPath[3] === 'tables' &&\n currentPath[5] === 'indexes';\n const isNamespaceTableForeignKeys =\n currentPath.length === 6 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'namespaces' &&\n currentPath[3] === 'tables' &&\n currentPath[5] === 'foreignKeys';\n\n // `storage.types.<name>.typeParams` is part of the StorageTypeInstance\n // shape (validators require it). Preserve it even when empty so the\n // emitted contract.json remains structurally valid after a round-trip.\n const isStorageTypeTypeParams =\n currentPath.length === 4 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'types' &&\n key === 'typeParams';\n\n const isFkBooleanField =\n currentPath.length === 7 &&\n currentPath[0] === 'storage' &&\n currentPath[1] === 'namespaces' &&\n currentPath[3] === 'tables' &&\n currentPath[5] === 'foreignKeys' &&\n (key === 'constraint' || key === 'index');\n\n const isNullableField = key === 'nullable';\n\n if (\n !isRequiredModels &&\n !isRequiredNamespaces &&\n !isNamespaceSlot &&\n !isRequiredNamespaceTables &&\n !isNamespaceTableEntry &&\n !isRequiredRoots &&\n !isRequiredExtensionPacks &&\n !isRequiredCapabilities &&\n !isRequiredMeta &&\n !isRequiredExecutionDefaults &&\n !isExtensionNamespace &&\n !isModelRelations &&\n !isModelStorage &&\n !isNamespaceTableUniques &&\n !isNamespaceTableIndexes &&\n !isNamespaceTableForeignKeys &&\n !isFkBooleanField &&\n !isNullableField &&\n !isStorageTypeTypeParams\n ) {\n continue;\n }\n }\n\n result[key] = omitDefaults(value, currentPath);\n }\n\n return result;\n}\n\nfunction sortObjectKeys(obj: unknown): unknown {\n if (obj === null || typeof obj !== 'object') {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map((item) => sortObjectKeys(item));\n }\n\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(obj).sort();\n for (const key of keys) {\n sorted[key] = sortObjectKeys((obj as Record<string, unknown>)[key]);\n }\n\n return sorted;\n}\n\ntype NamespaceObject = {\n tables?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\ntype StorageObject = {\n namespaces?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\ntype TableObject = {\n indexes?: unknown[];\n uniques?: unknown[];\n [key: string]: unknown;\n};\n\nfunction sortTableArrays(tableObj: TableObject): TableObject {\n const sortedTable: TableObject = { ...tableObj };\n\n if (Array.isArray(tableObj.indexes)) {\n sortedTable.indexes = [...tableObj.indexes].sort((a, b) => {\n const nameA = (a as { name?: string })?.name || '';\n const nameB = (b as { name?: string })?.name || '';\n return nameA.localeCompare(nameB);\n });\n }\n\n if (Array.isArray(tableObj.uniques)) {\n sortedTable.uniques = [...tableObj.uniques].sort((a, b) => {\n const nameA = (a as { name?: string })?.name || '';\n const nameB = (b as { name?: string })?.name || '';\n return nameA.localeCompare(nameB);\n });\n }\n\n return sortedTable;\n}\n\nfunction sortIndexesAndUniques(storage: unknown): unknown {\n if (!storage || typeof storage !== 'object') {\n return storage;\n }\n\n const storageObj = storage as StorageObject;\n if (!storageObj.namespaces || typeof storageObj.namespaces !== 'object') {\n return storage;\n }\n\n const namespaces = storageObj.namespaces;\n const result: StorageObject = { ...storageObj, namespaces: {} };\n const resultNamespaces = result.namespaces as Record<string, unknown>;\n\n for (const nsId of Object.keys(namespaces)) {\n const ns = namespaces[nsId];\n if (!ns || typeof ns !== 'object') {\n resultNamespaces[nsId] = ns;\n continue;\n }\n\n const nsObj = ns as NamespaceObject;\n if (!nsObj.tables || typeof nsObj.tables !== 'object') {\n resultNamespaces[nsId] = ns;\n continue;\n }\n\n const sortedTables: Record<string, unknown> = {};\n const sortedTableNames = Object.keys(nsObj.tables).sort();\n for (const tableName of sortedTableNames) {\n const table = nsObj.tables[tableName];\n if (!table || typeof table !== 'object') {\n sortedTables[tableName] = table;\n continue;\n }\n sortedTables[tableName] = sortTableArrays(table as TableObject);\n }\n\n resultNamespaces[nsId] = { ...nsObj, tables: sortedTables };\n }\n\n return result;\n}\n\nexport function orderTopLevel(obj: Record<string, unknown>): Record<string, unknown> {\n const ordered: Record<string, unknown> = {};\n const remaining = new Set(Object.keys(obj));\n\n for (const key of TOP_LEVEL_ORDER) {\n if (remaining.has(key)) {\n ordered[key] = obj[key];\n remaining.delete(key);\n }\n }\n\n for (const key of Array.from(remaining).sort()) {\n ordered[key] = obj[key];\n }\n\n return ordered;\n}\n\nexport interface CanonicalizeContractOptions {\n readonly schemaVersion?: string;\n /**\n * Per-target hook that converts the in-memory contract (which may\n * carry class-instance IR nodes) into a plain JsonObject before the\n * family-agnostic canonicalization steps run.\n *\n * Routing through the hook is what lets each target decide which\n * fields appear in the on-disk envelope; runtime-only class API\n * fields stay invisible to the canonicalization walk by virtue of\n * the per-target serializer not putting them in the JSON shape.\n */\n readonly serializeContract: SerializeContract;\n}\n\n/**\n * Object-form variant of {@link canonicalizeContract}. Exported because the\n * emitter writes the canonical contract through a separate JSON-stringify\n * pass and consumes the structured object directly.\n */\nexport function canonicalizeContractToObject(\n contract: Contract,\n options: CanonicalizeContractOptions,\n): Record<string, unknown> {\n const serialized = options.serializeContract(contract);\n const normalized: Record<string, unknown> = {\n ...ifDefined('schemaVersion', options.schemaVersion),\n targetFamily: serialized['targetFamily'],\n target: serialized['target'],\n profileHash: serialized['profileHash'],\n roots: serialized['roots'],\n models: serialized['models'],\n ...ifDefined('valueObjects', serialized['valueObjects']),\n storage: serialized['storage'],\n ...ifDefined('execution', serialized['execution']),\n extensionPacks: serialized['extensionPacks'],\n capabilities: serialized['capabilities'],\n meta: serialized['meta'],\n };\n const withDefaultsOmitted = omitDefaults(normalized, []) as Record<string, unknown>;\n const withSortedIndexes = sortIndexesAndUniques(withDefaultsOmitted['storage']);\n const withSortedStorage = { ...withDefaultsOmitted, storage: withSortedIndexes };\n const withSortedKeys = sortObjectKeys(withSortedStorage) as Record<string, unknown>;\n return orderTopLevel(withSortedKeys);\n}\n\nexport function canonicalizeContract(\n contract: Contract,\n options: CanonicalizeContractOptions,\n): string {\n return JSON.stringify(canonicalizeContractToObject(contract, options), null, 2);\n}\n","import { createHash } from 'node:crypto';\nimport type { JsonObject } from '@prisma-next/utils/json';\nimport { canonicalizeContract } from './canonicalization';\nimport type { Contract } from './contract-types';\nimport type { ExecutionHashBase, ProfileHashBase, StorageHashBase } from './types';\n\nconst SCHEMA_VERSION = '1';\n\nfunction sha256(content: string): string {\n const hash = createHash('sha256');\n hash.update(content);\n return `sha256:${hash.digest('hex')}`;\n}\n\nfunction hashContract(section: Record<string, unknown>): string {\n // Blind cast: the synthesised object is a hash-only stand-in\n // — never returned to callers, never executed as a Contract.\n // `canonicalizeContract` only walks the storage / execution /\n // capabilities slices, all of which are populated above, so the\n // missing precise Contract typing on the other slots is\n // immaterial for the hash result.\n const contract = {\n targetFamily: section['targetFamily'],\n target: section['target'],\n roots: {},\n models: {},\n storage: section['storage'] ?? {},\n execution: section['execution'],\n extensionPacks: {},\n capabilities: section['capabilities'] ?? {},\n meta: {},\n profileHash: '',\n ...section,\n } as unknown as Contract;\n return canonicalizeContract(contract, {\n schemaVersion: SCHEMA_VERSION,\n serializeContract: (c) => JSON.parse(JSON.stringify(c)) as JsonObject,\n });\n}\n\nexport function computeStorageHash(args: {\n target: string;\n targetFamily: string;\n storage: Record<string, unknown>;\n}): StorageHashBase<string> {\n return sha256(hashContract(args)) as StorageHashBase<string>;\n}\n\nexport function computeExecutionHash(args: {\n target: string;\n targetFamily: string;\n execution: Record<string, unknown>;\n}): ExecutionHashBase<string> {\n return sha256(hashContract(args)) as ExecutionHashBase<string>;\n}\n\nexport function computeProfileHash(args: {\n target: string;\n targetFamily: string;\n capabilities: Record<string, Record<string, boolean>>;\n}): ProfileHashBase<string> {\n return sha256(hashContract(args)) as ProfileHashBase<string>;\n}\n"],"mappings":"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"}
|