@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.
Files changed (97) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +31 -99
  3. package/dist/constants.d.mts +9 -0
  4. package/dist/constants.d.mts.map +1 -0
  5. package/dist/constants.mjs +10 -0
  6. package/dist/constants.mjs.map +1 -0
  7. package/dist/emission.d.mts +64 -0
  8. package/dist/emission.d.mts.map +1 -0
  9. package/dist/emission.mjs +374 -0
  10. package/dist/emission.mjs.map +1 -0
  11. package/dist/errors.d.mts +232 -0
  12. package/dist/errors.d.mts.map +1 -0
  13. package/dist/errors.mjs +330 -0
  14. package/dist/errors.mjs.map +1 -0
  15. package/dist/{schema-view.d.ts → schema-view-DObwT8x9.d.mts} +17 -14
  16. package/dist/schema-view-DObwT8x9.d.mts.map +1 -0
  17. package/dist/schema-view.d.mts +2 -0
  18. package/dist/schema-view.mjs +1 -0
  19. package/dist/stack.d.mts +30 -0
  20. package/dist/stack.d.mts.map +1 -0
  21. package/dist/stack.mjs +30 -0
  22. package/dist/stack.mjs.map +1 -0
  23. package/dist/types-B8rNCYSV.d.mts +615 -0
  24. package/dist/types-B8rNCYSV.d.mts.map +1 -0
  25. package/dist/types.d.mts +2 -0
  26. package/dist/types.mjs +1 -0
  27. package/package.json +28 -40
  28. package/src/constants.ts +5 -0
  29. package/src/emission/canonicalization.ts +82 -23
  30. package/src/emission/emit.ts +159 -25
  31. package/src/emission/hashing.ts +29 -27
  32. package/src/emission/types.ts +18 -2
  33. package/src/errors.ts +127 -20
  34. package/src/exports/constants.ts +1 -0
  35. package/src/exports/emission.ts +1 -1
  36. package/src/exports/errors.ts +6 -1
  37. package/src/exports/types.ts +0 -1
  38. package/src/migrations.ts +27 -1
  39. package/src/schema-view.ts +5 -5
  40. package/src/types.ts +25 -12
  41. package/dist/chunk-473ODD3P.js +0 -14
  42. package/dist/chunk-473ODD3P.js.map +0 -1
  43. package/dist/chunk-U5RYT6PT.js +0 -229
  44. package/dist/chunk-U5RYT6PT.js.map +0 -1
  45. package/dist/config-types.d.ts +0 -68
  46. package/dist/config-types.d.ts.map +0 -1
  47. package/dist/config-validation.d.ts +0 -10
  48. package/dist/config-validation.d.ts.map +0 -1
  49. package/dist/emission/canonicalization.d.ts +0 -6
  50. package/dist/emission/canonicalization.d.ts.map +0 -1
  51. package/dist/emission/emit.d.ts +0 -5
  52. package/dist/emission/emit.d.ts.map +0 -1
  53. package/dist/emission/hashing.d.ts +0 -17
  54. package/dist/emission/hashing.d.ts.map +0 -1
  55. package/dist/emission/types.d.ts +0 -16
  56. package/dist/emission/types.d.ts.map +0 -1
  57. package/dist/errors.d.ts +0 -183
  58. package/dist/errors.d.ts.map +0 -1
  59. package/dist/exports/config-types.d.ts +0 -3
  60. package/dist/exports/config-types.d.ts.map +0 -1
  61. package/dist/exports/config-types.js +0 -53
  62. package/dist/exports/config-types.js.map +0 -1
  63. package/dist/exports/config-validation.d.ts +0 -2
  64. package/dist/exports/config-validation.d.ts.map +0 -1
  65. package/dist/exports/config-validation.js +0 -252
  66. package/dist/exports/config-validation.js.map +0 -1
  67. package/dist/exports/emission.d.ts +0 -5
  68. package/dist/exports/emission.d.ts.map +0 -1
  69. package/dist/exports/emission.js +0 -310
  70. package/dist/exports/emission.js.map +0 -1
  71. package/dist/exports/errors.d.ts +0 -3
  72. package/dist/exports/errors.d.ts.map +0 -1
  73. package/dist/exports/errors.js +0 -43
  74. package/dist/exports/errors.js.map +0 -1
  75. package/dist/exports/schema-view.d.ts +0 -2
  76. package/dist/exports/schema-view.d.ts.map +0 -1
  77. package/dist/exports/schema-view.js +0 -1
  78. package/dist/exports/schema-view.js.map +0 -1
  79. package/dist/exports/stack.d.ts +0 -2
  80. package/dist/exports/stack.d.ts.map +0 -1
  81. package/dist/exports/stack.js +0 -7
  82. package/dist/exports/stack.js.map +0 -1
  83. package/dist/exports/types.d.ts +0 -3
  84. package/dist/exports/types.d.ts.map +0 -1
  85. package/dist/exports/types.js +0 -7
  86. package/dist/exports/types.js.map +0 -1
  87. package/dist/migrations.d.ts +0 -190
  88. package/dist/migrations.d.ts.map +0 -1
  89. package/dist/schema-view.d.ts.map +0 -1
  90. package/dist/stack.d.ts +0 -25
  91. package/dist/stack.d.ts.map +0 -1
  92. package/dist/types.d.ts +0 -411
  93. package/dist/types.d.ts.map +0 -1
  94. package/src/config-types.ts +0 -157
  95. package/src/config-validation.ts +0 -270
  96. package/src/exports/config-types.ts +0 -5
  97. 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.13",
