@prisma-next/sql-contract-ts 0.12.0-dev.9 → 0.13.0-dev.1
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/README.md +8 -0
- package/dist/{build-contract-BCYW3_wE.mjs → build-contract-C-x2pfu4.mjs} +214 -76
- package/dist/build-contract-C-x2pfu4.mjs.map +1 -0
- package/dist/config-types.d.mts +7 -3
- package/dist/config-types.d.mts.map +1 -1
- package/dist/config-types.mjs +12 -9
- package/dist/config-types.mjs.map +1 -1
- package/dist/contract-builder.d.mts +319 -23
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +280 -44
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +13 -13
- package/schemas/data-contract-sql-v1.json +31 -0
- package/src/build-contract.ts +340 -95
- package/src/config-types.ts +24 -6
- package/src/contract-builder.ts +137 -16
- package/src/contract-definition.ts +57 -3
- package/src/contract-dsl.ts +346 -18
- package/src/contract-lowering.ts +188 -15
- package/src/contract-types.ts +18 -21
- package/src/enum-type.ts +236 -0
- package/src/exports/contract-builder.ts +5 -0
- package/dist/build-contract-BCYW3_wE.mjs.map +0 -1
package/src/contract-lowering.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ColumnTypeDescriptor } from '@prisma-next/framework-components/codec';
|
|
2
|
+
import type { ExtensionPackRef } from '@prisma-next/framework-components/components';
|
|
2
3
|
import {
|
|
3
4
|
isPostgresEnumStorageEntry,
|
|
4
5
|
type PostgresEnumStorageEntry,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
emitTypedCrossModelFallbackWarnings,
|
|
36
37
|
emitTypedNamedTypeFallbackWarnings,
|
|
37
38
|
} from './contract-warnings';
|
|
39
|
+
import { isEnumTypeHandle } from './enum-type';
|
|
38
40
|
|
|
39
41
|
type RuntimeModel = ContractModelBuilder<
|
|
40
42
|
string | undefined,
|
|
@@ -84,6 +86,13 @@ function resolveFieldDescriptor(
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
if ('typeRef' in fieldState && fieldState.typeRef) {
|
|
89
|
+
if (isEnumTypeHandle(fieldState.typeRef)) {
|
|
90
|
+
return {
|
|
91
|
+
codecId: fieldState.typeRef.codecId,
|
|
92
|
+
nativeType: fieldState.typeRef.nativeType,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
const typeRef =
|
|
88
97
|
typeof fieldState.typeRef === 'string'
|
|
89
98
|
? fieldState.typeRef
|
|
@@ -253,6 +262,41 @@ function resolveRelationForeignKeys(
|
|
|
253
262
|
}
|
|
254
263
|
|
|
255
264
|
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
265
|
+
|
|
266
|
+
// F-relfk: cross-space relations carry a spaceId; skip the local spec lookup
|
|
267
|
+
// and include cross-space coordinates so resolveForeignKeyNodes routes the FK
|
|
268
|
+
// through the cross-space path.
|
|
269
|
+
if (relation.spaceId !== undefined) {
|
|
270
|
+
const fields = normalizeRelationFieldNames(relation.from);
|
|
271
|
+
const targetFields = normalizeRelationFieldNames(relation.to);
|
|
272
|
+
assertRelationFieldArity({
|
|
273
|
+
modelName: spec.modelName,
|
|
274
|
+
relationName,
|
|
275
|
+
leftLabel: 'source',
|
|
276
|
+
leftFields: fields,
|
|
277
|
+
rightLabel: 'target',
|
|
278
|
+
rightFields: targetFields,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
foreignKeys.push({
|
|
282
|
+
kind: 'fk',
|
|
283
|
+
fields,
|
|
284
|
+
targetModel: targetModelName,
|
|
285
|
+
targetFields,
|
|
286
|
+
targetSpaceId: relation.spaceId,
|
|
287
|
+
...(relation.namespaceId !== undefined ? { targetNamespaceId: relation.namespaceId } : {}),
|
|
288
|
+
...(relation.tableName !== undefined ? { targetTableName: relation.tableName } : {}),
|
|
289
|
+
...(relation.sql.fk.name ? { name: relation.sql.fk.name } : {}),
|
|
290
|
+
...(relation.sql.fk.onDelete ? { onDelete: relation.sql.fk.onDelete } : {}),
|
|
291
|
+
...(relation.sql.fk.onUpdate ? { onUpdate: relation.sql.fk.onUpdate } : {}),
|
|
292
|
+
...(relation.sql.fk.constraint !== undefined
|
|
293
|
+
? { constraint: relation.sql.fk.constraint }
|
|
294
|
+
: {}),
|
|
295
|
+
...(relation.sql.fk.index !== undefined ? { index: relation.sql.fk.index } : {}),
|
|
296
|
+
});
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
256
300
|
if (!allSpecs.has(targetModelName)) {
|
|
257
301
|
throw new Error(
|
|
258
302
|
`Relation "${spec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
|
|
@@ -308,17 +352,12 @@ function lowerBelongsToRelation(
|
|
|
308
352
|
relation: Extract<RelationState, { kind: 'belongsTo' }>,
|
|
309
353
|
currentSpec: RuntimeModelSpec,
|
|
310
354
|
allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
|
|
355
|
+
extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
311
356
|
): RelationNode {
|
|
312
357
|
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
313
|
-
const targetSpec = allSpecs.get(targetModelName);
|
|
314
|
-
if (!targetSpec) {
|
|
315
|
-
throw new Error(
|
|
316
|
-
`Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
358
|
const fromFields = normalizeRelationFieldNames(relation.from);
|
|
321
359
|
const toFields = normalizeRelationFieldNames(relation.to);
|
|
360
|
+
|
|
322
361
|
assertRelationFieldArity({
|
|
323
362
|
modelName: currentSpec.modelName,
|
|
324
363
|
relationName,
|
|
@@ -328,6 +367,48 @@ function lowerBelongsToRelation(
|
|
|
328
367
|
rightFields: toFields,
|
|
329
368
|
});
|
|
330
369
|
|
|
370
|
+
// Cross-space path: the target lives in a different contract space.
|
|
371
|
+
// Resolve from the brand carried on the BelongsToRelation instead of
|
|
372
|
+
// requiring a local model spec — matching how the FK lowering works.
|
|
373
|
+
if (relation.spaceId !== undefined) {
|
|
374
|
+
assertKnownExtensionPack(
|
|
375
|
+
extensionPacks,
|
|
376
|
+
relation.spaceId,
|
|
377
|
+
`Relation "${currentSpec.modelName}.${relationName}"`,
|
|
378
|
+
);
|
|
379
|
+
const targetTable = relation.tableName ?? targetModelName.toLowerCase();
|
|
380
|
+
const parentColumns = mapFieldNamesToColumnNames(
|
|
381
|
+
currentSpec.modelName,
|
|
382
|
+
fromFields,
|
|
383
|
+
currentSpec.fieldToColumn,
|
|
384
|
+
);
|
|
385
|
+
// For cross-space relations, the `to` field names map directly to column
|
|
386
|
+
// names because we have no fieldToColumn map for the remote model.
|
|
387
|
+
// (The brand carries the table name; field→column resolution on the remote
|
|
388
|
+
// side is deferred to the planner which has access to the remote contract.)
|
|
389
|
+
return {
|
|
390
|
+
fieldName: relationName,
|
|
391
|
+
toModel: targetModelName,
|
|
392
|
+
toTable: targetTable,
|
|
393
|
+
cardinality: 'N:1',
|
|
394
|
+
spaceId: relation.spaceId,
|
|
395
|
+
...(relation.namespaceId !== undefined ? { namespaceId: relation.namespaceId } : {}),
|
|
396
|
+
on: {
|
|
397
|
+
parentTable: currentSpec.tableName,
|
|
398
|
+
parentColumns,
|
|
399
|
+
childTable: targetTable,
|
|
400
|
+
childColumns: toFields,
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const targetSpec = allSpecs.get(targetModelName);
|
|
406
|
+
if (!targetSpec) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
331
412
|
return {
|
|
332
413
|
fieldName: relationName,
|
|
333
414
|
toModel: targetModelName,
|
|
@@ -472,9 +553,10 @@ function resolveRelationNode(
|
|
|
472
553
|
relation: RelationState,
|
|
473
554
|
currentSpec: RuntimeModelSpec,
|
|
474
555
|
allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
|
|
556
|
+
extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
475
557
|
): RelationNode {
|
|
476
558
|
if (relation.kind === 'belongsTo') {
|
|
477
|
-
return lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs);
|
|
559
|
+
return lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs, extensionPacks);
|
|
478
560
|
}
|
|
479
561
|
|
|
480
562
|
if (relation.kind === 'hasMany' || relation.kind === 'hasOne') {
|
|
@@ -484,7 +566,7 @@ function resolveRelationNode(
|
|
|
484
566
|
return lowerManyToManyRelation(relationName, relation, currentSpec, allSpecs);
|
|
485
567
|
}
|
|
486
568
|
|
|
487
|
-
function
|
|
569
|
+
function lowerLocalForeignKeyNode(
|
|
488
570
|
spec: RuntimeModelSpec,
|
|
489
571
|
targetSpec: RuntimeModelSpec,
|
|
490
572
|
foreignKey: {
|
|
@@ -516,11 +598,74 @@ function lowerForeignKeyNode(
|
|
|
516
598
|
};
|
|
517
599
|
}
|
|
518
600
|
|
|
601
|
+
function lowerCrossSpaceForeignKeyNode(
|
|
602
|
+
spec: RuntimeModelSpec,
|
|
603
|
+
foreignKey: {
|
|
604
|
+
readonly fields: readonly string[];
|
|
605
|
+
readonly targetFields: readonly string[];
|
|
606
|
+
readonly targetModel: string;
|
|
607
|
+
readonly targetSpaceId: string;
|
|
608
|
+
readonly targetNamespaceId?: string;
|
|
609
|
+
readonly targetTableName?: string;
|
|
610
|
+
readonly name?: string | undefined;
|
|
611
|
+
readonly onDelete?: ForeignKeyConstraint['onDelete'] | undefined;
|
|
612
|
+
readonly onUpdate?: ForeignKeyConstraint['onUpdate'] | undefined;
|
|
613
|
+
readonly constraint?: boolean | undefined;
|
|
614
|
+
readonly index?: boolean | undefined;
|
|
615
|
+
},
|
|
616
|
+
): ForeignKeyNode {
|
|
617
|
+
return {
|
|
618
|
+
columns: mapFieldNamesToColumnNames(spec.modelName, foreignKey.fields, spec.fieldToColumn),
|
|
619
|
+
references: {
|
|
620
|
+
model: foreignKey.targetModel,
|
|
621
|
+
table: foreignKey.targetTableName ?? foreignKey.targetModel.toLowerCase(),
|
|
622
|
+
columns: foreignKey.targetFields,
|
|
623
|
+
...(foreignKey.targetNamespaceId !== undefined
|
|
624
|
+
? { namespaceId: foreignKey.targetNamespaceId }
|
|
625
|
+
: {}),
|
|
626
|
+
spaceId: foreignKey.targetSpaceId,
|
|
627
|
+
},
|
|
628
|
+
...(foreignKey.name ? { name: foreignKey.name } : {}),
|
|
629
|
+
...(foreignKey.onDelete ? { onDelete: foreignKey.onDelete } : {}),
|
|
630
|
+
...(foreignKey.onUpdate ? { onUpdate: foreignKey.onUpdate } : {}),
|
|
631
|
+
...(foreignKey.constraint !== undefined ? { constraint: foreignKey.constraint } : {}),
|
|
632
|
+
...(foreignKey.index !== undefined ? { index: foreignKey.index } : {}),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function assertKnownExtensionPack(
|
|
637
|
+
extensionPacks: Record<string, ExtensionPackRef<'sql', string>> | undefined,
|
|
638
|
+
spaceId: string,
|
|
639
|
+
context: string,
|
|
640
|
+
): void {
|
|
641
|
+
if (extensionPacks !== undefined && Object.hasOwn(extensionPacks, spaceId)) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
throw new Error(
|
|
645
|
+
`${context} references contract space "${spaceId}" but "${spaceId}" is not declared in extensionPacks. Add the pack to extensionPacks.`,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
519
649
|
function resolveForeignKeyNodes(
|
|
520
650
|
spec: RuntimeModelSpec,
|
|
521
651
|
allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
|
|
652
|
+
extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
522
653
|
): readonly ForeignKeyNode[] {
|
|
523
654
|
const relationForeignKeys = resolveRelationForeignKeys(spec, allSpecs).map((foreignKey) => {
|
|
655
|
+
// F-relfk: relation-derived FKs for cross-space targets carry targetSpaceId;
|
|
656
|
+
// route them through the cross-space path, just like explicit sql() FKs.
|
|
657
|
+
if (foreignKey.targetSpaceId !== undefined) {
|
|
658
|
+
assertKnownExtensionPack(
|
|
659
|
+
extensionPacks,
|
|
660
|
+
foreignKey.targetSpaceId,
|
|
661
|
+
`Relation-derived foreign key on "${spec.modelName}"`,
|
|
662
|
+
);
|
|
663
|
+
return lowerCrossSpaceForeignKeyNode(spec, {
|
|
664
|
+
...foreignKey,
|
|
665
|
+
targetSpaceId: foreignKey.targetSpaceId,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
524
669
|
const targetSpec = allSpecs.get(foreignKey.targetModel);
|
|
525
670
|
if (!targetSpec) {
|
|
526
671
|
throw new Error(
|
|
@@ -528,10 +673,22 @@ function resolveForeignKeyNodes(
|
|
|
528
673
|
);
|
|
529
674
|
}
|
|
530
675
|
|
|
531
|
-
return
|
|
676
|
+
return lowerLocalForeignKeyNode(spec, targetSpec, foreignKey);
|
|
532
677
|
});
|
|
533
678
|
|
|
534
679
|
const sqlForeignKeys = (spec.sqlSpec?.foreignKeys ?? []).map((foreignKey) => {
|
|
680
|
+
if (foreignKey.targetSpaceId !== undefined) {
|
|
681
|
+
assertKnownExtensionPack(
|
|
682
|
+
extensionPacks,
|
|
683
|
+
foreignKey.targetSpaceId,
|
|
684
|
+
`Foreign key on "${spec.modelName}"`,
|
|
685
|
+
);
|
|
686
|
+
return lowerCrossSpaceForeignKeyNode(spec, {
|
|
687
|
+
...foreignKey,
|
|
688
|
+
targetSpaceId: foreignKey.targetSpaceId,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
535
692
|
const targetSpec = allSpecs.get(foreignKey.targetModel);
|
|
536
693
|
if (!targetSpec) {
|
|
537
694
|
throw new Error(
|
|
@@ -539,7 +696,7 @@ function resolveForeignKeyNodes(
|
|
|
539
696
|
);
|
|
540
697
|
}
|
|
541
698
|
|
|
542
|
-
return
|
|
699
|
+
return lowerLocalForeignKeyNode(spec, targetSpec, foreignKey);
|
|
543
700
|
});
|
|
544
701
|
|
|
545
702
|
return [...relationForeignKeys, ...sqlForeignKeys];
|
|
@@ -550,6 +707,7 @@ function resolveModelNode(
|
|
|
550
707
|
allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
|
|
551
708
|
storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
|
|
552
709
|
storageTypeReverseLookup: ReadonlyMap<StorageTypeInstance | PostgresEnumStorageEntry, string>,
|
|
710
|
+
extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
553
711
|
): ModelNode {
|
|
554
712
|
const fields: FieldNode[] = [];
|
|
555
713
|
|
|
@@ -567,6 +725,11 @@ function resolveModelNode(
|
|
|
567
725
|
throw new Error(`Column name resolution failed for "${spec.modelName}.${fieldName}"`);
|
|
568
726
|
}
|
|
569
727
|
|
|
728
|
+
const enumHandle =
|
|
729
|
+
'typeRef' in fieldState && isEnumTypeHandle(fieldState.typeRef)
|
|
730
|
+
? fieldState.typeRef
|
|
731
|
+
: undefined;
|
|
732
|
+
|
|
570
733
|
fields.push({
|
|
571
734
|
fieldName,
|
|
572
735
|
columnName,
|
|
@@ -574,6 +737,7 @@ function resolveModelNode(
|
|
|
574
737
|
nullable: fieldState.nullable,
|
|
575
738
|
...(fieldState.default ? { default: fieldState.default } : {}),
|
|
576
739
|
...(fieldState.executionDefaults ? { executionDefaults: fieldState.executionDefaults } : {}),
|
|
740
|
+
...(enumHandle !== undefined ? { enumTypeHandle: enumHandle } : {}),
|
|
577
741
|
});
|
|
578
742
|
}
|
|
579
743
|
|
|
@@ -588,9 +752,9 @@ function resolveModelNode(
|
|
|
588
752
|
...ifDefined('type', index.type),
|
|
589
753
|
...ifDefined('options', index.options),
|
|
590
754
|
})) satisfies readonly IndexNode[];
|
|
591
|
-
const foreignKeys = resolveForeignKeyNodes(spec, allSpecs);
|
|
755
|
+
const foreignKeys = resolveForeignKeyNodes(spec, allSpecs, extensionPacks);
|
|
592
756
|
const relations = Object.entries(spec.relations).map(([relationName, relationBuilder]) =>
|
|
593
|
-
resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs),
|
|
757
|
+
resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs, extensionPacks),
|
|
594
758
|
);
|
|
595
759
|
|
|
596
760
|
return {
|
|
@@ -614,6 +778,7 @@ function resolveModelNode(
|
|
|
614
778
|
...(indexes.length > 0 ? { indexes } : {}),
|
|
615
779
|
...(foreignKeys.length > 0 ? { foreignKeys } : {}),
|
|
616
780
|
...(relations.length > 0 ? { relations } : {}),
|
|
781
|
+
...ifDefined('control', spec.sqlSpec?.control),
|
|
617
782
|
};
|
|
618
783
|
}
|
|
619
784
|
|
|
@@ -687,7 +852,10 @@ function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection
|
|
|
687
852
|
};
|
|
688
853
|
}
|
|
689
854
|
|
|
690
|
-
function lowerModels(
|
|
855
|
+
function lowerModels(
|
|
856
|
+
collection: RuntimeCollection,
|
|
857
|
+
extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
858
|
+
): readonly ModelNode[] {
|
|
691
859
|
emitTypedCrossModelFallbackWarnings(collection);
|
|
692
860
|
|
|
693
861
|
const storageTypeReverseLookup = buildStorageTypeReverseLookup(collection.storageTypes);
|
|
@@ -697,16 +865,18 @@ function lowerModels(collection: RuntimeCollection): readonly ModelNode[] {
|
|
|
697
865
|
collection.modelSpecs,
|
|
698
866
|
collection.storageTypes,
|
|
699
867
|
storageTypeReverseLookup,
|
|
868
|
+
extensionPacks,
|
|
700
869
|
),
|
|
701
870
|
);
|
|
702
871
|
}
|
|
703
872
|
|
|
704
873
|
export function buildContractDefinition(definition: ContractInput): ContractDefinition {
|
|
705
874
|
const collection = collectRuntimeModelSpecs(definition);
|
|
706
|
-
const models = lowerModels(collection);
|
|
875
|
+
const models = lowerModels(collection, definition.extensionPacks);
|
|
707
876
|
|
|
708
877
|
return {
|
|
709
878
|
target: definition.target,
|
|
879
|
+
...ifDefined('defaultControlPolicy', definition.defaultControlPolicy),
|
|
710
880
|
...(definition.extensionPacks ? { extensionPacks: definition.extensionPacks } : {}),
|
|
711
881
|
...(definition.storageHash ? { storageHash: definition.storageHash } : {}),
|
|
712
882
|
...(definition.foreignKeyDefaults ? { foreignKeyDefaults: definition.foreignKeyDefaults } : {}),
|
|
@@ -715,6 +885,9 @@ export function buildContractDefinition(definition: ContractInput): ContractDefi
|
|
|
715
885
|
: {}),
|
|
716
886
|
...(definition.namespaces ? { namespaces: definition.namespaces } : {}),
|
|
717
887
|
...(definition.createNamespace ? { createNamespace: definition.createNamespace } : {}),
|
|
888
|
+
...(definition.enums && Object.keys(definition.enums).length > 0
|
|
889
|
+
? { enums: definition.enums }
|
|
890
|
+
: {}),
|
|
718
891
|
models,
|
|
719
892
|
};
|
|
720
893
|
}
|
package/src/contract-types.ts
CHANGED
|
@@ -6,11 +6,11 @@ import type {
|
|
|
6
6
|
StorageHashBase,
|
|
7
7
|
} from '@prisma-next/contract/types';
|
|
8
8
|
import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';
|
|
9
|
+
import type { StorageType } from '@prisma-next/framework-components/ir';
|
|
9
10
|
import type { IndexTypeRegistration } from '@prisma-next/sql-contract/index-types';
|
|
10
11
|
import type {
|
|
11
12
|
ContractWithTypeMaps,
|
|
12
13
|
Index,
|
|
13
|
-
PostgresEnumStorageEntry,
|
|
14
14
|
ReferentialAction,
|
|
15
15
|
StorageTypeInstance,
|
|
16
16
|
TypeMaps,
|
|
@@ -144,10 +144,7 @@ type DefinitionNamespaces<Definition> = Definition extends {
|
|
|
144
144
|
type DefinitionTypes<Definition> = Definition extends {
|
|
145
145
|
readonly types?: unknown;
|
|
146
146
|
}
|
|
147
|
-
? Present<Definition['types']> extends Record<
|
|
148
|
-
string,
|
|
149
|
-
StorageTypeInstance | PostgresEnumStorageEntry
|
|
150
|
-
>
|
|
147
|
+
? Present<Definition['types']> extends Record<string, StorageType>
|
|
151
148
|
? Present<Definition['types']>
|
|
152
149
|
: Record<never, never>
|
|
153
150
|
: Record<never, never>;
|
|
@@ -311,10 +308,7 @@ type DescriptorTypeRef<Descriptor> = Descriptor extends {
|
|
|
311
308
|
? TypeRef
|
|
312
309
|
: undefined;
|
|
313
310
|
|
|
314
|
-
type LookupNamedStorageTypeKeyByValue<
|
|
315
|
-
Definition,
|
|
316
|
-
TypeRef extends StorageTypeInstance | PostgresEnumStorageEntry,
|
|
317
|
-
> = {
|
|
311
|
+
type LookupNamedStorageTypeKeyByValue<Definition, TypeRef extends StorageType> = {
|
|
318
312
|
[TypeName in keyof DefinitionTypes<Definition> & string]: [TypeRef] extends [
|
|
319
313
|
DefinitionTypes<Definition>[TypeName],
|
|
320
314
|
]
|
|
@@ -326,7 +320,7 @@ type LookupNamedStorageTypeKeyByValue<
|
|
|
326
320
|
|
|
327
321
|
type ResolveNamedStorageTypeKey<Definition, TypeRef> = TypeRef extends string
|
|
328
322
|
? TypeRef
|
|
329
|
-
: TypeRef extends
|
|
323
|
+
: TypeRef extends StorageType
|
|
330
324
|
? [LookupNamedStorageTypeKeyByValue<Definition, TypeRef>] extends [never]
|
|
331
325
|
? string
|
|
332
326
|
: LookupNamedStorageTypeKeyByValue<Definition, TypeRef>
|
|
@@ -540,6 +534,7 @@ type BuiltStorageTables<Definition> = {
|
|
|
540
534
|
};
|
|
541
535
|
readonly target: {
|
|
542
536
|
readonly namespaceId: NamespaceId;
|
|
537
|
+
readonly spaceId?: string;
|
|
543
538
|
readonly tableName: string;
|
|
544
539
|
readonly columns: readonly string[];
|
|
545
540
|
};
|
|
@@ -564,9 +559,9 @@ type BuiltStorageTables<Definition> = {
|
|
|
564
559
|
};
|
|
565
560
|
|
|
566
561
|
type BuiltDocumentScopedTypes<Definition> = {
|
|
567
|
-
readonly [K in keyof DefinitionTypes<Definition> as DefinitionTypes<Definition>[K] extends
|
|
568
|
-
?
|
|
569
|
-
:
|
|
562
|
+
readonly [K in keyof DefinitionTypes<Definition> as DefinitionTypes<Definition>[K] extends StorageTypeInstance
|
|
563
|
+
? K
|
|
564
|
+
: never]: DefinitionTypes<Definition>[K];
|
|
570
565
|
};
|
|
571
566
|
|
|
572
567
|
type BuiltDomain<Definition> =
|
|
@@ -586,19 +581,20 @@ type BuiltStorage<Definition> = {
|
|
|
586
581
|
readonly types?: BuiltDocumentScopedTypes<Definition>;
|
|
587
582
|
// The primary namespace key is target-specific: Postgres uses `public` (the
|
|
588
583
|
// default schema), all other SQL targets use `__unbound__`. The namespace
|
|
589
|
-
// carries the narrowed
|
|
584
|
+
// carries the narrowed `entries.table` shape so downstream DSL surfaces keep
|
|
590
585
|
// literal-keyed access without an optional-narrowing dance. The shape is
|
|
591
586
|
// described inline (rather than intersecting with `SqlStorage['namespaces']`)
|
|
592
587
|
// so its `Readonly<Record<string, Namespace>>` index signature doesn't
|
|
593
|
-
// collapse
|
|
594
|
-
//
|
|
595
|
-
//
|
|
588
|
+
// collapse slot keys to `string`. The literal object is still structurally
|
|
589
|
+
// assignable to `SqlStorage['namespaces']` because every value satisfies the
|
|
590
|
+
// framework `Namespace` interface.
|
|
596
591
|
readonly namespaces: {
|
|
597
592
|
readonly [K in DefaultStorageNamespaceId<Definition>]: {
|
|
598
593
|
readonly id: K;
|
|
599
594
|
readonly kind: string;
|
|
600
|
-
readonly
|
|
601
|
-
|
|
595
|
+
readonly entries: {
|
|
596
|
+
readonly table: BuiltStorageTables<Definition>;
|
|
597
|
+
};
|
|
602
598
|
};
|
|
603
599
|
} & {
|
|
604
600
|
readonly [Ns in Exclude<
|
|
@@ -607,8 +603,9 @@ type BuiltStorage<Definition> = {
|
|
|
607
603
|
>]: {
|
|
608
604
|
readonly id: Ns;
|
|
609
605
|
readonly kind: string;
|
|
610
|
-
readonly
|
|
611
|
-
|
|
606
|
+
readonly entries: {
|
|
607
|
+
readonly table: Record<never, never>;
|
|
608
|
+
};
|
|
612
609
|
};
|
|
613
610
|
};
|
|
614
611
|
};
|
package/src/enum-type.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { ColumnTypeDescriptor } from '@prisma-next/framework-components/codec';
|
|
2
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// EnumMember — a single member declaration with literal type preservation
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A single enum member produced by `member()`. The `Name` and `Value` generics
|
|
10
|
+
* are preserved as literal types so `enumType()` can carry the ordered value
|
|
11
|
+
* tuple in its return type.
|
|
12
|
+
*/
|
|
13
|
+
export interface EnumMember<Name extends string, Value extends string> {
|
|
14
|
+
readonly name: Name;
|
|
15
|
+
readonly value: Value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Declare an enum member. The `value` defaults to `name` when omitted.
|
|
20
|
+
* Both generics are preserved as literals so downstream `enumType` can
|
|
21
|
+
* carry the ordered value tuple in its type.
|
|
22
|
+
*/
|
|
23
|
+
export function member<const Name extends string>(name: Name): EnumMember<Name, Name>;
|
|
24
|
+
export function member<const Name extends string, const Value extends string>(
|
|
25
|
+
name: Name,
|
|
26
|
+
value: Value,
|
|
27
|
+
): EnumMember<Name, Value>;
|
|
28
|
+
export function member<const Name extends string, const Value extends string = Name>(
|
|
29
|
+
name: Name,
|
|
30
|
+
value?: Value,
|
|
31
|
+
): EnumMember<Name, Value> {
|
|
32
|
+
return {
|
|
33
|
+
name,
|
|
34
|
+
value: blindCast<
|
|
35
|
+
Value,
|
|
36
|
+
'overload signatures enforce Value=Name when value is omitted; default generic Value=Name makes this safe'
|
|
37
|
+
>(value ?? name),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Internal types for inferring the literal tuple from the members spread
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
type MembersToValues<Members extends readonly EnumMember<string, string>[]> = {
|
|
46
|
+
readonly [K in keyof Members]: Members[K] extends EnumMember<string, infer V> ? V : never;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type MembersToNames<Members extends readonly EnumMember<string, string>[]> = {
|
|
50
|
+
readonly [K in keyof Members]: Members[K] extends EnumMember<infer N, string> ? N : never;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type MembersAccessorMap<Members extends readonly EnumMember<string, string>[]> = {
|
|
54
|
+
readonly [M in Members[number] as M['name']]: M['value'];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// EnumTypeHandle — the authoring handle returned by enumType()
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Internal brand that identifies an EnumTypeHandle in the lowering pipeline.
|
|
63
|
+
* Not exported — callers only interact with `EnumTypeHandle`.
|
|
64
|
+
*/
|
|
65
|
+
export const ENUM_TYPE_HANDLE_BRAND = Symbol('EnumTypeHandle');
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Authoring handle returned by `enumType()`. Carries:
|
|
69
|
+
*
|
|
70
|
+
* - The ordered literal value tuple (`.values`) and name tuple (`.names`)
|
|
71
|
+
* so downstream type-tests can assert literal preservation.
|
|
72
|
+
* - A namespaced member accessor map (`.members`) to avoid collisions with
|
|
73
|
+
* `.values` / `.has` / `.nameOf` / `.ordinalOf`.
|
|
74
|
+
* - Runtime helpers `.has()`, `.nameOf()`, `.ordinalOf()`.
|
|
75
|
+
* - Internal metadata (`enumName`, `codecId`, `nativeType`,
|
|
76
|
+
* `enumMembers`) for the lowering pipeline.
|
|
77
|
+
*
|
|
78
|
+
* The type is generic over the ordered value tuple so callers that assign
|
|
79
|
+
* `const Role = enumType(...)` retain the literal tuple on `.values`.
|
|
80
|
+
*/
|
|
81
|
+
export interface EnumTypeHandle<
|
|
82
|
+
Name extends string = string,
|
|
83
|
+
Values extends readonly string[] = readonly string[],
|
|
84
|
+
Names extends readonly string[] = readonly string[],
|
|
85
|
+
MembersMap extends Record<string, string> = Record<string, string>,
|
|
86
|
+
> {
|
|
87
|
+
/** Internal brand for lowering-pipeline detection. */
|
|
88
|
+
readonly [ENUM_TYPE_HANDLE_BRAND]: true;
|
|
89
|
+
|
|
90
|
+
/** The enum's declared name (used as the key in domain `enum` / storage `valueSet`). */
|
|
91
|
+
readonly enumName: Name;
|
|
92
|
+
|
|
93
|
+
/** codecId from the codec passed to `enumType`. */
|
|
94
|
+
readonly codecId: string;
|
|
95
|
+
|
|
96
|
+
/** nativeType from the codec passed to `enumType`. */
|
|
97
|
+
readonly nativeType: string;
|
|
98
|
+
|
|
99
|
+
/** Ordered member list for lowering (name + value pairs). */
|
|
100
|
+
readonly enumMembers: readonly { readonly name: string; readonly value: string }[];
|
|
101
|
+
|
|
102
|
+
/** Ordered literal value tuple. Declaration order is preserved. */
|
|
103
|
+
readonly values: Values;
|
|
104
|
+
|
|
105
|
+
/** Ordered literal name tuple. Declaration order is preserved. */
|
|
106
|
+
readonly names: Names;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Namespaced accessor map: `Role.members.User === 'user'`.
|
|
110
|
+
* Namespaced under `.members` to avoid collisions with `.values` / `.has`.
|
|
111
|
+
*/
|
|
112
|
+
readonly members: MembersMap;
|
|
113
|
+
|
|
114
|
+
/** Returns `true` if `v` is a declared member value. */
|
|
115
|
+
has(v: string): boolean;
|
|
116
|
+
|
|
117
|
+
/** Returns the member name for a value, or `undefined` if not found. */
|
|
118
|
+
nameOf(v: string): string | undefined;
|
|
119
|
+
|
|
120
|
+
/** Returns the zero-based declaration index of a value, or `-1` if not found. */
|
|
121
|
+
ordinalOf(v: string): number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// enumType()
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Declare a domain enum for use in TS-authoring contracts.
|
|
130
|
+
*
|
|
131
|
+
* - The codec is an explicit required argument — the `codecId` and
|
|
132
|
+
* `nativeType` are taken from the passed `ColumnTypeDescriptor` (e.g.
|
|
133
|
+
* `{ codecId: 'pg/text@1', nativeType: 'text' }` from a field preset
|
|
134
|
+
* output or a direct inline object).
|
|
135
|
+
* - `const` generics on the members spread preserve the ordered literal
|
|
136
|
+
* value tuple so `Role.values` is `readonly ['user','admin']`, not
|
|
137
|
+
* `string[]`.
|
|
138
|
+
* - Well-formedness assertions at construction: non-empty member list;
|
|
139
|
+
* unique names; unique values.
|
|
140
|
+
*
|
|
141
|
+
* The returned handle wires into `field.namedType(handle)` to set
|
|
142
|
+
* `valueSet` refs on both the domain field and the storage column.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* const Role = enumType('Role', { codecId: 'pg/text@1', nativeType: 'text' },
|
|
147
|
+
* member('User', 'user'),
|
|
148
|
+
* member('Admin', 'admin'),
|
|
149
|
+
* );
|
|
150
|
+
* // Role.values → readonly ['user', 'admin']
|
|
151
|
+
* // Role.members.User → 'user'
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export function enumType<
|
|
155
|
+
const Name extends string,
|
|
156
|
+
const Codec extends Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
|
|
157
|
+
const Members extends readonly [EnumMember<string, string>, ...EnumMember<string, string>[]],
|
|
158
|
+
>(
|
|
159
|
+
name: Name,
|
|
160
|
+
codec: Codec,
|
|
161
|
+
...members: Members
|
|
162
|
+
): EnumTypeHandle<
|
|
163
|
+
Name,
|
|
164
|
+
MembersToValues<[...Members]>,
|
|
165
|
+
MembersToNames<[...Members]>,
|
|
166
|
+
MembersAccessorMap<[...Members]>
|
|
167
|
+
>;
|
|
168
|
+
export function enumType(
|
|
169
|
+
name: string,
|
|
170
|
+
codec: Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
|
|
171
|
+
...members: EnumMember<string, string>[]
|
|
172
|
+
): EnumTypeHandle;
|
|
173
|
+
export function enumType(
|
|
174
|
+
name: string,
|
|
175
|
+
codec: Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
|
|
176
|
+
...members: EnumMember<string, string>[]
|
|
177
|
+
): EnumTypeHandle {
|
|
178
|
+
if (members.length === 0) {
|
|
179
|
+
throw new Error(`enumType("${name}"): must have at least one member.`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const seenNames = new Set<string>();
|
|
183
|
+
const seenValues = new Set<string>();
|
|
184
|
+
for (const m of members) {
|
|
185
|
+
if (seenNames.has(m.name)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`enumType("${name}"): duplicate member name "${m.name}". Member names must be unique.`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
seenNames.add(m.name);
|
|
191
|
+
|
|
192
|
+
if (seenValues.has(m.value)) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`enumType("${name}"): duplicate member value "${m.value}". Member values must be unique.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
seenValues.add(m.value);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const values = Object.freeze(members.map((m) => m.value));
|
|
201
|
+
const names = Object.freeze(members.map((m) => m.name));
|
|
202
|
+
const enumMembers = Object.freeze(members.map((m) => ({ name: m.name, value: m.value })));
|
|
203
|
+
|
|
204
|
+
const membersAccessor = Object.freeze(Object.fromEntries(members.map((m) => [m.name, m.value])));
|
|
205
|
+
|
|
206
|
+
const valueSet = new Set(values);
|
|
207
|
+
const valueToName = new Map(members.map((m) => [m.value, m.name]));
|
|
208
|
+
const valueToOrdinal = new Map(values.map((v, i) => [v, i]));
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
[ENUM_TYPE_HANDLE_BRAND]: true,
|
|
212
|
+
enumName: name,
|
|
213
|
+
codecId: codec.codecId,
|
|
214
|
+
nativeType: codec.nativeType,
|
|
215
|
+
enumMembers,
|
|
216
|
+
values,
|
|
217
|
+
names,
|
|
218
|
+
members: membersAccessor,
|
|
219
|
+
has: (v: string) => valueSet.has(v),
|
|
220
|
+
nameOf: (v: string) => valueToName.get(v),
|
|
221
|
+
ordinalOf: (v: string) => valueToOrdinal.get(v) ?? -1,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Returns true when the value is an `EnumTypeHandle` produced by
|
|
227
|
+
* `enumType()`. Used in the lowering pipeline to detect enum handles
|
|
228
|
+
* in field state without importing the BRAND symbol at every call site.
|
|
229
|
+
*/
|
|
230
|
+
export function isEnumTypeHandle(value: unknown): value is EnumTypeHandle {
|
|
231
|
+
return (
|
|
232
|
+
typeof value === 'object' &&
|
|
233
|
+
value !== null &&
|
|
234
|
+
Reflect.get(value, ENUM_TYPE_HANDLE_BRAND) === true
|
|
235
|
+
);
|
|
236
|
+
}
|