@prisma-next/sql-contract-ts 0.14.0-dev.50 → 0.14.0-dev.52
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-Bi8UaU_S.mjs → build-contract-CSyrbCoJ.mjs} +212 -3
- package/dist/build-contract-CSyrbCoJ.mjs.map +1 -0
- package/dist/config-types.mjs +1 -1
- package/dist/contract-builder.d.mts +198 -164
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +29 -3
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +10 -10
- package/src/build-contract.ts +341 -3
- package/src/contract-builder.ts +63 -0
- package/src/contract-definition.ts +23 -0
- package/src/contract-dsl.ts +9 -0
- package/src/contract-lowering.ts +3 -0
- package/dist/build-contract-Bi8UaU_S.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.52",
|
|
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.52",
|
|
10
|
+
"@prisma-next/contract": "0.14.0-dev.52",
|
|
11
|
+
"@prisma-next/contract-authoring": "0.14.0-dev.52",
|
|
12
|
+
"@prisma-next/framework-components": "0.14.0-dev.52",
|
|
13
|
+
"@prisma-next/sql-contract": "0.14.0-dev.52",
|
|
14
|
+
"@prisma-next/utils": "0.14.0-dev.52",
|
|
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.52",
|
|
21
|
+
"@prisma-next/tsconfig": "0.14.0-dev.52",
|
|
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.52",
|
|
25
25
|
"tsdown": "0.22.1",
|
|
26
26
|
"typescript": "5.9.3",
|
|
27
27
|
"vitest": "4.1.8"
|
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,
|
|
@@ -44,6 +51,7 @@ import {
|
|
|
44
51
|
toStorageTypeInstance,
|
|
45
52
|
} from '@prisma-next/sql-contract/types';
|
|
46
53
|
import { validateStorageSemantics } from '@prisma-next/sql-contract/validators';
|
|
54
|
+
import { deriveValueSetFromEntity } from '@prisma-next/sql-contract/value-set-derivation-hook';
|
|
47
55
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
48
56
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
49
57
|
import type {
|
|
@@ -172,6 +180,174 @@ function isValueObjectField(
|
|
|
172
180
|
return 'valueObjectName' in field;
|
|
173
181
|
}
|
|
174
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
|
+
|
|
175
351
|
const JSONB_CODEC_ID = 'pg/jsonb@1';
|
|
176
352
|
const JSONB_NATIVE_TYPE = 'jsonb';
|
|
177
353
|
|
|
@@ -310,9 +486,122 @@ function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): S
|
|
|
310
486
|
ids.add(model.namespaceId);
|
|
311
487
|
}
|
|
312
488
|
}
|
|
489
|
+
for (const id of Object.keys(definition.packEntities ?? {})) {
|
|
490
|
+
if (id.length > 0) {
|
|
491
|
+
ids.add(id);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
313
494
|
return ids;
|
|
314
495
|
}
|
|
315
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
|
+
|
|
316
605
|
function ensureUnboundNamespaceSlot(
|
|
317
606
|
namespaces: SqlStorageInput['namespaces'],
|
|
318
607
|
createNamespace: ContractDefinition['createNamespace'],
|
|
@@ -337,6 +626,7 @@ export function buildSqlContractFromDefinition(
|
|
|
337
626
|
): Contract<SqlStorage> {
|
|
338
627
|
const target = definition.target.targetId;
|
|
339
628
|
const defaultNamespaceId = definition.target.defaultNamespaceId;
|
|
629
|
+
const qualifyColumnType = resolveColumnTypeQualifier(definition.target);
|
|
340
630
|
const targetFamily = 'sql';
|
|
341
631
|
const resolveNamespaceId = (m: ModelNode): string =>
|
|
342
632
|
m.namespaceId !== undefined && m.namespaceId.length > 0 ? m.namespaceId : defaultNamespaceId;
|
|
@@ -355,6 +645,7 @@ export function buildSqlContractFromDefinition(
|
|
|
355
645
|
const modelNameToNamespaceId = new Map<string, string>();
|
|
356
646
|
const executionDefaults: ExecutionMutationDefault[] = [];
|
|
357
647
|
const modelsByNamespace: Record<string, Record<string, ContractModel>> = {};
|
|
648
|
+
const collectedPackEntities: CollectedPackEntities = {};
|
|
358
649
|
const rootEntries: Array<{
|
|
359
650
|
readonly tableName: string;
|
|
360
651
|
readonly namespaceId: string;
|
|
@@ -427,7 +718,36 @@ export function buildSqlContractFromDefinition(
|
|
|
427
718
|
}
|
|
428
719
|
: undefined;
|
|
429
720
|
|
|
430
|
-
|
|
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);
|
|
431
751
|
columns[field.columnName] = column;
|
|
432
752
|
fieldToColumn[field.fieldName] = field.columnName;
|
|
433
753
|
|
|
@@ -749,13 +1069,31 @@ export function buildSqlContractFromDefinition(
|
|
|
749
1069
|
}
|
|
750
1070
|
|
|
751
1071
|
const { createNamespace } = definition;
|
|
1072
|
+
const entityTypesByDiscriminator = collectEntityTypeDescriptorsByDiscriminator(definition);
|
|
752
1073
|
const namespaces: SqlStorageInput['namespaces'] = Object.fromEntries(
|
|
753
1074
|
[...namespaceCoordinateIds].sort().map((id) => {
|
|
754
|
-
const
|
|
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
|
+
|
|
755
1092
|
const nsInput: SqlNamespaceInput = {
|
|
756
1093
|
id,
|
|
757
1094
|
entries: {
|
|
758
1095
|
table: tablesByNamespace[id] ?? {},
|
|
1096
|
+
...packEntitiesForNs,
|
|
759
1097
|
...(valueSetEntries !== undefined && Object.keys(valueSetEntries).length > 0
|
|
760
1098
|
? { valueSet: valueSetEntries }
|
|
761
1099
|
: {}),
|
package/src/contract-builder.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type ComposedAuthoringHelpers,
|
|
19
19
|
createComposedAuthoringHelpers,
|
|
20
20
|
} from './composed-authoring-helpers';
|
|
21
|
+
import type { PackEntitiesInput } from './contract-definition';
|
|
21
22
|
import {
|
|
22
23
|
type ContractInput,
|
|
23
24
|
type ContractModelBuilder,
|
|
@@ -76,6 +77,7 @@ type ContractDefinition<
|
|
|
76
77
|
readonly models?: Models;
|
|
77
78
|
readonly codecLookup?: CodecLookup;
|
|
78
79
|
readonly enums?: Enums;
|
|
80
|
+
readonly packEntities?: PackEntitiesInput;
|
|
79
81
|
};
|
|
80
82
|
|
|
81
83
|
type ContractScaffold<
|
|
@@ -101,6 +103,7 @@ type ContractScaffold<
|
|
|
101
103
|
readonly models?: never;
|
|
102
104
|
readonly codecLookup?: CodecLookup;
|
|
103
105
|
readonly enums?: Enums;
|
|
106
|
+
readonly packEntities?: PackEntitiesInput;
|
|
104
107
|
};
|
|
105
108
|
|
|
106
109
|
type ContractFactory<
|
|
@@ -114,6 +117,7 @@ type ContractFactory<
|
|
|
114
117
|
readonly types?: Types;
|
|
115
118
|
readonly models?: Models;
|
|
116
119
|
readonly enums?: Enums;
|
|
120
|
+
readonly packEntities?: PackEntitiesInput;
|
|
117
121
|
};
|
|
118
122
|
|
|
119
123
|
function validateTargetPackRef(
|
|
@@ -322,6 +326,7 @@ type BoundDefinitionInput<
|
|
|
322
326
|
readonly models?: Models;
|
|
323
327
|
readonly codecLookup?: CodecLookup;
|
|
324
328
|
readonly enums?: Record<string, EnumTypeHandle>;
|
|
329
|
+
readonly packEntities?: PackEntitiesInput;
|
|
325
330
|
};
|
|
326
331
|
|
|
327
332
|
// A bare `Record<string, EnumTypeHandle>` (no literal keys) is the widened
|
|
@@ -345,6 +350,60 @@ type WithFamilyTarget<
|
|
|
345
350
|
T extends TargetPackRef<'sql', string>,
|
|
346
351
|
> = Input & { readonly family: F; readonly target: T };
|
|
347
352
|
|
|
353
|
+
// Deep-merges packEntities authored on the scaffold definition with those
|
|
354
|
+
// returned from the factory callback — three levels deep (namespace, kind,
|
|
355
|
+
// name), unlike `enums`' flat shallow merge, since a factory-declared
|
|
356
|
+
// namespace/kind must not silently drop a scaffold-declared sibling entry
|
|
357
|
+
// under the same namespace or kind. A same-name key present on both sides is
|
|
358
|
+
// only merged when it is the identical entity instance; two *different*
|
|
359
|
+
// entities of the same namespace/kind/name is a collision (the emitted
|
|
360
|
+
// entry could reflect only one), rejected here the same way
|
|
361
|
+
// `mergeCollectedPackEntities` in `build-contract.ts` rejects it.
|
|
362
|
+
function mergePackEntityNames(
|
|
363
|
+
namespaceId: string,
|
|
364
|
+
kind: string,
|
|
365
|
+
a: Readonly<Record<string, unknown>> | undefined,
|
|
366
|
+
b: Readonly<Record<string, unknown>> | undefined,
|
|
367
|
+
): Readonly<Record<string, unknown>> {
|
|
368
|
+
if (a !== undefined && b !== undefined) {
|
|
369
|
+
for (const [name, entity] of Object.entries(b)) {
|
|
370
|
+
const existing = a[name];
|
|
371
|
+
if (existing !== undefined && existing !== entity) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`defineContract: two different "${kind}" entities named "${name}" in namespace "${namespaceId}" — a factory-returned pack entity conflicts with a scaffold-declared one; pack-entity names must be unique per namespace.`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return { ...(a ?? {}), ...(b ?? {}) };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function mergePackEntityKinds(
|
|
382
|
+
namespaceId: string,
|
|
383
|
+
a: Readonly<Record<string, Readonly<Record<string, unknown>>>> | undefined,
|
|
384
|
+
b: Readonly<Record<string, Readonly<Record<string, unknown>>>> | undefined,
|
|
385
|
+
): Readonly<Record<string, Readonly<Record<string, unknown>>>> {
|
|
386
|
+
const kinds = new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]);
|
|
387
|
+
const result: Record<string, Readonly<Record<string, unknown>>> = {};
|
|
388
|
+
for (const kind of kinds) {
|
|
389
|
+
result[kind] = mergePackEntityNames(namespaceId, kind, a?.[kind], b?.[kind]);
|
|
390
|
+
}
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function mergePackEntities(
|
|
395
|
+
a: PackEntitiesInput | undefined,
|
|
396
|
+
b: PackEntitiesInput | undefined,
|
|
397
|
+
): PackEntitiesInput | undefined {
|
|
398
|
+
if (a === undefined && b === undefined) return undefined;
|
|
399
|
+
const namespaces = new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})]);
|
|
400
|
+
const result: Record<string, Readonly<Record<string, Readonly<Record<string, unknown>>>>> = {};
|
|
401
|
+
for (const namespaceId of namespaces) {
|
|
402
|
+
result[namespaceId] = mergePackEntityKinds(namespaceId, a?.[namespaceId], b?.[namespaceId]);
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
|
|
348
407
|
/**
|
|
349
408
|
* Shared builder that assembles a SqlContract with pre-bound family and target.
|
|
350
409
|
* Extension wrappers keep their own public overloads and delegate their impl body here;
|
|
@@ -390,6 +449,7 @@ export function buildBoundContract<
|
|
|
390
449
|
readonly types?: Record<string, StorageTypeInstance>;
|
|
391
450
|
readonly models?: Record<string, ModelLike>;
|
|
392
451
|
readonly enums?: Record<string, EnumTypeHandle>;
|
|
452
|
+
readonly packEntities?: PackEntitiesInput;
|
|
393
453
|
},
|
|
394
454
|
>(
|
|
395
455
|
family: F,
|
|
@@ -415,6 +475,7 @@ export function buildBoundContract(
|
|
|
415
475
|
readonly types?: Record<string, StorageTypeInstance>;
|
|
416
476
|
readonly models?: Record<string, ModelLike>;
|
|
417
477
|
readonly enums?: Record<string, EnumTypeHandle>;
|
|
478
|
+
readonly packEntities?: PackEntitiesInput;
|
|
418
479
|
})
|
|
419
480
|
| undefined,
|
|
420
481
|
) {
|
|
@@ -429,11 +490,13 @@ export function buildBoundContract(
|
|
|
429
490
|
}),
|
|
430
491
|
);
|
|
431
492
|
const mergedEnums = { ...(definition.enums ?? {}), ...built.enums };
|
|
493
|
+
const mergedPackEntities = mergePackEntities(definition.packEntities, built.packEntities);
|
|
432
494
|
return buildContractFromDsl({
|
|
433
495
|
...full,
|
|
434
496
|
...ifDefined('types', built.types),
|
|
435
497
|
...ifDefined('models', built.models),
|
|
436
498
|
...ifDefined('enums', Object.keys(mergedEnums).length > 0 ? mergedEnums : undefined),
|
|
499
|
+
...ifDefined('packEntities', mergedPackEntities),
|
|
437
500
|
});
|
|
438
501
|
}
|
|
439
502
|
|
|
@@ -16,6 +16,20 @@ import type { EnumTypeHandle } from './enum-type';
|
|
|
16
16
|
|
|
17
17
|
export type { ExecutionMutationDefaultPhases };
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* An author-declared, namespace-scoped pack entity attachment: namespace id →
|
|
21
|
+
* entity kind (the discriminator the target/extension pack registered its
|
|
22
|
+
* `AuthoringContributions.entityTypes` descriptor under, e.g. `native_enum`)
|
|
23
|
+
* → entity name → the lowered entity instance. Generic on purpose — neither
|
|
24
|
+
* the framework nor `contract-ts` names a specific entity kind here; the
|
|
25
|
+
* shape mirrors `SqlNamespaceInput.entries` (`entries.<kind>[name]`), just
|
|
26
|
+
* namespace-nested so an author can target any declared namespace (default
|
|
27
|
+
* or named), not only the contract's default namespace.
|
|
28
|
+
*/
|
|
29
|
+
export type PackEntitiesInput = Readonly<
|
|
30
|
+
Record<string, Readonly<Record<string, Readonly<Record<string, unknown>>>>>
|
|
31
|
+
>;
|
|
32
|
+
|
|
19
33
|
export interface FieldNode {
|
|
20
34
|
readonly fieldName: string;
|
|
21
35
|
readonly columnName: string;
|
|
@@ -185,4 +199,13 @@ export interface ContractDefinition {
|
|
|
185
199
|
* default namespace.
|
|
186
200
|
*/
|
|
187
201
|
readonly enums?: Record<string, EnumTypeHandle>;
|
|
202
|
+
/**
|
|
203
|
+
* Author-declared pack entities, keyed by namespace then entity kind then
|
|
204
|
+
* name. Each entity lowers into `storage.namespaces[ns].entries.<kind>`;
|
|
205
|
+
* when the registered entity-type descriptor's factory output implements
|
|
206
|
+
* the `SqlValueSetDerivingEntityTypeOutput.deriveValueSet` hook, the
|
|
207
|
+
* derived value-set also folds into `entries.valueSet`, mirroring how
|
|
208
|
+
* `enums` flows there.
|
|
209
|
+
*/
|
|
210
|
+
readonly packEntities?: PackEntitiesInput;
|
|
188
211
|
}
|
package/src/contract-dsl.ts
CHANGED
|
@@ -1583,6 +1583,15 @@ export type ContractInput<
|
|
|
1583
1583
|
* default namespace. Fields reference the enum via `field.namedType(handle)`.
|
|
1584
1584
|
*/
|
|
1585
1585
|
readonly enums?: Record<string, import('./enum-type').EnumTypeHandle>;
|
|
1586
|
+
/**
|
|
1587
|
+
* Author-declared pack entities, keyed by namespace id then entity kind
|
|
1588
|
+
* then name — e.g. `{ auth: { native_enum: { AalLevel: <entity> } } }`.
|
|
1589
|
+
* Each entity lowers into `storage.namespaces[ns].entries.<kind>`; when its
|
|
1590
|
+
* registered entity-type descriptor derives a value-set, that also folds
|
|
1591
|
+
* into `entries.valueSet`, mirroring how `enums` flows there. Generic on
|
|
1592
|
+
* purpose — neither this type nor the assembler names a specific kind.
|
|
1593
|
+
*/
|
|
1594
|
+
readonly packEntities?: import('./contract-definition').PackEntitiesInput;
|
|
1586
1595
|
};
|
|
1587
1596
|
|
|
1588
1597
|
export function model<
|
package/src/contract-lowering.ts
CHANGED
|
@@ -882,6 +882,9 @@ export function buildContractDefinition(definition: ContractInput): ContractDefi
|
|
|
882
882
|
...(definition.enums && Object.keys(definition.enums).length > 0
|
|
883
883
|
? { enums: definition.enums }
|
|
884
884
|
: {}),
|
|
885
|
+
...(definition.packEntities && Object.keys(definition.packEntities).length > 0
|
|
886
|
+
? { packEntities: definition.packEntities }
|
|
887
|
+
: {}),
|
|
885
888
|
models,
|
|
886
889
|
};
|
|
887
890
|
}
|