3
+ "version": "0.3.0-dev.131",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
- "description": "Control plane domain actions, config types, validation, and error factories for Prisma Next",
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/contract": "0.3.0-dev.13",
11
- "@prisma-next/operations": "0.3.0-dev.13",
12
- "@prisma-next/utils": "0.3.0-dev.13"
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
- "@vitest/coverage-v8": "4.0.16",
16
- "tsup": "8.5.1",
15
+ "tsdown": "0.18.4",
17
16
  "typescript": "5.9.3",
18
- "vitest": "4.0.16",
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
- "./config-types": {
27
- "types": "./dist/exports/config-types.d.ts",
28
- "import": "./dist/exports/config-types.js"
29
- },
30
- "./config-validation": {
31
- "types": "./dist/exports/config-validation.d.ts",
32
- "import": "./dist/exports/config-validation.js"
33
- },
34
- "./errors": {
35
- "types": "./dist/exports/errors.d.ts",
36
- "import": "./dist/exports/errors.js"
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": "tsup --config tsup.config.ts && tsc --project tsconfig.build.json",
44
+ "build": "tsdown",
57
45
  "test": "vitest run --passWithNoTests",
58
46
  "test:coverage": "vitest run --coverage --passWithNoTests",
59
- "typecheck": "tsc --project tsconfig.json --noEmit",
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": "node ../../../../../scripts/clean.mjs"
51
+ "clean": "rm -rf dist dist-tsc dist-tsc-prod coverage .tmp-output"
64
52
  }
65
53
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Sentinel value representing the absence of a contract (empty/new project).
3
+ * This is a human-readable marker, not a real SHA-256 hash.
4
+ */
5
+ export const EMPTY_CONTRACT_HASH = 'sha256:empty' as const;
@@ -1,19 +1,39 @@
1
- import type { ContractIR } from '@prisma-next/contract/ir';
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
- coreHash?: string;
9
+ storageHash?: string;
10
+ executionHash?: string;
9
11
  profileHash?: string;
12
+ roots?: Record<string, string>;
10
13
  models: Record<string, unknown>;
11
- relations: Record<string, unknown>;
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
- sources: Record<string, unknown>;
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
- 'coreHash',
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 isRequiredSources = isArrayEqual(currentPath, ['sources']);
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
- !isRequiredSources &&
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: ir.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
- if (ir.coreHash !== undefined) {
239
- normalized.coreHash = ir.coreHash;
240
- }
241
-
242
- if (ir.profileHash !== undefined) {
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, null, 2);
311
+ return JSON.stringify(withOrderedTopLevel, bigintJsonReplacer, 2);
253
312
  }
@@ -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 { computeCoreHash, computeProfileHash } from './hashing';
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 (!ir.relations || typeof ir.relations !== 'object') {
25
- throw new Error('ContractIR must have relations');
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 { operationRegistry, codecTypeImports, operationTypeImports, extensionIds } = options;
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 ? { operationRegistry } : {}),
52
- ...(codecTypeImports ? { codecTypeImports } : {}),
53
- ...(operationTypeImports ? { operationTypeImports } : {}),
54
- ...(extensionIds ? { 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 contractJson = {
172
+ const canonicalContract = {
61
173
  schemaVersion: ir.schemaVersion,
62
174
  targetFamily: ir.targetFamily,
63
175
  target: ir.target,
64
- models: ir.models,
65
- relations: ir.relations,
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
- sources: ir.sources,
71
- } as const;
184
+ };
185
+ assertCanonicalArtifactShape(canonicalContract);
72
186
 
73
- const coreHash = computeCoreHash(contractJson);
74
- const profileHash = computeProfileHash(contractJson);
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: ContractIR & { coreHash?: string; profileHash?: string } = {
77
- ...ir,
78
- schemaVersion: contractJson.schemaVersion,
79
- coreHash,
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
- coreHash,
248
+ storageHash,
249
+ ...ifDefined('executionHash', executionHash),
116
250
  profileHash,
117
251
  };
118
252
  }
@@ -1,46 +1,32 @@
1
1
  import { createHash } from 'node:crypto';
2
- import type { ContractIR } from '@prisma-next/contract/ir';
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 computeCoreHash(contract: ContractInput): string {
26
- const coreContract: ContractIR = {
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
- extensionPacks: contract.extensionPacks,
34
- sources: contract.sources,
35
- capabilities: contract.capabilities,
36
- meta: contract.meta,
18
+ models: {},
19
+ relations: {},
20
+ extensionPacks: {},
21
+ capabilities: {},
22
+ meta: {},
37
23
  };
38
- const canonical = canonicalizeContract(coreContract);
24
+ const canonical = canonicalizeContract(storageContract);
39
25
  return computeHash(canonical);
40
26
  }
41
27
 
42
- export function computeProfileHash(contract: ContractInput): string {
43
- const profileContract: ContractIR = {
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
+ }
@@ -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 coreHash: string;
30
+ readonly storageHash: string;
31
+ readonly executionHash?: string;
16
32
  readonly profileHash: string;
17
33
  }