@prisma-next/core-control-plane 0.3.0-dev.13 → 0.3.0-dev.131
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 +31 -99
- package/dist/constants.d.mts +9 -0
- package/dist/constants.d.mts.map +1 -0
- package/dist/constants.mjs +10 -0
- package/dist/constants.mjs.map +1 -0
- package/dist/emission.d.mts +64 -0
- package/dist/emission.d.mts.map +1 -0
- package/dist/emission.mjs +374 -0
- package/dist/emission.mjs.map +1 -0
- package/dist/errors.d.mts +232 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +330 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/{schema-view.d.ts → schema-view-DObwT8x9.d.mts} +17 -14
- package/dist/schema-view-DObwT8x9.d.mts.map +1 -0
- package/dist/schema-view.d.mts +2 -0
- package/dist/schema-view.mjs +1 -0
- package/dist/stack.d.mts +30 -0
- package/dist/stack.d.mts.map +1 -0
- package/dist/stack.mjs +30 -0
- package/dist/stack.mjs.map +1 -0
- package/dist/types-B8rNCYSV.d.mts +615 -0
- package/dist/types-B8rNCYSV.d.mts.map +1 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/package.json +28 -40
- package/src/constants.ts +5 -0
- package/src/emission/canonicalization.ts +82 -23
- package/src/emission/emit.ts +159 -25
- package/src/emission/hashing.ts +29 -27
- package/src/emission/types.ts +18 -2
- package/src/errors.ts +127 -20
- package/src/exports/constants.ts +1 -0
- package/src/exports/emission.ts +1 -1
- package/src/exports/errors.ts +6 -1
- package/src/exports/types.ts +0 -1
- package/src/migrations.ts +27 -1
- package/src/schema-view.ts +5 -5
- package/src/types.ts +25 -12
- package/dist/chunk-473ODD3P.js +0 -14
- package/dist/chunk-473ODD3P.js.map +0 -1
- package/dist/chunk-U5RYT6PT.js +0 -229
- package/dist/chunk-U5RYT6PT.js.map +0 -1
- package/dist/config-types.d.ts +0 -68
- package/dist/config-types.d.ts.map +0 -1
- package/dist/config-validation.d.ts +0 -10
- package/dist/config-validation.d.ts.map +0 -1
- package/dist/emission/canonicalization.d.ts +0 -6
- package/dist/emission/canonicalization.d.ts.map +0 -1
- package/dist/emission/emit.d.ts +0 -5
- package/dist/emission/emit.d.ts.map +0 -1
- package/dist/emission/hashing.d.ts +0 -17
- package/dist/emission/hashing.d.ts.map +0 -1
- package/dist/emission/types.d.ts +0 -16
- package/dist/emission/types.d.ts.map +0 -1
- package/dist/errors.d.ts +0 -183
- package/dist/errors.d.ts.map +0 -1
- package/dist/exports/config-types.d.ts +0 -3
- package/dist/exports/config-types.d.ts.map +0 -1
- package/dist/exports/config-types.js +0 -53
- package/dist/exports/config-types.js.map +0 -1
- package/dist/exports/config-validation.d.ts +0 -2
- package/dist/exports/config-validation.d.ts.map +0 -1
- package/dist/exports/config-validation.js +0 -252
- package/dist/exports/config-validation.js.map +0 -1
- package/dist/exports/emission.d.ts +0 -5
- package/dist/exports/emission.d.ts.map +0 -1
- package/dist/exports/emission.js +0 -310
- package/dist/exports/emission.js.map +0 -1
- package/dist/exports/errors.d.ts +0 -3
- package/dist/exports/errors.d.ts.map +0 -1
- package/dist/exports/errors.js +0 -43
- package/dist/exports/errors.js.map +0 -1
- package/dist/exports/schema-view.d.ts +0 -2
- package/dist/exports/schema-view.d.ts.map +0 -1
- package/dist/exports/schema-view.js +0 -1
- package/dist/exports/schema-view.js.map +0 -1
- package/dist/exports/stack.d.ts +0 -2
- package/dist/exports/stack.d.ts.map +0 -1
- package/dist/exports/stack.js +0 -7
- package/dist/exports/stack.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 -7
- package/dist/exports/types.js.map +0 -1
- package/dist/migrations.d.ts +0 -190
- package/dist/migrations.d.ts.map +0 -1
- package/dist/schema-view.d.ts.map +0 -1
- package/dist/stack.d.ts +0 -25
- package/dist/stack.d.ts.map +0 -1
- package/dist/types.d.ts +0 -411
- package/dist/types.d.ts.map +0 -1
- package/src/config-types.ts +0 -157
- package/src/config-validation.ts +0 -270
- package/src/exports/config-types.ts +0 -5
- package/src/exports/config-validation.ts +0 -1
package/package.json
CHANGED
|
@@ -1,65 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/core-control-plane",
|
|
3
|
-
"version": "0.3.0-dev.
|
|
3
|
+
"version": "0.3.0-dev.131",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
|
-
"description": "Control
|
|
6
|
+
"description": "Control-plane migration/emission primitives and structured error utilities",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"arktype": "^2.1.26",
|
|
9
9
|
"prettier": "^3.3.3",
|
|
10
|
-
"@prisma-next/
|
|
11
|
-
"@prisma-next/
|
|
12
|
-
"@prisma-next/utils": "0.3.0-dev.
|
|
10
|
+
"@prisma-next/operations": "0.3.0-dev.131",
|
|
11
|
+
"@prisma-next/contract": "0.3.0-dev.131",
|
|
12
|
+
"@prisma-next/utils": "0.3.0-dev.131"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"
|
|
16
|
-
"tsup": "8.5.1",
|
|
15
|
+
"tsdown": "0.18.4",
|
|
17
16
|
"typescript": "5.9.3",
|
|
18
|
-
"vitest": "4.0.
|
|
19
|
-
"@prisma-next/test-utils": "0.0.1"
|
|
17
|
+
"vitest": "4.0.17",
|
|
18
|
+
"@prisma-next/test-utils": "0.0.1",
|
|
19
|
+
"@prisma-next/tsconfig": "0.0.0",
|
|
20
|
+
"@prisma-next/tsdown": "0.0.0"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
23
|
"dist",
|
|
23
24
|
"src"
|
|
24
25
|
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
25
29
|
"exports": {
|
|
26
|
-
"./
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"./
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"./types": {
|
|
39
|
-
"types": "./dist/exports/types.d.ts",
|
|
40
|
-
"import": "./dist/exports/types.js"
|
|
41
|
-
},
|
|
42
|
-
"./stack": {
|
|
43
|
-
"types": "./dist/exports/stack.d.ts",
|
|
44
|
-
"import": "./dist/exports/stack.js"
|
|
45
|
-
},
|
|
46
|
-
"./emission": {
|
|
47
|
-
"types": "./dist/exports/emission.d.ts",
|
|
48
|
-
"import": "./dist/exports/emission.js"
|
|
49
|
-
},
|
|
50
|
-
"./schema-view": {
|
|
51
|
-
"types": "./dist/exports/schema-view.d.ts",
|
|
52
|
-
"import": "./dist/exports/schema-view.js"
|
|
53
|
-
}
|
|
30
|
+
"./constants": "./dist/constants.mjs",
|
|
31
|
+
"./emission": "./dist/emission.mjs",
|
|
32
|
+
"./errors": "./dist/errors.mjs",
|
|
33
|
+
"./schema-view": "./dist/schema-view.mjs",
|
|
34
|
+
"./stack": "./dist/stack.mjs",
|
|
35
|
+
"./types": "./dist/types.mjs",
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/prisma/prisma-next.git",
|
|
41
|
+
"directory": "packages/1-framework/1-core/migration/control-plane"
|
|
54
42
|
},
|
|
55
43
|
"scripts": {
|
|
56
|
-
"build": "
|
|
44
|
+
"build": "tsdown",
|
|
57
45
|
"test": "vitest run --passWithNoTests",
|
|
58
46
|
"test:coverage": "vitest run --coverage --passWithNoTests",
|
|
59
|
-
"typecheck": "tsc --
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
60
48
|
"lint": "biome check . --error-on-warnings",
|
|
61
49
|
"lint:fix": "biome check --write .",
|
|
62
50
|
"lint:fix:unsafe": "biome check --write --unsafe .",
|
|
63
|
-
"clean": "
|
|
51
|
+
"clean": "rm -rf dist dist-tsc dist-tsc-prod coverage .tmp-output"
|
|
64
52
|
}
|
|
65
53
|
}
|
package/src/constants.ts
ADDED
|
@@ -1,19 +1,39 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { bigintJsonReplacer } from '@prisma-next/contract/types';
|
|
2
2
|
import { isArrayEqual } from '@prisma-next/utils/array-equal';
|
|
3
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
3
4
|
|
|
4
5
|
type NormalizedContract = {
|
|
5
6
|
schemaVersion: string;
|
|
6
7
|
targetFamily: string;
|
|
7
8
|
target: string;
|
|
8
|
-
|
|
9
|
+
storageHash?: string;
|
|
10
|
+
executionHash?: string;
|
|
9
11
|
profileHash?: string;
|
|
12
|
+
roots?: Record<string, string>;
|
|
10
13
|
models: Record<string, unknown>;
|
|
11
|
-
relations
|
|
14
|
+
relations?: Record<string, unknown>;
|
|
12
15
|
storage: Record<string, unknown>;
|
|
16
|
+
execution?: Record<string, unknown>;
|
|
13
17
|
extensionPacks: Record<string, unknown>;
|
|
14
18
|
capabilities: Record<string, Record<string, boolean>>;
|
|
15
19
|
meta: Record<string, unknown>;
|
|
16
|
-
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type CanonicalContractInput = {
|
|
23
|
+
schemaVersion: string;
|
|
24
|
+
targetFamily: string;
|
|
25
|
+
target: string;
|
|
26
|
+
roots?: Record<string, string>;
|
|
27
|
+
models: Record<string, unknown>;
|
|
28
|
+
relations?: Record<string, unknown>;
|
|
29
|
+
storage: Record<string, unknown>;
|
|
30
|
+
execution?: Record<string, unknown>;
|
|
31
|
+
extensionPacks: Record<string, unknown>;
|
|
32
|
+
capabilities: Record<string, Record<string, boolean>>;
|
|
33
|
+
meta: Record<string, unknown>;
|
|
34
|
+
storageHash?: string;
|
|
35
|
+
executionHash?: string;
|
|
36
|
+
profileHash?: string;
|
|
17
37
|
};
|
|
18
38
|
|
|
19
39
|
const TOP_LEVEL_ORDER = [
|
|
@@ -21,19 +41,23 @@ const TOP_LEVEL_ORDER = [
|
|
|
21
41
|
'canonicalVersion',
|
|
22
42
|
'targetFamily',
|
|
23
43
|
'target',
|
|
24
|
-
'
|
|
44
|
+
'storageHash',
|
|
45
|
+
'executionHash',
|
|
25
46
|
'profileHash',
|
|
47
|
+
'roots',
|
|
26
48
|
'models',
|
|
49
|
+
'relations',
|
|
27
50
|
'storage',
|
|
51
|
+
'execution',
|
|
28
52
|
'capabilities',
|
|
29
53
|
'extensionPacks',
|
|
30
54
|
'meta',
|
|
31
|
-
'sources',
|
|
32
55
|
] as const;
|
|
33
56
|
|
|
34
57
|
function isDefaultValue(value: unknown): boolean {
|
|
35
58
|
if (value === false) return true;
|
|
36
59
|
if (value === null) return false;
|
|
60
|
+
if (value instanceof Date) return false;
|
|
37
61
|
if (Array.isArray(value) && value.length === 0) return true;
|
|
38
62
|
if (typeof value === 'object' && value !== null) {
|
|
39
63
|
const keys = Object.keys(value);
|
|
@@ -47,6 +71,10 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
47
71
|
return obj;
|
|
48
72
|
}
|
|
49
73
|
|
|
74
|
+
if (obj instanceof Date) {
|
|
75
|
+
return obj;
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
if (Array.isArray(obj)) {
|
|
51
79
|
return obj.map((item) => omitDefaults(item, path));
|
|
52
80
|
}
|
|
@@ -69,14 +97,30 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
69
97
|
continue;
|
|
70
98
|
}
|
|
71
99
|
|
|
100
|
+
// Strip 'noAction' referential actions (the database default) for hash stability.
|
|
101
|
+
// A contract with explicit `onDelete: 'noAction'` is semantically identical to
|
|
102
|
+
// one that omits `onDelete` entirely, so they should produce the same hash.
|
|
103
|
+
if ((key === 'onDelete' || key === 'onUpdate') && value === 'noAction') {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
if (isDefaultValue(value)) {
|
|
73
108
|
const isRequiredModels = isArrayEqual(currentPath, ['models']);
|
|
74
109
|
const isRequiredTables = isArrayEqual(currentPath, ['storage', 'tables']);
|
|
110
|
+
const isRequiredCollections = isArrayEqual(currentPath, ['storage', 'collections']);
|
|
111
|
+
const isCollectionEntry =
|
|
112
|
+
currentPath.length === 3 &&
|
|
113
|
+
isArrayEqual([currentPath[0], currentPath[1]], ['storage', 'collections']);
|
|
75
114
|
const isRequiredRelations = isArrayEqual(currentPath, ['relations']);
|
|
115
|
+
const isRequiredRoots = isArrayEqual(currentPath, ['roots']);
|
|
76
116
|
const isRequiredExtensionPacks = isArrayEqual(currentPath, ['extensionPacks']);
|
|
77
117
|
const isRequiredCapabilities = isArrayEqual(currentPath, ['capabilities']);
|
|
78
118
|
const isRequiredMeta = isArrayEqual(currentPath, ['meta']);
|
|
79
|
-
const
|
|
119
|
+
const isRequiredExecutionDefaults = isArrayEqual(currentPath, [
|
|
120
|
+
'execution',
|
|
121
|
+
'mutations',
|
|
122
|
+
'defaults',
|
|
123
|
+
]);
|
|
80
124
|
const isExtensionNamespace = currentPath.length === 2 && currentPath[0] === 'extensionPacks';
|
|
81
125
|
const isModelRelations =
|
|
82
126
|
currentPath.length === 3 &&
|
|
@@ -100,19 +144,33 @@ function omitDefaults(obj: unknown, path: readonly string[]): unknown {
|
|
|
100
144
|
['storage', 'tables', 'foreignKeys'],
|
|
101
145
|
);
|
|
102
146
|
|
|
147
|
+
// Preserve per-FK `constraint` and `index` booleans (even when `false`)
|
|
148
|
+
// so that hash distinguishes `false` from absent.
|
|
149
|
+
// Path: ['storage', 'tables', <tableName>, 'foreignKeys', 'constraint' | 'index']
|
|
150
|
+
const isFkBooleanField =
|
|
151
|
+
currentPath.length === 5 &&
|
|
152
|
+
currentPath[0] === 'storage' &&
|
|
153
|
+
currentPath[1] === 'tables' &&
|
|
154
|
+
currentPath[3] === 'foreignKeys' &&
|
|
155
|
+
(key === 'constraint' || key === 'index');
|
|
156
|
+
|
|
103
157
|
if (
|
|
104
158
|
!isRequiredModels &&
|
|
105
159
|
!isRequiredTables &&
|
|
160
|
+
!isRequiredCollections &&
|
|
161
|
+
!isCollectionEntry &&
|
|
106
162
|
!isRequiredRelations &&
|
|
163
|
+
!isRequiredRoots &&
|
|
107
164
|
!isRequiredExtensionPacks &&
|
|
108
165
|
!isRequiredCapabilities &&
|
|
109
166
|
!isRequiredMeta &&
|
|
110
|
-
!
|
|
167
|
+
!isRequiredExecutionDefaults &&
|
|
111
168
|
!isExtensionNamespace &&
|
|
112
169
|
!isModelRelations &&
|
|
113
170
|
!isTableUniques &&
|
|
114
171
|
!isTableIndexes &&
|
|
115
|
-
!isTableForeignKeys
|
|
172
|
+
!isTableForeignKeys &&
|
|
173
|
+
!isFkBooleanField
|
|
116
174
|
) {
|
|
117
175
|
continue;
|
|
118
176
|
}
|
|
@@ -129,6 +187,10 @@ function sortObjectKeys(obj: unknown): unknown {
|
|
|
129
187
|
return obj;
|
|
130
188
|
}
|
|
131
189
|
|
|
190
|
+
if (obj instanceof Date) {
|
|
191
|
+
return obj;
|
|
192
|
+
}
|
|
193
|
+
|
|
132
194
|
if (Array.isArray(obj)) {
|
|
133
195
|
return obj.map((item) => sortObjectKeys(item));
|
|
134
196
|
}
|
|
@@ -219,29 +281,26 @@ function orderTopLevel(obj: Record<string, unknown>): Record<string, unknown> {
|
|
|
219
281
|
return ordered;
|
|
220
282
|
}
|
|
221
283
|
|
|
222
|
-
export function canonicalizeContract(
|
|
223
|
-
ir: ContractIR & { coreHash?: string; profileHash?: string },
|
|
224
|
-
): string {
|
|
284
|
+
export function canonicalizeContract(ir: CanonicalContractInput): string {
|
|
225
285
|
const normalized: NormalizedContract = {
|
|
226
286
|
schemaVersion: ir.schemaVersion,
|
|
227
287
|
targetFamily: ir.targetFamily,
|
|
228
288
|
target: ir.target,
|
|
289
|
+
...ifDefined('roots', ir.roots),
|
|
229
290
|
models: ir.models,
|
|
230
|
-
relations
|
|
291
|
+
...ifDefined('relations', ir.relations),
|
|
231
292
|
storage: ir.storage,
|
|
293
|
+
...ifDefined('execution', ir.execution),
|
|
232
294
|
extensionPacks: ir.extensionPacks,
|
|
233
295
|
capabilities: ir.capabilities,
|
|
234
296
|
meta: ir.meta,
|
|
235
|
-
sources: ir.sources,
|
|
236
297
|
};
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
normalized.profileHash = ir.profileHash;
|
|
244
|
-
}
|
|
298
|
+
Object.assign(
|
|
299
|
+
normalized,
|
|
300
|
+
ifDefined('storageHash', ir.storageHash),
|
|
301
|
+
ifDefined('executionHash', ir.executionHash),
|
|
302
|
+
ifDefined('profileHash', ir.profileHash),
|
|
303
|
+
);
|
|
245
304
|
|
|
246
305
|
const withDefaultsOmitted = omitDefaults(normalized, []) as NormalizedContract;
|
|
247
306
|
const withSortedIndexes = sortIndexesAndUniques(withDefaultsOmitted.storage);
|
|
@@ -249,5 +308,5 @@ export function canonicalizeContract(
|
|
|
249
308
|
const withSortedKeys = sortObjectKeys(withSortedStorage) as Record<string, unknown>;
|
|
250
309
|
const withOrderedTopLevel = orderTopLevel(withSortedKeys);
|
|
251
310
|
|
|
252
|
-
return JSON.stringify(withOrderedTopLevel,
|
|
311
|
+
return JSON.stringify(withOrderedTopLevel, bigintJsonReplacer, 2);
|
|
253
312
|
}
|
package/src/emission/emit.ts
CHANGED
|
@@ -1,10 +1,117 @@
|
|
|
1
1
|
import type { ContractIR } from '@prisma-next/contract/ir';
|
|
2
2
|
import type { TargetFamilyHook, ValidationContext } from '@prisma-next/contract/types';
|
|
3
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
4
|
+
import { type } from 'arktype';
|
|
3
5
|
import { format } from 'prettier';
|
|
4
6
|
import { canonicalizeContract } from './canonicalization';
|
|
5
|
-
import {
|
|
7
|
+
import { computeExecutionHash, computeProfileHash, computeStorageHash } from './hashing';
|
|
6
8
|
import type { EmitOptions, EmitResult } from './types';
|
|
7
9
|
|
|
10
|
+
function stripStrategyFromRelations(
|
|
11
|
+
relations: Record<string, Record<string, unknown>> | undefined,
|
|
12
|
+
): Record<string, Record<string, unknown>> | undefined {
|
|
13
|
+
if (!relations) return undefined;
|
|
14
|
+
const result: Record<string, Record<string, unknown>> = {};
|
|
15
|
+
for (const [relName, rel] of Object.entries(relations)) {
|
|
16
|
+
const { strategy: _, ...rest } = rel;
|
|
17
|
+
result[relName] = rest;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toDomainModel(models: Record<string, unknown>): Record<string, unknown> {
|
|
23
|
+
const result: Record<string, unknown> = {};
|
|
24
|
+
for (const [modelName, modelUnknown] of Object.entries(models)) {
|
|
25
|
+
const model = modelUnknown as Record<string, unknown>;
|
|
26
|
+
const relations = model['relations'] as Record<string, Record<string, unknown>> | undefined;
|
|
27
|
+
const cleanedRelations = stripStrategyFromRelations(relations);
|
|
28
|
+
|
|
29
|
+
const fields = model['fields'] as Record<string, Record<string, unknown>> | undefined;
|
|
30
|
+
if (!fields) {
|
|
31
|
+
result[modelName] = {
|
|
32
|
+
...model,
|
|
33
|
+
...(cleanedRelations !== undefined ? { relations: cleanedRelations } : {}),
|
|
34
|
+
};
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const hasEnrichedFields = Object.values(fields).some((f) => f['codecId'] !== undefined);
|
|
39
|
+
if (!hasEnrichedFields) {
|
|
40
|
+
result[modelName] = {
|
|
41
|
+
...model,
|
|
42
|
+
...(cleanedRelations !== undefined ? { relations: cleanedRelations } : {}),
|
|
43
|
+
};
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const storage = (model['storage'] ?? {}) as Record<string, unknown>;
|
|
48
|
+
const existingStorageFields = (storage['fields'] ?? {}) as Record<
|
|
49
|
+
string,
|
|
50
|
+
Record<string, unknown>
|
|
51
|
+
>;
|
|
52
|
+
const mergedStorageFields: Record<string, Record<string, unknown>> = {
|
|
53
|
+
...existingStorageFields,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const cleanedFields: Record<string, Record<string, unknown>> = {};
|
|
57
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
58
|
+
const { column, ...domainOnly } = field;
|
|
59
|
+
if (domainOnly['nullable'] === undefined) {
|
|
60
|
+
domainOnly['nullable'] = false;
|
|
61
|
+
}
|
|
62
|
+
cleanedFields[fieldName] = domainOnly;
|
|
63
|
+
|
|
64
|
+
if (column !== undefined && !mergedStorageFields[fieldName]) {
|
|
65
|
+
mergedStorageFields[fieldName] = { column };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
result[modelName] = {
|
|
70
|
+
...model,
|
|
71
|
+
fields: cleanedFields,
|
|
72
|
+
...(cleanedRelations !== undefined ? { relations: cleanedRelations } : {}),
|
|
73
|
+
storage: { ...storage, fields: mergedStorageFields },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const CanonicalMetaSchema = type({
|
|
80
|
+
'[string]': 'unknown',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const CanonicalContractSchema = type({
|
|
84
|
+
'+': 'reject',
|
|
85
|
+
schemaVersion: 'string',
|
|
86
|
+
targetFamily: 'string',
|
|
87
|
+
target: 'string',
|
|
88
|
+
models: type({ '[string]': 'unknown' }),
|
|
89
|
+
'relations?': type({ '[string]': 'unknown' }),
|
|
90
|
+
'roots?': 'Record<string, string>',
|
|
91
|
+
storage: type({ '[string]': 'unknown' }),
|
|
92
|
+
'execution?': type({ '[string]': 'unknown' }),
|
|
93
|
+
extensionPacks: type({ '[string]': 'unknown' }),
|
|
94
|
+
capabilities: type({
|
|
95
|
+
'[string]': type({
|
|
96
|
+
'[string]': 'boolean',
|
|
97
|
+
}),
|
|
98
|
+
}),
|
|
99
|
+
meta: CanonicalMetaSchema,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function assertCanonicalArtifactShape(value: unknown): void {
|
|
103
|
+
const result = CanonicalContractSchema(value);
|
|
104
|
+
if (result instanceof type.errors) {
|
|
105
|
+
const issues = result
|
|
106
|
+
.map((error) => {
|
|
107
|
+
const path = error.path?.toString() ?? '<root>';
|
|
108
|
+
return `${path}: ${error.message}`;
|
|
109
|
+
})
|
|
110
|
+
.join('; ');
|
|
111
|
+
throw new Error(`ContractIR canonical artifact validation failed: ${issues}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
8
115
|
function validateCoreStructure(ir: ContractIR): void {
|
|
9
116
|
if (!ir.targetFamily) {
|
|
10
117
|
throw new Error('ContractIR must have targetFamily');
|
|
@@ -21,8 +128,8 @@ function validateCoreStructure(ir: ContractIR): void {
|
|
|
21
128
|
if (!ir.storage || typeof ir.storage !== 'object') {
|
|
22
129
|
throw new Error('ContractIR must have storage');
|
|
23
130
|
}
|
|
24
|
-
if (
|
|
25
|
-
throw new Error('ContractIR must
|
|
131
|
+
if (ir.relations !== undefined && typeof ir.relations !== 'object') {
|
|
132
|
+
throw new Error('ContractIR relations must be an object when provided');
|
|
26
133
|
}
|
|
27
134
|
if (!ir.extensionPacks || typeof ir.extensionPacks !== 'object') {
|
|
28
135
|
throw new Error('ContractIR must have extensionPacks');
|
|
@@ -33,9 +140,6 @@ function validateCoreStructure(ir: ContractIR): void {
|
|
|
33
140
|
if (!ir.meta || typeof ir.meta !== 'object') {
|
|
34
141
|
throw new Error('ContractIR must have meta');
|
|
35
142
|
}
|
|
36
|
-
if (!ir.sources || typeof ir.sources !== 'object') {
|
|
37
|
-
throw new Error('ContractIR must have sources');
|
|
38
|
-
}
|
|
39
143
|
}
|
|
40
144
|
|
|
41
145
|
export async function emit(
|
|
@@ -43,40 +147,53 @@ export async function emit(
|
|
|
43
147
|
options: EmitOptions,
|
|
44
148
|
targetFamily: TargetFamilyHook,
|
|
45
149
|
): Promise<EmitResult> {
|
|
46
|
-
const {
|
|
150
|
+
const {
|
|
151
|
+
operationRegistry,
|
|
152
|
+
codecTypeImports,
|
|
153
|
+
operationTypeImports,
|
|
154
|
+
extensionIds,
|
|
155
|
+
parameterizedRenderers,
|
|
156
|
+
parameterizedTypeImports,
|
|
157
|
+
queryOperationTypeImports,
|
|
158
|
+
} = options;
|
|
47
159
|
|
|
48
160
|
validateCoreStructure(ir);
|
|
49
161
|
|
|
50
162
|
const ctx: ValidationContext = {
|
|
51
|
-
...(operationRegistry
|
|
52
|
-
...(codecTypeImports
|
|
53
|
-
...(operationTypeImports
|
|
54
|
-
...(extensionIds
|
|
163
|
+
...ifDefined('operationRegistry', operationRegistry),
|
|
164
|
+
...ifDefined('codecTypeImports', codecTypeImports),
|
|
165
|
+
...ifDefined('operationTypeImports', operationTypeImports),
|
|
166
|
+
...ifDefined('extensionIds', extensionIds),
|
|
55
167
|
};
|
|
56
168
|
targetFamily.validateTypes(ir, ctx);
|
|
57
169
|
|
|
58
170
|
targetFamily.validateStructure(ir);
|
|
59
171
|
|
|
60
|
-
const
|
|
172
|
+
const canonicalContract = {
|
|
61
173
|
schemaVersion: ir.schemaVersion,
|
|
62
174
|
targetFamily: ir.targetFamily,
|
|
63
175
|
target: ir.target,
|
|
64
|
-
|
|
65
|
-
|
|
176
|
+
...ifDefined('roots', ir.roots),
|
|
177
|
+
models: toDomainModel(ir.models as Record<string, unknown>),
|
|
178
|
+
...ifDefined('relations', ir.relations),
|
|
66
179
|
storage: ir.storage,
|
|
180
|
+
...ifDefined('execution', ir.execution),
|
|
67
181
|
extensionPacks: ir.extensionPacks,
|
|
68
182
|
capabilities: ir.capabilities,
|
|
69
183
|
meta: ir.meta,
|
|
70
|
-
|
|
71
|
-
|
|
184
|
+
};
|
|
185
|
+
assertCanonicalArtifactShape(canonicalContract);
|
|
72
186
|
|
|
73
|
-
const
|
|
74
|
-
const
|
|
187
|
+
const storageHash = computeStorageHash(canonicalContract);
|
|
188
|
+
const executionHash = canonicalContract.execution
|
|
189
|
+
? computeExecutionHash(canonicalContract)
|
|
190
|
+
: undefined;
|
|
191
|
+
const profileHash = computeProfileHash(canonicalContract);
|
|
75
192
|
|
|
76
|
-
const contractWithHashes
|
|
77
|
-
...
|
|
78
|
-
|
|
79
|
-
|
|
193
|
+
const contractWithHashes = {
|
|
194
|
+
...canonicalContract,
|
|
195
|
+
storageHash,
|
|
196
|
+
...ifDefined('executionHash', executionHash),
|
|
80
197
|
profileHash,
|
|
81
198
|
};
|
|
82
199
|
|
|
@@ -91,16 +208,32 @@ export async function emit(
|
|
|
91
208
|
...contractJsonObj,
|
|
92
209
|
_generated: {
|
|
93
210
|
warning: '⚠️ GENERATED FILE - DO NOT EDIT',
|
|
94
|
-
message: 'This file is automatically generated by "prisma-next emit".',
|
|
95
|
-
regenerate: 'To regenerate, run: prisma-next emit',
|
|
211
|
+
message: 'This file is automatically generated by "prisma-next contract emit".',
|
|
212
|
+
regenerate: 'To regenerate, run: prisma-next contract emit',
|
|
96
213
|
},
|
|
97
214
|
};
|
|
98
215
|
const contractJsonString = JSON.stringify(contractJsonWithMeta, null, 2);
|
|
99
216
|
|
|
217
|
+
const generateOptions =
|
|
218
|
+
parameterizedRenderers || parameterizedTypeImports || queryOperationTypeImports
|
|
219
|
+
? {
|
|
220
|
+
...ifDefined('parameterizedRenderers', parameterizedRenderers),
|
|
221
|
+
...ifDefined('parameterizedTypeImports', parameterizedTypeImports),
|
|
222
|
+
...ifDefined('queryOperationTypeImports', queryOperationTypeImports),
|
|
223
|
+
}
|
|
224
|
+
: undefined;
|
|
225
|
+
|
|
226
|
+
const contractTypeHashes = {
|
|
227
|
+
storageHash,
|
|
228
|
+
...ifDefined('executionHash', executionHash),
|
|
229
|
+
profileHash,
|
|
230
|
+
};
|
|
100
231
|
const contractDtsRaw = targetFamily.generateContractTypes(
|
|
101
232
|
ir,
|
|
102
233
|
codecTypeImports ?? [],
|
|
103
234
|
operationTypeImports ?? [],
|
|
235
|
+
contractTypeHashes,
|
|
236
|
+
generateOptions,
|
|
104
237
|
);
|
|
105
238
|
const contractDts = await format(contractDtsRaw, {
|
|
106
239
|
parser: 'typescript',
|
|
@@ -112,7 +245,8 @@ export async function emit(
|
|
|
112
245
|
return {
|
|
113
246
|
contractJson: contractJsonString,
|
|
114
247
|
contractDts,
|
|
115
|
-
|
|
248
|
+
storageHash,
|
|
249
|
+
...ifDefined('executionHash', executionHash),
|
|
116
250
|
profileHash,
|
|
117
251
|
};
|
|
118
252
|
}
|
package/src/emission/hashing.ts
CHANGED
|
@@ -1,46 +1,32 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import
|
|
2
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
3
|
+
import type { CanonicalContractInput } from './canonicalization';
|
|
3
4
|
import { canonicalizeContract } from './canonicalization';
|
|
4
5
|
|
|
5
|
-
type ContractInput = {
|
|
6
|
-
schemaVersion: string;
|
|
7
|
-
targetFamily: string;
|
|
8
|
-
target: string;
|
|
9
|
-
models: Record<string, unknown>;
|
|
10
|
-
relations: Record<string, unknown>;
|
|
11
|
-
storage: Record<string, unknown>;
|
|
12
|
-
extensionPacks: Record<string, unknown>;
|
|
13
|
-
sources: Record<string, unknown>;
|
|
14
|
-
capabilities: Record<string, Record<string, boolean>>;
|
|
15
|
-
meta: Record<string, unknown>;
|
|
16
|
-
[key: string]: unknown;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
6
|
function computeHash(content: string): string {
|
|
20
7
|
const hash = createHash('sha256');
|
|
21
8
|
hash.update(content);
|
|
22
9
|
return `sha256:${hash.digest('hex')}`;
|
|
23
10
|
}
|
|
24
11
|
|
|
25
|
-
export function
|
|
26
|
-
const
|
|
12
|
+
export function computeStorageHash(contract: CanonicalContractInput): string {
|
|
13
|
+
const storageContract = {
|
|
27
14
|
schemaVersion: contract.schemaVersion,
|
|
28
15
|
targetFamily: contract.targetFamily,
|
|
29
16
|
target: contract.target,
|
|
30
|
-
models: contract.models,
|
|
31
|
-
relations: contract.relations,
|
|
32
17
|
storage: contract.storage,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
models: {},
|
|
19
|
+
relations: {},
|
|
20
|
+
extensionPacks: {},
|
|
21
|
+
capabilities: {},
|
|
22
|
+
meta: {},
|
|
37
23
|
};
|
|
38
|
-
const canonical = canonicalizeContract(
|
|
24
|
+
const canonical = canonicalizeContract(storageContract);
|
|
39
25
|
return computeHash(canonical);
|
|
40
26
|
}
|
|
41
27
|
|
|
42
|
-
export function computeProfileHash(contract:
|
|
43
|
-
const profileContract
|
|
28
|
+
export function computeProfileHash(contract: CanonicalContractInput): string {
|
|
29
|
+
const profileContract = {
|
|
44
30
|
schemaVersion: contract.schemaVersion,
|
|
45
31
|
targetFamily: contract.targetFamily,
|
|
46
32
|
target: contract.target,
|
|
@@ -50,8 +36,24 @@ export function computeProfileHash(contract: ContractInput): string {
|
|
|
50
36
|
extensionPacks: {},
|
|
51
37
|
capabilities: contract.capabilities,
|
|
52
38
|
meta: {},
|
|
53
|
-
sources: {},
|
|
54
39
|
};
|
|
55
40
|
const canonical = canonicalizeContract(profileContract);
|
|
56
41
|
return computeHash(canonical);
|
|
57
42
|
}
|
|
43
|
+
|
|
44
|
+
export function computeExecutionHash(contract: CanonicalContractInput): string {
|
|
45
|
+
const executionContract = {
|
|
46
|
+
schemaVersion: contract.schemaVersion,
|
|
47
|
+
targetFamily: contract.targetFamily,
|
|
48
|
+
target: contract.target,
|
|
49
|
+
models: {},
|
|
50
|
+
relations: {},
|
|
51
|
+
storage: {},
|
|
52
|
+
extensionPacks: {},
|
|
53
|
+
capabilities: {},
|
|
54
|
+
meta: {},
|
|
55
|
+
...ifDefined('execution', contract.execution),
|
|
56
|
+
};
|
|
57
|
+
const canonical = canonicalizeContract(executionContract);
|
|
58
|
+
return computeHash(canonical);
|
|
59
|
+
}
|
package/src/emission/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TypesImportSpec } from '@prisma-next/contract/types';
|
|
1
|
+
import type { TypeRenderEntry, TypesImportSpec } from '@prisma-next/contract/types';
|
|
2
2
|
import type { OperationRegistry } from '@prisma-next/operations';
|
|
3
3
|
|
|
4
4
|
export interface EmitOptions {
|
|
@@ -7,11 +7,27 @@ export interface EmitOptions {
|
|
|
7
7
|
readonly codecTypeImports?: ReadonlyArray<TypesImportSpec>;
|
|
8
8
|
readonly operationTypeImports?: ReadonlyArray<TypesImportSpec>;
|
|
9
9
|
readonly extensionIds?: ReadonlyArray<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Normalized parameterized type renderers, keyed by codecId.
|
|
12
|
+
* These are extracted from descriptors and normalized during assembly.
|
|
13
|
+
*/
|
|
14
|
+
readonly parameterizedRenderers?: Map<string, TypeRenderEntry>;
|
|
15
|
+
/**
|
|
16
|
+
* Type imports for parameterized codecs.
|
|
17
|
+
* These are added to contract.d.ts alongside codec and operation type imports.
|
|
18
|
+
*/
|
|
19
|
+
readonly parameterizedTypeImports?: ReadonlyArray<TypesImportSpec>;
|
|
20
|
+
/**
|
|
21
|
+
* Query operation type imports for the query builder.
|
|
22
|
+
* Flat operation signatures keyed by operation name.
|
|
23
|
+
*/
|
|
24
|
+
readonly queryOperationTypeImports?: ReadonlyArray<TypesImportSpec>;
|
|
10
25
|
}
|
|
11
26
|
|
|
12
27
|
export interface EmitResult {
|
|
13
28
|
readonly contractJson: string;
|
|
14
29
|
readonly contractDts: string;
|
|
15
|
-
readonly
|
|
30
|
+
readonly storageHash: string;
|
|
31
|
+
readonly executionHash?: string;
|
|
16
32
|
readonly profileHash: string;
|
|
17
33
|
}
|