@prisma-next/sql-contract-ts 0.14.0-dev.6 → 0.14.0-dev.60
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/{build-contract-CQ4u83jx.mjs → build-contract-CSyrbCoJ.mjs} +239 -30
- package/dist/build-contract-CSyrbCoJ.mjs.map +1 -0
- package/dist/config-types.d.mts +2 -0
- package/dist/config-types.d.mts.map +1 -1
- package/dist/config-types.mjs +2 -1
- package/dist/config-types.mjs.map +1 -1
- package/dist/contract-builder.d.mts +231 -328
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +43 -75
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +10 -10
- package/src/authoring-helper-runtime.ts +2 -6
- package/src/build-contract.ts +402 -49
- package/src/composed-authoring-helpers.ts +3 -6
- package/src/config-types.ts +7 -1
- package/src/contract-builder.ts +71 -5
- package/src/contract-definition.ts +27 -4
- package/src/contract-dsl.ts +117 -41
- package/src/contract-lowering.ts +5 -1
- package/src/contract-types.ts +34 -10
- package/src/enum-type.ts +14 -306
- package/dist/build-contract-CQ4u83jx.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-contract-ts",
|
|
3
|
-
"version": "0.14.0-dev.
|
|
3
|
+
"version": "0.14.0-dev.60",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "SQL-specific TypeScript contract authoring surface for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/config": "0.14.0-dev.
|
|
10
|
-
"@prisma-next/contract": "0.14.0-dev.
|
|
11
|
-
"@prisma-next/contract-authoring": "0.14.0-dev.
|
|
12
|
-
"@prisma-next/framework-components": "0.14.0-dev.
|
|
13
|
-
"@prisma-next/sql-contract": "0.14.0-dev.
|
|
14
|
-
"@prisma-next/utils": "0.14.0-dev.
|
|
9
|
+
"@prisma-next/config": "0.14.0-dev.60",
|
|
10
|
+
"@prisma-next/contract": "0.14.0-dev.60",
|
|
11
|
+
"@prisma-next/contract-authoring": "0.14.0-dev.60",
|
|
12
|
+
"@prisma-next/framework-components": "0.14.0-dev.60",
|
|
13
|
+
"@prisma-next/sql-contract": "0.14.0-dev.60",
|
|
14
|
+
"@prisma-next/utils": "0.14.0-dev.60",
|
|
15
15
|
"arktype": "^2.2.0",
|
|
16
16
|
"pathe": "^2.0.3",
|
|
17
17
|
"ts-toolbelt": "^9.6.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@prisma-next/test-utils": "0.14.0-dev.
|
|
21
|
-
"@prisma-next/tsconfig": "0.14.0-dev.
|
|
20
|
+
"@prisma-next/test-utils": "0.14.0-dev.60",
|
|
21
|
+
"@prisma-next/tsconfig": "0.14.0-dev.60",
|
|
22
22
|
"@types/pg": "8.20.0",
|
|
23
23
|
"pg": "8.21.0",
|
|
24
|
-
"@prisma-next/tsdown": "0.14.0-dev.
|
|
24
|
+
"@prisma-next/tsdown": "0.14.0-dev.60",
|
|
25
25
|
"tsdown": "0.22.1",
|
|
26
26
|
"typescript": "5.9.3",
|
|
27
27
|
"vitest": "4.1.8"
|
|
@@ -63,7 +63,7 @@ export function createTypeHelpersFromNamespace(
|
|
|
63
63
|
continue;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
helpers[key] = createTypeHelpersFromNamespace(value
|
|
66
|
+
helpers[key] = createTypeHelpersFromNamespace(value, currentPath);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
return helpers;
|
|
@@ -133,11 +133,7 @@ export function createFieldHelpersFromNamespace(
|
|
|
133
133
|
continue;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
helpers[key] = createFieldHelpersFromNamespace(
|
|
137
|
-
value as AuthoringFieldNamespace,
|
|
138
|
-
createLeafHelper,
|
|
139
|
-
currentPath,
|
|
140
|
-
);
|
|
136
|
+
helpers[key] = createFieldHelpersFromNamespace(value, createLeafHelper, currentPath);
|
|
141
137
|
}
|
|
142
138
|
|
|
143
139
|
return helpers;
|
package/src/build-contract.ts
CHANGED
|
@@ -22,9 +22,16 @@ import {
|
|
|
22
22
|
type ValueSetRef,
|
|
23
23
|
} from '@prisma-next/contract/types';
|
|
24
24
|
import { type CapabilityMatrix, mergeCapabilityMatrices } from '@prisma-next/contract-authoring';
|
|
25
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
AuthoringContributions,
|
|
27
|
+
AuthoringEntityTypeDescriptor,
|
|
28
|
+
AuthoringEntityTypeNamespace,
|
|
29
|
+
} from '@prisma-next/framework-components/authoring';
|
|
30
|
+
import { isAuthoringEntityTypeDescriptor } from '@prisma-next/framework-components/authoring';
|
|
31
|
+
import type { CodecLookup, ColumnTypeDescriptor } from '@prisma-next/framework-components/codec';
|
|
26
32
|
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
27
33
|
import { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';
|
|
34
|
+
import { tableEntityKind, valueSetEntityKind } from '@prisma-next/sql-contract/entity-kinds';
|
|
28
35
|
import { validateIndexTypes } from '@prisma-next/sql-contract/index-type-validation';
|
|
29
36
|
import {
|
|
30
37
|
createIndexTypeRegistry,
|
|
@@ -33,9 +40,8 @@ import {
|
|
|
33
40
|
} from '@prisma-next/sql-contract/index-types';
|
|
34
41
|
import {
|
|
35
42
|
applyFkDefaults,
|
|
36
|
-
buildSqlNamespace,
|
|
37
43
|
type CheckConstraintInput,
|
|
38
|
-
type
|
|
44
|
+
type SqlNamespaceInput,
|
|
39
45
|
SqlStorage,
|
|
40
46
|
type SqlStorageInput,
|
|
41
47
|
type StorageColumn,
|
|
@@ -45,6 +51,7 @@ import {
|
|
|
45
51
|
toStorageTypeInstance,
|
|
46
52
|
} from '@prisma-next/sql-contract/types';
|
|
47
53
|
import { validateStorageSemantics } from '@prisma-next/sql-contract/validators';
|
|
54
|
+
import { deriveValueSetFromEntity } from '@prisma-next/sql-contract/value-set-derivation-hook';
|
|
48
55
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
49
56
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
50
57
|
import type {
|
|
@@ -74,10 +81,23 @@ function encodeColumnDefault(
|
|
|
74
81
|
defaultInput: ColumnDefault,
|
|
75
82
|
codecId: string,
|
|
76
83
|
codecLookup?: CodecLookup,
|
|
84
|
+
many = false,
|
|
77
85
|
): ColumnDefault {
|
|
78
86
|
if (defaultInput.kind === 'function') {
|
|
79
87
|
return { kind: 'function', expression: defaultInput.expression };
|
|
80
88
|
}
|
|
89
|
+
if (many) {
|
|
90
|
+
if (!Array.isArray(defaultInput.value)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Literal default on a list column must be an array; received ${typeof defaultInput.value}. ` +
|
|
93
|
+
'A scalar default on a list field must be rejected at the authoring surface.',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
kind: 'literal',
|
|
98
|
+
value: defaultInput.value.map((element) => encodeViaCodec(element, codecId, codecLookup)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
81
101
|
return {
|
|
82
102
|
kind: 'literal',
|
|
83
103
|
value: encodeViaCodec(defaultInput.value, codecId, codecLookup),
|
|
@@ -160,6 +180,174 @@ function isValueObjectField(
|
|
|
160
180
|
return 'valueObjectName' in field;
|
|
161
181
|
}
|
|
162
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Resolves a deferred entity-ref column descriptor (e.g. a `pg.enum(handle)`
|
|
185
|
+
* column) against the field's now-known owning namespace: attaches the
|
|
186
|
+
* storage `valueSet` ref the collected entity's derived value-set is stored
|
|
187
|
+
* under. `nativeType` / `typeParams.typeName` stay bare here — schema
|
|
188
|
+
* qualification (e.g. `auth.aal_level`) is a target concern applied in the
|
|
189
|
+
* next step, `qualifyColumnDescriptor`. A descriptor with no `entityRef` (the
|
|
190
|
+
* ordinary case) passes through unchanged.
|
|
191
|
+
*/
|
|
192
|
+
function resolveEntityRefDescriptor(
|
|
193
|
+
descriptor: ColumnTypeDescriptor,
|
|
194
|
+
namespaceId: string,
|
|
195
|
+
): ColumnTypeDescriptor {
|
|
196
|
+
const entityRef = descriptor.entityRef;
|
|
197
|
+
if (entityRef === undefined) return descriptor;
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
...descriptor,
|
|
201
|
+
valueSet: {
|
|
202
|
+
plane: 'storage',
|
|
203
|
+
entityKind: 'valueSet',
|
|
204
|
+
namespaceId,
|
|
205
|
+
entityName: entityRef.entityName,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A target's contract-construction-time column-type qualifier, contributed
|
|
212
|
+
* through `target.authoring.qualifyColumnType`. Given a column's bare type
|
|
213
|
+
* info and its owning `namespaceId`, it returns the type info the target's
|
|
214
|
+
* schema semantics require (e.g. Postgres schema-qualifies a native-enum
|
|
215
|
+
* column's type name to `auth.aal_level`). The dispatch keys off the codec
|
|
216
|
+
* id, so every codec — including ones needing no change — is passed through
|
|
217
|
+
* and the caller stays codec-blind. Targets without the hook leave every
|
|
218
|
+
* column bare.
|
|
219
|
+
*/
|
|
220
|
+
type ColumnTypeQualifier = (
|
|
221
|
+
input: {
|
|
222
|
+
readonly codecId: string;
|
|
223
|
+
readonly nativeType: string;
|
|
224
|
+
readonly typeParams?: Record<string, unknown>;
|
|
225
|
+
},
|
|
226
|
+
namespaceId: string,
|
|
227
|
+
) => { readonly nativeType: string; readonly typeParams?: Record<string, unknown> };
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Structural check for a target that contributes a `qualifyColumnType` hook
|
|
231
|
+
* on its authoring contributions. Duck-typed (mirroring
|
|
232
|
+
* `contract-psl`'s `hasColumnFromEntityHook`) so the SQL family stays blind
|
|
233
|
+
* to the target's qualification logic and no framework/family interface has
|
|
234
|
+
* to name the hook.
|
|
235
|
+
*/
|
|
236
|
+
function hasColumnTypeQualifier(
|
|
237
|
+
authoring: AuthoringContributions,
|
|
238
|
+
): authoring is AuthoringContributions & { readonly qualifyColumnType: ColumnTypeQualifier } {
|
|
239
|
+
return 'qualifyColumnType' in authoring && typeof authoring.qualifyColumnType === 'function';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function resolveColumnTypeQualifier(
|
|
243
|
+
target: ContractDefinition['target'],
|
|
244
|
+
): ColumnTypeQualifier | undefined {
|
|
245
|
+
const authoring = target.authoring;
|
|
246
|
+
if (authoring === undefined) return undefined;
|
|
247
|
+
return hasColumnTypeQualifier(authoring) ? authoring.qualifyColumnType : undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Applies the target's `qualifyColumnType` hook to a scalar column descriptor
|
|
252
|
+
* at construction, so the storage column and the domain field (which derives
|
|
253
|
+
* its `type.typeParams` from the storage column) are both built already
|
|
254
|
+
* qualified in a single pass. A descriptor whose codec the target leaves
|
|
255
|
+
* unchanged passes through untouched.
|
|
256
|
+
*/
|
|
257
|
+
function qualifyColumnDescriptor(
|
|
258
|
+
descriptor: ColumnTypeDescriptor,
|
|
259
|
+
namespaceId: string,
|
|
260
|
+
qualify: ColumnTypeQualifier | undefined,
|
|
261
|
+
): ColumnTypeDescriptor {
|
|
262
|
+
if (qualify === undefined) return descriptor;
|
|
263
|
+
const qualified = qualify(
|
|
264
|
+
{
|
|
265
|
+
codecId: descriptor.codecId,
|
|
266
|
+
nativeType: descriptor.nativeType,
|
|
267
|
+
...ifDefined('typeParams', descriptor.typeParams),
|
|
268
|
+
},
|
|
269
|
+
namespaceId,
|
|
270
|
+
);
|
|
271
|
+
if (
|
|
272
|
+
qualified.nativeType === descriptor.nativeType &&
|
|
273
|
+
qualified.typeParams === descriptor.typeParams
|
|
274
|
+
) {
|
|
275
|
+
return descriptor;
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
...descriptor,
|
|
279
|
+
nativeType: qualified.nativeType,
|
|
280
|
+
...ifDefined('typeParams', qualified.typeParams),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
type CollectedPackEntities = Record<string, Record<string, Record<string, unknown>>>;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Records a deferred column's entity-ref into the namespace-scoped collection
|
|
288
|
+
* accumulator, keyed the same way author-declared `packEntities` are
|
|
289
|
+
* (`namespaceId → entityKind → entityName`) — folded into the same namespace
|
|
290
|
+
* assembly `derivePackEntityValueSets`/`entries.<kind>` step, so a collected
|
|
291
|
+
* entity gets its value-set the same way an author-declared one does.
|
|
292
|
+
*
|
|
293
|
+
* The same handle reused by many columns in one namespace is normal (a native
|
|
294
|
+
* enum type backs any number of columns) and records the identical entity once.
|
|
295
|
+
* Two *different* entity instances sharing a name+kind in one namespace is a
|
|
296
|
+
* name collision — the emitted `entries.valueSet.<name>` could only reflect one
|
|
297
|
+
* of them, silently mismatching the other column's type/cast. PSL hard-errors
|
|
298
|
+
* on the equivalent (`PSL_DUPLICATE_DECLARATION`); the TS path rejects it too.
|
|
299
|
+
*/
|
|
300
|
+
function collectPackEntityFromColumn(
|
|
301
|
+
collected: CollectedPackEntities,
|
|
302
|
+
namespaceId: string,
|
|
303
|
+
entityRef: NonNullable<ColumnTypeDescriptor['entityRef']>,
|
|
304
|
+
): void {
|
|
305
|
+
const forNs = collected[namespaceId] ?? {};
|
|
306
|
+
const forKind = forNs[entityRef.entityKind] ?? {};
|
|
307
|
+
const existing = forKind[entityRef.entityName];
|
|
308
|
+
if (existing !== undefined && existing !== entityRef.entity) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`buildSqlContractFromDefinition: two different "${entityRef.entityKind}" entities named "${entityRef.entityName}" in namespace "${namespaceId}" — pack-entity names must be unique per namespace.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
forKind[entityRef.entityName] = entityRef.entity;
|
|
314
|
+
forNs[entityRef.entityKind] = forKind;
|
|
315
|
+
collected[namespaceId] = forNs;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Merges the author-declared `packEntities` for one namespace with the entities
|
|
320
|
+
* collected from that namespace's deferred entity-ref columns. A collected
|
|
321
|
+
* entity that shadows a *different* declared entity of the same kind+name (or
|
|
322
|
+
* vice-versa) is the same name-collision bug `collectPackEntityFromColumn`
|
|
323
|
+
* guards against across columns, so it is rejected the same way — by entity
|
|
324
|
+
* identity, so the same handle declared and used by a column does not throw.
|
|
325
|
+
*/
|
|
326
|
+
function mergeCollectedPackEntities(
|
|
327
|
+
namespaceId: string,
|
|
328
|
+
declared: Readonly<Record<string, Readonly<Record<string, unknown>>>> | undefined,
|
|
329
|
+
collected: Readonly<Record<string, Readonly<Record<string, unknown>>>> | undefined,
|
|
330
|
+
): Readonly<Record<string, Readonly<Record<string, unknown>>>> | undefined {
|
|
331
|
+
if (declared === undefined) return collected;
|
|
332
|
+
if (collected === undefined) return declared;
|
|
333
|
+
const kinds = new Set([...Object.keys(declared), ...Object.keys(collected)]);
|
|
334
|
+
const result: Record<string, Readonly<Record<string, unknown>>> = {};
|
|
335
|
+
for (const kind of kinds) {
|
|
336
|
+
const declaredForKind = declared[kind];
|
|
337
|
+
const collectedForKind = collected[kind];
|
|
338
|
+
for (const [name, entity] of Object.entries(collectedForKind ?? {})) {
|
|
339
|
+
const existing = declaredForKind?.[name];
|
|
340
|
+
if (existing !== undefined && existing !== entity) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`buildSqlContractFromDefinition: two different "${kind}" entities named "${name}" in namespace "${namespaceId}" — a collected pack entity conflicts with an author-declared one; pack-entity names must be unique per namespace.`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
result[kind] = { ...declaredForKind, ...collectedForKind };
|
|
347
|
+
}
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
163
351
|
const JSONB_CODEC_ID = 'pg/jsonb@1';
|
|
164
352
|
const JSONB_NATIVE_TYPE = 'jsonb';
|
|
165
353
|
|
|
@@ -235,28 +423,28 @@ function buildStorageColumn(
|
|
|
235
423
|
};
|
|
236
424
|
}
|
|
237
425
|
|
|
238
|
-
if (field.many) {
|
|
239
|
-
return {
|
|
240
|
-
nativeType: JSONB_NATIVE_TYPE,
|
|
241
|
-
codecId: JSONB_CODEC_ID,
|
|
242
|
-
nullable: field.nullable,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
426
|
const codecId = field.descriptor.codecId;
|
|
247
427
|
const encodedDefault =
|
|
248
428
|
field.default !== undefined
|
|
249
|
-
? encodeColumnDefault(field.default, codecId, codecLookup)
|
|
429
|
+
? encodeColumnDefault(field.default, codecId, codecLookup, field.many === true)
|
|
250
430
|
: undefined;
|
|
251
431
|
|
|
432
|
+
// `storageValueSetRef` (derived from an `enumTypeHandle`) takes precedence
|
|
433
|
+
// when present — the established domain-enum path. `field.descriptor.valueSet`
|
|
434
|
+
// is the fallback: set by an entity-ref type constructor (e.g. `pg.enum(Ref)`)
|
|
435
|
+
// that resolved the field's type against a value-set-deriving entity with no
|
|
436
|
+
// domain enum involved. A field carries at most one of the two in practice.
|
|
437
|
+
const valueSet = storageValueSetRef ?? field.descriptor.valueSet;
|
|
438
|
+
|
|
252
439
|
return {
|
|
253
440
|
nativeType: field.descriptor.nativeType,
|
|
254
441
|
codecId,
|
|
255
442
|
nullable: field.nullable,
|
|
443
|
+
...(field.many ? { many: true as const } : {}),
|
|
256
444
|
...ifDefined('typeParams', field.descriptor.typeParams),
|
|
257
445
|
...ifDefined('default', encodedDefault),
|
|
258
446
|
...ifDefined('typeRef', field.descriptor.typeRef),
|
|
259
|
-
...ifDefined('valueSet',
|
|
447
|
+
...ifDefined('valueSet', valueSet),
|
|
260
448
|
};
|
|
261
449
|
}
|
|
262
450
|
|
|
@@ -298,9 +486,122 @@ function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): S
|
|
|
298
486
|
ids.add(model.namespaceId);
|
|
299
487
|
}
|
|
300
488
|
}
|
|
489
|
+
for (const id of Object.keys(definition.packEntities ?? {})) {
|
|
490
|
+
if (id.length > 0) {
|
|
491
|
+
ids.add(id);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
301
494
|
return ids;
|
|
302
495
|
}
|
|
303
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Entry kinds the framework assembler itself manages (`table` from models,
|
|
499
|
+
* `valueSet` from `enums` and pack-entity value-set derivation). An
|
|
500
|
+
* author-declared pack entity claiming one of these would silently clobber
|
|
501
|
+
* or be clobbered by the managed slot, so it is rejected outright.
|
|
502
|
+
*/
|
|
503
|
+
const MANAGED_ENTRY_KINDS = new Set([tableEntityKind.kind, valueSetEntityKind.kind]);
|
|
504
|
+
|
|
505
|
+
function assertNoManagedPackEntityKinds(
|
|
506
|
+
namespaceId: string,
|
|
507
|
+
packEntitiesForNs: Readonly<Record<string, unknown>> | undefined,
|
|
508
|
+
): void {
|
|
509
|
+
if (packEntitiesForNs === undefined) return;
|
|
510
|
+
for (const kind of Object.keys(packEntitiesForNs)) {
|
|
511
|
+
if (MANAGED_ENTRY_KINDS.has(kind)) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
`buildSqlContractFromDefinition: packEntities in namespace "${namespaceId}" declares entry kind "${kind}", which is managed by the framework (table/valueSet) and cannot be supplied via packEntities.`,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Walks the flat `entityTypes` namespace tree contributed by the target pack
|
|
521
|
+
* and every extension pack, indexing descriptors by their `discriminator` —
|
|
522
|
+
* the same string a pack entity's entries-map key (`entries.<kind>`) uses.
|
|
523
|
+
* Mirrors `contract-psl`'s `buildEntityTypesByDiscriminator`, recomposed here
|
|
524
|
+
* from the packs `ContractDefinition` already carries (`target` +
|
|
525
|
+
* `extensionPacks`) since the TS assembler has no single pre-merged
|
|
526
|
+
* `AuthoringContributions` input to read the way the PSL interpreter does.
|
|
527
|
+
*/
|
|
528
|
+
function collectEntityTypeDescriptorsByDiscriminator(
|
|
529
|
+
definition: ContractDefinition,
|
|
530
|
+
): ReadonlyMap<string, AuthoringEntityTypeDescriptor> {
|
|
531
|
+
const result = new Map<string, AuthoringEntityTypeDescriptor>();
|
|
532
|
+
const walk = (namespace: AuthoringEntityTypeNamespace): void => {
|
|
533
|
+
for (const value of Object.values(namespace)) {
|
|
534
|
+
if (isAuthoringEntityTypeDescriptor(value)) {
|
|
535
|
+
result.set(value.discriminator, value);
|
|
536
|
+
} else {
|
|
537
|
+
walk(value);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
const components = [definition.target, ...Object.values(definition.extensionPacks ?? {})];
|
|
542
|
+
for (const component of components) {
|
|
543
|
+
const entityTypes = component.authoring?.entityTypes;
|
|
544
|
+
if (entityTypes !== undefined) {
|
|
545
|
+
walk(entityTypes);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Derives value-sets for every pack entity declared in one namespace,
|
|
553
|
+
* reusing the same `SqlValueSetDerivingEntityTypeOutput.deriveValueSet` hook
|
|
554
|
+
* `contract-psl`'s `lowerExtensionBlocksForNamespace` folds into
|
|
555
|
+
* `entries.valueSet` on the PSL path — so a TS-attached entity (e.g. a
|
|
556
|
+
* native enum) gets its value-set the same way. Entity kinds with no
|
|
557
|
+
* registered descriptor, or whose descriptor output doesn't derive a
|
|
558
|
+
* value-set, contribute nothing.
|
|
559
|
+
*/
|
|
560
|
+
function derivePackEntityValueSets(
|
|
561
|
+
packEntitiesForNs: Readonly<Record<string, Readonly<Record<string, unknown>>>> | undefined,
|
|
562
|
+
entityTypesByDiscriminator: ReadonlyMap<string, AuthoringEntityTypeDescriptor>,
|
|
563
|
+
): Record<string, StorageValueSetInput> | undefined {
|
|
564
|
+
if (packEntitiesForNs === undefined) return undefined;
|
|
565
|
+
let result: Record<string, StorageValueSetInput> | undefined;
|
|
566
|
+
for (const [kind, entitiesByName] of Object.entries(packEntitiesForNs)) {
|
|
567
|
+
const descriptor = entityTypesByDiscriminator.get(kind);
|
|
568
|
+
if (descriptor === undefined) continue;
|
|
569
|
+
for (const [name, entity] of Object.entries(entitiesByName)) {
|
|
570
|
+
const derivedValueSet = deriveValueSetFromEntity(descriptor.output, entity);
|
|
571
|
+
if (derivedValueSet === undefined) continue;
|
|
572
|
+
result ??= {};
|
|
573
|
+
result[name] = derivedValueSet;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Merges a namespace's `enumType()`-derived value-sets with its pack-entity-
|
|
581
|
+
* derived value-sets. Both land in the same `entries.valueSet[name]` slot —
|
|
582
|
+
* which drives value-set → codec typing and the domain-enum CHECK — so a
|
|
583
|
+
* same-named entry in both would let one silently overwrite the other and
|
|
584
|
+
* corrupt whichever column resolves against it. The same collision class the
|
|
585
|
+
* `mergeCollectedPackEntities` guard rejects; the PSL path already hard-errors
|
|
586
|
+
* on the equivalent (`interpretPslDocumentToSqlContract`). Reject it here too.
|
|
587
|
+
*/
|
|
588
|
+
function mergeNamespaceValueSets(
|
|
589
|
+
namespaceId: string,
|
|
590
|
+
enumValueSets: Record<string, StorageValueSetInput> | undefined,
|
|
591
|
+
packValueSets: Record<string, StorageValueSetInput> | undefined,
|
|
592
|
+
): Record<string, StorageValueSetInput> {
|
|
593
|
+
if (enumValueSets !== undefined && packValueSets !== undefined) {
|
|
594
|
+
for (const name of Object.keys(packValueSets)) {
|
|
595
|
+
if (Object.hasOwn(enumValueSets, name)) {
|
|
596
|
+
throw new Error(
|
|
597
|
+
`buildSqlContractFromDefinition: value-set "${name}" in namespace "${namespaceId}" is derived from both an enum and a pack entity — names must be unique per namespace.`,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return { ...enumValueSets, ...packValueSets };
|
|
603
|
+
}
|
|
604
|
+
|
|
304
605
|
function ensureUnboundNamespaceSlot(
|
|
305
606
|
namespaces: SqlStorageInput['namespaces'],
|
|
306
607
|
createNamespace: ContractDefinition['createNamespace'],
|
|
@@ -308,18 +609,15 @@ function ensureUnboundNamespaceSlot(
|
|
|
308
609
|
if (Object.hasOwn(namespaces, UNBOUND_NAMESPACE_ID)) {
|
|
309
610
|
return namespaces;
|
|
310
611
|
}
|
|
311
|
-
const unboundInput:
|
|
612
|
+
const unboundInput: SqlNamespaceInput = {
|
|
312
613
|
id: UNBOUND_NAMESPACE_ID,
|
|
313
614
|
entries: { table: {} },
|
|
314
615
|
};
|
|
315
|
-
const unbound = createNamespace
|
|
316
|
-
return
|
|
317
|
-
SqlStorageInput['namespaces'],
|
|
318
|
-
'createNamespace may return a target namespace concretion; the unbound slot matches SqlNamespace at runtime'
|
|
319
|
-
>({
|
|
616
|
+
const unbound = createNamespace(unboundInput);
|
|
617
|
+
return {
|
|
320
618
|
[UNBOUND_NAMESPACE_ID]: unbound,
|
|
321
619
|
...namespaces,
|
|
322
|
-
}
|
|
620
|
+
};
|
|
323
621
|
}
|
|
324
622
|
|
|
325
623
|
export function buildSqlContractFromDefinition(
|
|
@@ -328,6 +626,7 @@ export function buildSqlContractFromDefinition(
|
|
|
328
626
|
): Contract<SqlStorage> {
|
|
329
627
|
const target = definition.target.targetId;
|
|
330
628
|
const defaultNamespaceId = definition.target.defaultNamespaceId;
|
|
629
|
+
const qualifyColumnType = resolveColumnTypeQualifier(definition.target);
|
|
331
630
|
const targetFamily = 'sql';
|
|
332
631
|
const resolveNamespaceId = (m: ModelNode): string =>
|
|
333
632
|
m.namespaceId !== undefined && m.namespaceId.length > 0 ? m.namespaceId : defaultNamespaceId;
|
|
@@ -346,6 +645,7 @@ export function buildSqlContractFromDefinition(
|
|
|
346
645
|
const modelNameToNamespaceId = new Map<string, string>();
|
|
347
646
|
const executionDefaults: ExecutionMutationDefault[] = [];
|
|
348
647
|
const modelsByNamespace: Record<string, Record<string, ContractModel>> = {};
|
|
648
|
+
const collectedPackEntities: CollectedPackEntities = {};
|
|
349
649
|
const rootEntries: Array<{
|
|
350
650
|
readonly tableName: string;
|
|
351
651
|
readonly namespaceId: string;
|
|
@@ -375,6 +675,7 @@ export function buildSqlContractFromDefinition(
|
|
|
375
675
|
const fieldToColumn: Record<string, string> = {};
|
|
376
676
|
const domainFields: Record<string, ContractField> = {};
|
|
377
677
|
const domainFieldRefs: Record<string, DomainFieldRef> = {};
|
|
678
|
+
const checksForTable: CheckConstraintInput[] = [];
|
|
378
679
|
|
|
379
680
|
for (const field of semanticModel.fields) {
|
|
380
681
|
const executionDefaultPhases =
|
|
@@ -417,10 +718,58 @@ export function buildSqlContractFromDefinition(
|
|
|
417
718
|
}
|
|
418
719
|
: undefined;
|
|
419
720
|
|
|
420
|
-
|
|
721
|
+
// A field authored through a deferred entity-ref column helper (e.g.
|
|
722
|
+
// `pg.enum(handle)`) carries `descriptor.entityRef`: the referenced
|
|
723
|
+
// entity is collected into `collectedPackEntities` (folded into the
|
|
724
|
+
// same `entries.<kind>` + `entries.valueSet` assembly an author-declared
|
|
725
|
+
// `packEntities` entry goes through) and the descriptor is resolved
|
|
726
|
+
// against this field's now-known `namespaceId` — the builder call that
|
|
727
|
+
// produced it ran before the enclosing model associated one. The
|
|
728
|
+
// descriptor is then handed to the target's `qualifyColumnType` hook,
|
|
729
|
+
// which schema-qualifies a native-enum column's type name for its
|
|
730
|
+
// namespace. Keying off the codec id (inside the hook) catches both the
|
|
731
|
+
// TS `pg.enum(handle)` path (via `entityRef`) and the PSL `pg.enum(Ref)`
|
|
732
|
+
// path (resolved inline in the interpreter, no `entityRef`). Because the
|
|
733
|
+
// storage column is built from this qualified descriptor and the domain
|
|
734
|
+
// field derives its `type.typeParams` from that column, both come out
|
|
735
|
+
// qualified in this single pass.
|
|
736
|
+
let resolvedField: FieldNode | ValueObjectFieldNode = field;
|
|
737
|
+
if (!isValueObjectField(field)) {
|
|
738
|
+
let descriptor = field.descriptor;
|
|
739
|
+
const entityRef = descriptor.entityRef;
|
|
740
|
+
if (entityRef !== undefined) {
|
|
741
|
+
collectPackEntityFromColumn(collectedPackEntities, namespaceId, entityRef);
|
|
742
|
+
descriptor = resolveEntityRefDescriptor(descriptor, namespaceId);
|
|
743
|
+
}
|
|
744
|
+
descriptor = qualifyColumnDescriptor(descriptor, namespaceId, qualifyColumnType);
|
|
745
|
+
if (descriptor !== field.descriptor) {
|
|
746
|
+
resolvedField = { ...field, descriptor };
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const column = buildStorageColumn(resolvedField, storageValueSetRef, codecLookup);
|
|
421
751
|
columns[field.columnName] = column;
|
|
422
752
|
fieldToColumn[field.fieldName] = field.columnName;
|
|
423
753
|
|
|
754
|
+
// A domain enum (`storageValueSetRef`, from an `enumType()` handle) is
|
|
755
|
+
// stored as a plain scalar column (`text`, `int4`, …) with no native
|
|
756
|
+
// type of its own to enforce membership, so it needs an explicit
|
|
757
|
+
// CHECK — scalar or array, since a `text[]` array has no element-level
|
|
758
|
+
// enforcement either. A value set resolved by an entity-ref type
|
|
759
|
+
// constructor (`field.descriptor.valueSet`, e.g. `pg.enum(Ref)`) binds
|
|
760
|
+
// the column to a codec/native-type pairing that IS the storage-level
|
|
761
|
+
// enforcement (a Postgres native enum type, or another target's
|
|
762
|
+
// equivalent) — including array columns, since the target enforces
|
|
763
|
+
// membership on every element of a native-typed array — so no CHECK
|
|
764
|
+
// for those.
|
|
765
|
+
if (column.valueSet !== undefined && storageValueSetRef !== undefined) {
|
|
766
|
+
checksForTable.push({
|
|
767
|
+
name: `${tableName}_${field.columnName}_check`,
|
|
768
|
+
column: field.columnName,
|
|
769
|
+
valueSet: column.valueSet,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
424
773
|
domainFields[field.fieldName] = buildDomainField(field, column, domainValueSetRef);
|
|
425
774
|
|
|
426
775
|
if (isValueObjectField(field)) {
|
|
@@ -511,15 +860,6 @@ export function buildSqlContractFromDefinition(
|
|
|
511
860
|
// materialised onto the base `ModelNode`, so the variant builds a domain
|
|
512
861
|
// model (below) but no storage table of its own.
|
|
513
862
|
if (!semanticModel.sharesBaseTable) {
|
|
514
|
-
const checksForTable: CheckConstraintInput[] = Object.entries(columns).flatMap(
|
|
515
|
-
([columnName, col]) => {
|
|
516
|
-
const valueSet = col.valueSet;
|
|
517
|
-
return valueSet === undefined
|
|
518
|
-
? []
|
|
519
|
-
: [{ name: `${tableName}_${columnName}_check`, column: columnName, valueSet }];
|
|
520
|
-
},
|
|
521
|
-
);
|
|
522
|
-
|
|
523
863
|
const tableInput: StorageTableInput = {
|
|
524
864
|
columns,
|
|
525
865
|
...ifDefined('control', semanticModel.control),
|
|
@@ -729,25 +1069,38 @@ export function buildSqlContractFromDefinition(
|
|
|
729
1069
|
}
|
|
730
1070
|
|
|
731
1071
|
const { createNamespace } = definition;
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1072
|
+
const entityTypesByDiscriminator = collectEntityTypeDescriptorsByDiscriminator(definition);
|
|
1073
|
+
const namespaces: SqlStorageInput['namespaces'] = Object.fromEntries(
|
|
1074
|
+
[...namespaceCoordinateIds].sort().map((id) => {
|
|
1075
|
+
const packEntitiesForNs = mergeCollectedPackEntities(
|
|
1076
|
+
id,
|
|
1077
|
+
definition.packEntities?.[id],
|
|
1078
|
+
collectedPackEntities[id],
|
|
1079
|
+
);
|
|
1080
|
+
assertNoManagedPackEntityKinds(id, packEntitiesForNs);
|
|
1081
|
+
|
|
1082
|
+
const enumValueSetEntries = storageValueSetsByNs[id];
|
|
1083
|
+
const packValueSetEntries = derivePackEntityValueSets(
|
|
1084
|
+
packEntitiesForNs,
|
|
1085
|
+
entityTypesByDiscriminator,
|
|
1086
|
+
);
|
|
1087
|
+
const valueSetEntries =
|
|
1088
|
+
enumValueSetEntries !== undefined || packValueSetEntries !== undefined
|
|
1089
|
+
? mergeNamespaceValueSets(id, enumValueSetEntries, packValueSetEntries)
|
|
1090
|
+
: undefined;
|
|
1091
|
+
|
|
1092
|
+
const nsInput: SqlNamespaceInput = {
|
|
1093
|
+
id,
|
|
1094
|
+
entries: {
|
|
1095
|
+
table: tablesByNamespace[id] ?? {},
|
|
1096
|
+
...packEntitiesForNs,
|
|
1097
|
+
...(valueSetEntries !== undefined && Object.keys(valueSetEntries).length > 0
|
|
1098
|
+
? { valueSet: valueSetEntries }
|
|
1099
|
+
: {}),
|
|
1100
|
+
},
|
|
1101
|
+
};
|
|
1102
|
+
return [id, createNamespace(nsInput)];
|
|
1103
|
+
}),
|
|
751
1104
|
);
|
|
752
1105
|
const storageWithoutHash = {
|
|
753
1106
|
...(Object.keys(documentTypes).length > 0 ? { types: documentTypes } : {}),
|
|
@@ -13,9 +13,6 @@ import type {
|
|
|
13
13
|
} from '@prisma-next/framework-components/authoring';
|
|
14
14
|
import {
|
|
15
15
|
assertNoCrossRegistryCollisions,
|
|
16
|
-
isAuthoringEntityTypeDescriptor,
|
|
17
|
-
isAuthoringFieldPresetDescriptor,
|
|
18
|
-
isAuthoringTypeConstructorDescriptor,
|
|
19
16
|
mergeAuthoringNamespaces,
|
|
20
17
|
} from '@prisma-next/framework-components/authoring';
|
|
21
18
|
import type {
|
|
@@ -183,7 +180,7 @@ function composeTypeNamespace(components: readonly AuthoringComponent[]): Author
|
|
|
183
180
|
for (const component of components) {
|
|
184
181
|
const ns = extractTypeNamespace(component);
|
|
185
182
|
if (Object.keys(ns).length > 0) {
|
|
186
|
-
mergeAuthoringNamespaces(merged, ns, [],
|
|
183
|
+
mergeAuthoringNamespaces(merged, ns, [], 'typeConstructor', 'type');
|
|
187
184
|
}
|
|
188
185
|
}
|
|
189
186
|
return merged as AuthoringTypeNamespace;
|
|
@@ -194,7 +191,7 @@ function composeFieldNamespace(components: readonly AuthoringComponent[]): Autho
|
|
|
194
191
|
for (const component of components) {
|
|
195
192
|
const ns = extractFieldNamespace(component);
|
|
196
193
|
if (Object.keys(ns).length > 0) {
|
|
197
|
-
mergeAuthoringNamespaces(merged, ns, [],
|
|
194
|
+
mergeAuthoringNamespaces(merged, ns, [], 'fieldPreset', 'field');
|
|
198
195
|
}
|
|
199
196
|
}
|
|
200
197
|
return merged as AuthoringFieldNamespace;
|
|
@@ -207,7 +204,7 @@ function composeEntityNamespace(
|
|
|
207
204
|
for (const component of components) {
|
|
208
205
|
const ns = extractEntitiesNamespace(component);
|
|
209
206
|
if (Object.keys(ns).length > 0) {
|
|
210
|
-
mergeAuthoringNamespaces(merged, ns, [],
|
|
207
|
+
mergeAuthoringNamespaces(merged, ns, [], 'entity', 'entity');
|
|
211
208
|
}
|
|
212
209
|
}
|
|
213
210
|
return merged as AuthoringEntityTypeNamespace;
|