@prisma-next/sql-contract-ts 0.14.0-dev.8 → 0.14.0-dev.81
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-BzAyIu0y.mjs} +240 -28
- package/dist/build-contract-BzAyIu0y.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 +104 -187
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +144 -87
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +14 -14
- package/src/authoring-helper-runtime.ts +2 -6
- package/src/authoring-type-utils.ts +6 -3
- package/src/build-contract.ts +401 -49
- package/src/composed-authoring-helpers.ts +3 -6
- package/src/config-types.ts +7 -1
- package/src/contract-builder.ts +17 -5
- package/src/contract-definition.ts +32 -4
- package/src/contract-dsl.ts +215 -108
- package/src/contract-lowering.ts +155 -1
- package/src/contract-types.ts +70 -13
- package/src/enum-type.ts +14 -306
- package/src/exports/contract-builder.ts +2 -0
- package/dist/build-contract-CQ4u83jx.mjs.map +0 -1
package/src/contract-lowering.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AuthoringEntityTypeNamespace,
|
|
3
|
+
isAuthoringEntityTypeDescriptor,
|
|
4
|
+
} from '@prisma-next/framework-components/authoring';
|
|
1
5
|
import type { ColumnTypeDescriptor } from '@prisma-next/framework-components/codec';
|
|
2
6
|
import type { ExtensionPackRef } from '@prisma-next/framework-components/components';
|
|
7
|
+
import {
|
|
8
|
+
providesEntityHandleLowering,
|
|
9
|
+
type ResolvedEntityHandleRef,
|
|
10
|
+
type ResolvedPackEntityHandle,
|
|
11
|
+
} from '@prisma-next/sql-contract/entity-handle-lowering-hook';
|
|
3
12
|
import type { StorageTypeInstance } from '@prisma-next/sql-contract/types';
|
|
4
13
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
5
14
|
import type {
|
|
15
|
+
AttachedEntities,
|
|
6
16
|
ContractDefinition,
|
|
7
17
|
FieldNode,
|
|
8
18
|
ForeignKeyNode,
|
|
@@ -19,6 +29,7 @@ import {
|
|
|
19
29
|
type FieldStateOf,
|
|
20
30
|
type ForeignKeyConstraint,
|
|
21
31
|
type IdConstraint,
|
|
32
|
+
isCrossSpaceHandle,
|
|
22
33
|
type ModelAttributesSpec,
|
|
23
34
|
normalizeRelationFieldNames,
|
|
24
35
|
type RelationBuilder,
|
|
@@ -727,6 +738,7 @@ function resolveModelNode(
|
|
|
727
738
|
columnName,
|
|
728
739
|
descriptor,
|
|
729
740
|
nullable: fieldState.nullable,
|
|
741
|
+
...(fieldState.many === true ? { many: true } : {}),
|
|
730
742
|
...(fieldState.default ? { default: fieldState.default } : {}),
|
|
731
743
|
...(fieldState.executionDefaults ? { executionDefaults: fieldState.executionDefaults } : {}),
|
|
732
744
|
...(enumHandle !== undefined ? { enumTypeHandle: enumHandle } : {}),
|
|
@@ -863,9 +875,150 @@ function lowerModels(
|
|
|
863
875
|
);
|
|
864
876
|
}
|
|
865
877
|
|
|
878
|
+
/**
|
|
879
|
+
* Kind-agnostic walk over the author-declared `entities` handle list:
|
|
880
|
+
*
|
|
881
|
+
* 1. Index the bound packs' `entityTypes` contributions by discriminator so
|
|
882
|
+
* each handle's `entityKind` maps to the pack that registered it; a
|
|
883
|
+
* handle whose kind no composed pack registers is an error naming the
|
|
884
|
+
* kind.
|
|
885
|
+
* 2. Resolve each handle's declared model refs (`handle.refs`, actual
|
|
886
|
+
* model-handle objects) to storage table coordinates — identity against
|
|
887
|
+
* the contract's `models` record first, then the handle's declared model
|
|
888
|
+
* name against the build's model specs; never by re-deriving a table
|
|
889
|
+
* name. A cross-space (extensionModel) handle resolves to its own
|
|
890
|
+
* coordinate annotated with `spaceId`.
|
|
891
|
+
* 3. Call each owning pack's batch lowering hook once with all of its
|
|
892
|
+
* claimed handles, and fold the returned rows into the namespace-scoped
|
|
893
|
+
* attachments (namespace → kind → key), rejecting two different entities
|
|
894
|
+
* in one slot. The result becomes `ContractDefinition.attachedEntities`.
|
|
895
|
+
*
|
|
896
|
+
* No entity kind is named anywhere in this walk.
|
|
897
|
+
*/
|
|
898
|
+
function lowerPackEntityHandles(
|
|
899
|
+
definition: ContractInput,
|
|
900
|
+
modelSpecs: ReadonlyMap<string, RuntimeModelSpec>,
|
|
901
|
+
): AttachedEntities | undefined {
|
|
902
|
+
const entities = definition.entities;
|
|
903
|
+
if (entities === undefined || entities.length === 0) return undefined;
|
|
904
|
+
|
|
905
|
+
const components: readonly {
|
|
906
|
+
readonly authoring?: import('@prisma-next/framework-components/authoring').AuthoringContributions;
|
|
907
|
+
}[] = [
|
|
908
|
+
definition.target,
|
|
909
|
+
...Object.values<ExtensionPackRef<'sql', string>>(definition.extensionPacks ?? {}),
|
|
910
|
+
];
|
|
911
|
+
const owningComponent = new Map<string, (typeof components)[number]>();
|
|
912
|
+
const walkEntityTypes = (
|
|
913
|
+
namespace: AuthoringEntityTypeNamespace,
|
|
914
|
+
component: (typeof components)[number],
|
|
915
|
+
): void => {
|
|
916
|
+
for (const value of Object.values(namespace)) {
|
|
917
|
+
if (isAuthoringEntityTypeDescriptor(value)) {
|
|
918
|
+
owningComponent.set(value.discriminator, component);
|
|
919
|
+
} else {
|
|
920
|
+
walkEntityTypes(value, component);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
for (const component of components) {
|
|
925
|
+
const entityTypes = component.authoring?.entityTypes;
|
|
926
|
+
if (entityTypes !== undefined) walkEntityTypes(entityTypes, component);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const defaultNamespaceId = definition.target.defaultNamespaceId;
|
|
930
|
+
const modelNamesByIdentity = new Map<unknown, string>();
|
|
931
|
+
for (const [modelName, modelBuilder] of Object.entries(definition.models ?? {})) {
|
|
932
|
+
modelNamesByIdentity.set(modelBuilder, modelName);
|
|
933
|
+
}
|
|
934
|
+
const coordinateOf = (modelName: string): ResolvedEntityHandleRef | undefined => {
|
|
935
|
+
const spec = modelSpecs.get(modelName);
|
|
936
|
+
if (spec === undefined) return undefined;
|
|
937
|
+
return {
|
|
938
|
+
kind: 'resolved',
|
|
939
|
+
namespaceId: spec.namespace ?? defaultNamespaceId,
|
|
940
|
+
tableName: spec.tableName,
|
|
941
|
+
modelName,
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
const declaredModelName = (value: unknown): string | undefined => {
|
|
945
|
+
if (typeof value !== 'object' || value === null || !('stageOne' in value)) return undefined;
|
|
946
|
+
const stageOne = value.stageOne;
|
|
947
|
+
if (typeof stageOne !== 'object' || stageOne === null || !('modelName' in stageOne)) {
|
|
948
|
+
return undefined;
|
|
949
|
+
}
|
|
950
|
+
return typeof stageOne.modelName === 'string' ? stageOne.modelName : undefined;
|
|
951
|
+
};
|
|
952
|
+
const resolveRef = (value: unknown): ResolvedEntityHandleRef => {
|
|
953
|
+
const modelName = declaredModelName(value);
|
|
954
|
+
if (isCrossSpaceHandle(value)) {
|
|
955
|
+
const tableName = value.tableName;
|
|
956
|
+
const namespaceId = value.stageOne.namespace;
|
|
957
|
+
if (tableName !== undefined && namespaceId !== undefined) {
|
|
958
|
+
return {
|
|
959
|
+
kind: 'cross-space',
|
|
960
|
+
spaceId: value.spaceId,
|
|
961
|
+
namespaceId,
|
|
962
|
+
tableName,
|
|
963
|
+
...ifDefined('modelName', modelName),
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
return { kind: 'unresolved', ...ifDefined('modelName', modelName) };
|
|
967
|
+
}
|
|
968
|
+
const identityName = modelNamesByIdentity.get(value);
|
|
969
|
+
const resolved =
|
|
970
|
+
(identityName !== undefined ? coordinateOf(identityName) : undefined) ??
|
|
971
|
+
(modelName !== undefined ? coordinateOf(modelName) : undefined);
|
|
972
|
+
return resolved ?? { kind: 'unresolved', ...ifDefined('modelName', modelName) };
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
const claimed = new Map<(typeof components)[number], ResolvedPackEntityHandle[]>();
|
|
976
|
+
for (const handle of entities) {
|
|
977
|
+
const component = owningComponent.get(handle.entityKind);
|
|
978
|
+
if (component === undefined) {
|
|
979
|
+
throw new Error(
|
|
980
|
+
`defineContract: entities contains a handle with entityKind "${handle.entityKind}", which no composed pack registers. Compose a pack whose entityTypes contribution claims "${handle.entityKind}", or remove the handle.`,
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
const refs: Record<string, ResolvedEntityHandleRef> = {};
|
|
984
|
+
for (const [refName, refValue] of Object.entries(handle.refs ?? {})) {
|
|
985
|
+
refs[refName] = resolveRef(refValue);
|
|
986
|
+
}
|
|
987
|
+
const forComponent = claimed.get(component) ?? [];
|
|
988
|
+
forComponent.push({ handle, refs });
|
|
989
|
+
claimed.set(component, forComponent);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const pack: Record<string, Record<string, Record<string, unknown>>> = {};
|
|
993
|
+
for (const [component, handles] of claimed) {
|
|
994
|
+
const authoring = component.authoring;
|
|
995
|
+
if (!providesEntityHandleLowering(authoring)) {
|
|
996
|
+
const kinds = [...new Set(handles.map((entry) => entry.handle.entityKind))].sort();
|
|
997
|
+
throw new Error(
|
|
998
|
+
`defineContract: entityKind(s) ${kinds.map((kind) => `"${kind}"`).join(', ')} are registered by a pack that does not implement entity-handle lowering (no lowerEntityHandles on its authoring contributions).`,
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
for (const row of authoring.lowerEntityHandles({ handles, defaultNamespaceId })) {
|
|
1002
|
+
const forNamespace = pack[row.namespaceId] ?? {};
|
|
1003
|
+
pack[row.namespaceId] = forNamespace;
|
|
1004
|
+
const forKind = forNamespace[row.entityKind] ?? {};
|
|
1005
|
+
forNamespace[row.entityKind] = forKind;
|
|
1006
|
+
const existing = forKind[row.key];
|
|
1007
|
+
if (existing !== undefined && existing !== row.entity) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`defineContract: two different "${row.entityKind}" entities named "${row.key}" in namespace "${row.namespaceId}" — pack-entity names must be unique per namespace.`,
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
forKind[row.key] = row.entity;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return pack;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
866
1018
|
export function buildContractDefinition(definition: ContractInput): ContractDefinition {
|
|
867
1019
|
const collection = collectRuntimeModelSpecs(definition);
|
|
868
1020
|
const models = lowerModels(collection, definition.extensionPacks);
|
|
1021
|
+
const attachedEntities = lowerPackEntityHandles(definition, collection.modelSpecs);
|
|
869
1022
|
|
|
870
1023
|
return {
|
|
871
1024
|
target: definition.target,
|
|
@@ -877,10 +1030,11 @@ export function buildContractDefinition(definition: ContractInput): ContractDefi
|
|
|
877
1030
|
? { storageTypes: collection.storageTypes }
|
|
878
1031
|
: {}),
|
|
879
1032
|
...(definition.namespaces ? { namespaces: definition.namespaces } : {}),
|
|
880
|
-
|
|
1033
|
+
createNamespace: definition.createNamespace,
|
|
881
1034
|
...(definition.enums && Object.keys(definition.enums).length > 0
|
|
882
1035
|
? { enums: definition.enums }
|
|
883
1036
|
: {}),
|
|
1037
|
+
...(attachedEntities && Object.keys(attachedEntities).length > 0 ? { attachedEntities } : {}),
|
|
884
1038
|
models,
|
|
885
1039
|
};
|
|
886
1040
|
}
|
package/src/contract-types.ts
CHANGED
|
@@ -21,10 +21,10 @@ import type { UnionToIntersection } from './authoring-type-utils';
|
|
|
21
21
|
import type { AttributeStageIdFieldNames, FieldStateOf, ScalarFieldBuilder } from './contract-dsl';
|
|
22
22
|
import type { EnumTypeHandle } from './enum-type';
|
|
23
23
|
|
|
24
|
-
export type ExtractCodecTypesFromPack<P> = P extends {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
export type ExtractCodecTypesFromPack<P> = P extends {
|
|
25
|
+
__codecTypes?: infer C extends Record<string, { output: unknown }>;
|
|
26
|
+
}
|
|
27
|
+
? C
|
|
28
28
|
: Record<string, never>;
|
|
29
29
|
|
|
30
30
|
export type MergeExtensionCodecTypes<Packs extends Record<string, unknown>> = UnionToIntersection<
|
|
@@ -279,6 +279,8 @@ type FieldNullableOf<FieldState> = FieldState extends {
|
|
|
279
279
|
? Nullable
|
|
280
280
|
: boolean;
|
|
281
281
|
|
|
282
|
+
type FieldManyOf<FieldState> = FieldState extends { readonly many?: true } ? true : false;
|
|
283
|
+
|
|
282
284
|
type FieldColumnOverrideOf<FieldState> = Present<
|
|
283
285
|
FieldState extends { readonly columnName?: infer ColumnName } ? ColumnName : never
|
|
284
286
|
>;
|
|
@@ -471,15 +473,17 @@ type StorageColumn<
|
|
|
471
473
|
NativeType extends string,
|
|
472
474
|
TypeRef extends string | undefined = undefined,
|
|
473
475
|
TypeParams extends Record<string, unknown> | undefined = undefined,
|
|
476
|
+
Many extends boolean = false,
|
|
474
477
|
> = {
|
|
475
478
|
readonly nativeType: NativeType;
|
|
476
479
|
readonly codecId: CodecId;
|
|
477
480
|
readonly nullable: Nullable;
|
|
478
481
|
readonly default?: ColumnDefault;
|
|
479
|
-
} & (TypeRef extends string ? { readonly typeRef: TypeRef } : Record<
|
|
482
|
+
} & (TypeRef extends string ? { readonly typeRef: TypeRef } : Record<never, never>) &
|
|
480
483
|
(TypeParams extends Record<string, unknown>
|
|
481
484
|
? { readonly typeParams: TypeParams }
|
|
482
|
-
: Record<
|
|
485
|
+
: Record<never, never>) &
|
|
486
|
+
(Many extends true ? { readonly many: true } : Record<never, never>);
|
|
483
487
|
|
|
484
488
|
type ModelStorageColumn<
|
|
485
489
|
Definition,
|
|
@@ -496,7 +500,8 @@ type ModelStorageColumn<
|
|
|
496
500
|
ResolveFieldDescriptor<Definition, ModelFieldState<Definition, ModelName, FieldName>>
|
|
497
501
|
>,
|
|
498
502
|
ResolveFieldColumnTypeRef<Definition, ModelFieldState<Definition, ModelName, FieldName>>,
|
|
499
|
-
ResolveFieldColumnTypeParams<Definition, ModelFieldState<Definition, ModelName, FieldName
|
|
503
|
+
ResolveFieldColumnTypeParams<Definition, ModelFieldState<Definition, ModelName, FieldName>>,
|
|
504
|
+
FieldManyOf<ModelFieldState<Definition, ModelName, FieldName>>
|
|
500
505
|
>
|
|
501
506
|
: never;
|
|
502
507
|
|
|
@@ -677,6 +682,8 @@ type BuiltStorage<Definition> = {
|
|
|
677
682
|
};
|
|
678
683
|
};
|
|
679
684
|
|
|
685
|
+
type StorageColumnManyOf<Col> = Col extends { readonly many: true } ? true : false;
|
|
686
|
+
|
|
680
687
|
// The enum value union for an enum-typed field, or `never` for a non-enum
|
|
681
688
|
// field. The field's `typeRef` carries the authored `EnumTypeHandle`, whose
|
|
682
689
|
// `Values` tuple preserves the literal member values (text or numeric).
|
|
@@ -688,6 +695,31 @@ type EnumValueUnion<FieldState> = [FieldTypeRefOf<FieldState>] extends [
|
|
|
688
695
|
: Values[number]
|
|
689
696
|
: never;
|
|
690
697
|
|
|
698
|
+
// The member-value literal tuple carried on a descriptor's `entityRef.entity`
|
|
699
|
+
// (e.g. a target's native-enum entity), or `never` when the descriptor has no
|
|
700
|
+
// entityRef, its entity has no `members`, or `members` is widened to
|
|
701
|
+
// `readonly string[]` — this is checked with non-optional property shapes so
|
|
702
|
+
// a descriptor genuinely lacking `entityRef` fails the structural match
|
|
703
|
+
// instead of matching vacuously through the framework type's optional slot.
|
|
704
|
+
type DescriptorEntityMembers<Descriptor> = Descriptor extends {
|
|
705
|
+
readonly entityRef: {
|
|
706
|
+
readonly entity: { readonly members: infer Members extends readonly string[] };
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
? Members
|
|
710
|
+
: never;
|
|
711
|
+
|
|
712
|
+
// The value-set member union for a descriptor-carried entity (the type-level
|
|
713
|
+
// mirror of the runtime's generic `deriveValueSetFromEntity` fold), or
|
|
714
|
+
// `never` for a field with no descriptor, a descriptor with no entityRef.entity,
|
|
715
|
+
// or a widened (non-literal) members tuple — mirroring `EnumValueUnion`'s
|
|
716
|
+
// erasure guard.
|
|
717
|
+
type DescriptorValueSetUnion<FieldState> = [FieldDescriptorOf<FieldState>] extends [never]
|
|
718
|
+
? never
|
|
719
|
+
: readonly string[] extends DescriptorEntityMembers<FieldDescriptorOf<FieldState>>
|
|
720
|
+
? never
|
|
721
|
+
: DescriptorEntityMembers<FieldDescriptorOf<FieldState>>[number];
|
|
722
|
+
|
|
691
723
|
// The codec's `output` / `input` JS type for a field's column, before
|
|
692
724
|
// nullability. `unknown` when the codec is not in the definition's codec map.
|
|
693
725
|
type CodecChannelType<
|
|
@@ -698,21 +730,33 @@ type CodecChannelType<
|
|
|
698
730
|
> = ModelStorageColumn<Definition, ModelName, FieldName>['codecId'] extends infer Id extends
|
|
699
731
|
keyof CodecTypesFromDefinition<Definition>
|
|
700
732
|
? CodecTypesFromDefinition<Definition>[Id] extends { readonly [K in Channel]: infer T }
|
|
701
|
-
?
|
|
733
|
+
? StorageColumnManyOf<ModelStorageColumn<Definition, ModelName, FieldName>> extends true
|
|
734
|
+
? ReadonlyArray<T>
|
|
735
|
+
: T
|
|
702
736
|
: unknown
|
|
703
737
|
: unknown;
|
|
704
738
|
|
|
705
|
-
//
|
|
706
|
-
//
|
|
739
|
+
// The literal value union for a field: the enum-typed union takes precedence
|
|
740
|
+
// (matching today's behavior), falling back to the descriptor-carried
|
|
741
|
+
// value-set union; `never` when neither applies.
|
|
742
|
+
type FieldValueUnion<FieldState> = [EnumValueUnion<FieldState>] extends [never]
|
|
743
|
+
? DescriptorValueSetUnion<FieldState>
|
|
744
|
+
: EnumValueUnion<FieldState>;
|
|
745
|
+
|
|
746
|
+
// A field's read/write JS type: the value union (enum or descriptor value-set)
|
|
747
|
+
// when the field carries one, otherwise the codec channel type, with column
|
|
748
|
+
// nullability applied.
|
|
707
749
|
type FieldChannelType<
|
|
708
750
|
Definition,
|
|
709
751
|
ModelName extends ModelNames<Definition>,
|
|
710
752
|
FieldName extends ModelFieldNames<Definition, ModelName>,
|
|
711
753
|
Channel extends 'output' | 'input',
|
|
712
754
|
> =
|
|
713
|
-
| ([
|
|
755
|
+
| ([FieldValueUnion<ModelFieldState<Definition, ModelName, FieldName>>] extends [never]
|
|
714
756
|
? CodecChannelType<Definition, ModelName, FieldName, Channel>
|
|
715
|
-
:
|
|
757
|
+
: StorageColumnManyOf<ModelStorageColumn<Definition, ModelName, FieldName>> extends true
|
|
758
|
+
? ReadonlyArray<FieldValueUnion<ModelFieldState<Definition, ModelName, FieldName>>>
|
|
759
|
+
: FieldValueUnion<ModelFieldState<Definition, ModelName, FieldName>>)
|
|
716
760
|
| (FieldNullableOf<ModelFieldState<Definition, ModelName, FieldName>> extends true
|
|
717
761
|
? null
|
|
718
762
|
: never);
|
|
@@ -735,6 +779,17 @@ type FieldChannelTypes<Definition, Channel extends 'output' | 'input'> = {
|
|
|
735
779
|
};
|
|
736
780
|
};
|
|
737
781
|
|
|
782
|
+
type StorageColumnChannelTypes<Definition, Channel extends 'output' | 'input'> = {
|
|
783
|
+
readonly [Ns in DefaultStorageNamespaceId<Definition>]: {
|
|
784
|
+
readonly [ModelName in ModelNames<Definition> as BuiltModelTableName<Definition, ModelName>]: {
|
|
785
|
+
readonly [FieldName in ModelFieldNames<Definition, ModelName> as BuiltModelColumnMappings<
|
|
786
|
+
Definition,
|
|
787
|
+
ModelName
|
|
788
|
+
>[FieldName]['column']]: FieldChannelType<Definition, ModelName, FieldName, Channel>;
|
|
789
|
+
};
|
|
790
|
+
};
|
|
791
|
+
};
|
|
792
|
+
|
|
738
793
|
export type SqlContractResult<Definition> = ContractWithTypeMaps<
|
|
739
794
|
Omit<Contract<BuiltStorage<Definition>>, 'domain'> & {
|
|
740
795
|
readonly target: DefinitionTargetId<Definition>;
|
|
@@ -754,6 +809,8 @@ export type SqlContractResult<Definition> = ContractWithTypeMaps<
|
|
|
754
809
|
CodecTypesFromDefinition<Definition>,
|
|
755
810
|
Record<string, never>,
|
|
756
811
|
FieldChannelTypes<Definition, 'output'>,
|
|
757
|
-
FieldChannelTypes<Definition, 'input'
|
|
812
|
+
FieldChannelTypes<Definition, 'input'>,
|
|
813
|
+
StorageColumnChannelTypes<Definition, 'output'>,
|
|
814
|
+
StorageColumnChannelTypes<Definition, 'input'>
|
|
758
815
|
>
|
|
759
816
|
>;
|
package/src/enum-type.ts
CHANGED
|
@@ -1,306 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
readonly name: Name;
|
|
16
|
-
readonly value: Value;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Declare an enum member. The `value` defaults to `name` when omitted. The
|
|
21
|
-
* value is an unconstrained literal here; `enumType` constrains it against the
|
|
22
|
-
* codec's input type. Both generics are preserved as literals so downstream
|
|
23
|
-
* `enumType` carries the value union in its type; the value is serialized to its
|
|
24
|
-
* codec string form only at lowering.
|
|
25
|
-
*/
|
|
26
|
-
export function member<const Name extends string>(name: Name): EnumMember<Name, Name>;
|
|
27
|
-
export function member<const Name extends string, const Value>(
|
|
28
|
-
name: Name,
|
|
29
|
-
value: Value,
|
|
30
|
-
): EnumMember<Name, Value>;
|
|
31
|
-
export function member<const Name extends string, const Value = Name>(
|
|
32
|
-
name: Name,
|
|
33
|
-
value?: Value,
|
|
34
|
-
): EnumMember<Name, Value> {
|
|
35
|
-
return {
|
|
36
|
-
name,
|
|
37
|
-
value: blindCast<
|
|
38
|
-
Value,
|
|
39
|
-
'overload signatures enforce Value=Name when value is omitted; default generic Value=Name makes this safe'
|
|
40
|
-
>(value ?? name),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// Internal types for inferring the literal tuple from the members spread
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
type MembersToValues<Members extends readonly EnumMember<string, unknown>[]> = {
|
|
49
|
-
readonly [K in keyof Members]: Members[K] extends EnumMember<string, infer V> ? V : never;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
type MembersToNames<Members extends readonly EnumMember<string, unknown>[]> = {
|
|
53
|
-
readonly [K in keyof Members]: Members[K] extends EnumMember<infer N, unknown> ? N : never;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
type MembersAccessorMap<Members extends readonly EnumMember<string, unknown>[]> = {
|
|
57
|
-
readonly [M in Members[number] as M['name']]: M['value'];
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// EnumTypeHandle — the authoring handle returned by enumType()
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Internal brand that identifies an EnumTypeHandle in the lowering pipeline.
|
|
66
|
-
* Not exported — callers only interact with `EnumTypeHandle`.
|
|
67
|
-
*/
|
|
68
|
-
export const ENUM_TYPE_HANDLE_BRAND = Symbol('EnumTypeHandle');
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Authoring handle returned by `enumType()`. Carries:
|
|
72
|
-
*
|
|
73
|
-
* - The ordered literal value tuple (`.values`) and name tuple (`.names`)
|
|
74
|
-
* so downstream type-tests can assert literal preservation.
|
|
75
|
-
* - A namespaced member accessor map (`.members`) to avoid collisions with
|
|
76
|
-
* `.values` / `.has` / `.nameOf` / `.ordinalOf`.
|
|
77
|
-
* - Runtime helpers `.has()`, `.nameOf()`, `.ordinalOf()`.
|
|
78
|
-
* - Internal metadata (`enumName`, `codecId`, `nativeType`,
|
|
79
|
-
* `enumMembers`) for the lowering pipeline.
|
|
80
|
-
*
|
|
81
|
-
* The type is generic over the ordered value tuple so callers that assign
|
|
82
|
-
* `const Role = enumType(...)` retain the literal tuple on `.values`.
|
|
83
|
-
*/
|
|
84
|
-
export interface EnumTypeHandle<
|
|
85
|
-
Name extends string = string,
|
|
86
|
-
Values extends readonly unknown[] = readonly unknown[],
|
|
87
|
-
Names extends readonly string[] = readonly string[],
|
|
88
|
-
MembersMap extends Record<string, unknown> = Record<string, unknown>,
|
|
89
|
-
> {
|
|
90
|
-
/** Internal brand for lowering-pipeline detection. */
|
|
91
|
-
readonly [ENUM_TYPE_HANDLE_BRAND]: true;
|
|
92
|
-
|
|
93
|
-
/** The enum's declared name (used as the key in domain `enum` / storage `valueSet`). */
|
|
94
|
-
readonly enumName: Name;
|
|
95
|
-
|
|
96
|
-
/** codecId from the codec passed to `enumType`. */
|
|
97
|
-
readonly codecId: string;
|
|
98
|
-
|
|
99
|
-
/** nativeType from the codec passed to `enumType`. */
|
|
100
|
-
readonly nativeType: string;
|
|
101
|
-
|
|
102
|
-
/** Ordered member list for lowering (name + value pairs). */
|
|
103
|
-
readonly enumMembers: readonly { readonly name: string; readonly value: Values[number] }[];
|
|
104
|
-
|
|
105
|
-
/** Ordered literal value tuple. Declaration order is preserved. */
|
|
106
|
-
readonly values: Values;
|
|
107
|
-
|
|
108
|
-
/** Ordered literal name tuple. Declaration order is preserved. */
|
|
109
|
-
readonly names: Names;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Namespaced accessor map: `Role.members.User === 'user'`.
|
|
113
|
-
* Namespaced under `.members` to avoid collisions with `.values` / `.has`.
|
|
114
|
-
*/
|
|
115
|
-
readonly members: MembersMap;
|
|
116
|
-
|
|
117
|
-
/** Returns `true` if `v` is a declared member value. */
|
|
118
|
-
has(v: Values[number]): boolean;
|
|
119
|
-
|
|
120
|
-
/** Returns the member name for a value, or `undefined` if not found. */
|
|
121
|
-
nameOf(v: Values[number]): string | undefined;
|
|
122
|
-
|
|
123
|
-
/** Returns the zero-based declaration index of a value, or `-1` if not found. */
|
|
124
|
-
ordinalOf(v: Values[number]): number;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
// enumType()
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* A codec typemap: codecId → `{ input, output }`, the same shape the query
|
|
133
|
-
* lanes consume (e.g. `{ 'pg/text@1': { input: string }, 'pg/int4@1': { input: number } }`).
|
|
134
|
-
* The bound `enumType` wrappers supply the target pack's typemap; the core
|
|
135
|
-
* defaults to an empty map (no codec is known), so member values stay
|
|
136
|
-
* unconstrained.
|
|
137
|
-
*/
|
|
138
|
-
export type CodecTypeMap = Record<string, { readonly input?: unknown }>;
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* The application input type the codec dictates for an enum's member values:
|
|
142
|
-
* looks `Codec['codecId']` up in the supplied codec typemap. When the codecId
|
|
143
|
-
* isn't in the map (the core's empty default, or an unknown codec) the input is
|
|
144
|
-
* unconstrained, so any member-value literal is accepted and inferred verbatim.
|
|
145
|
-
*/
|
|
146
|
-
export type CodecInput<
|
|
147
|
-
CodecTypes extends CodecTypeMap,
|
|
148
|
-
Codec extends { readonly codecId: string },
|
|
149
|
-
> = Codec['codecId'] extends keyof CodecTypes
|
|
150
|
-
? CodecTypes[Codec['codecId']] extends { readonly input: infer In }
|
|
151
|
-
? In
|
|
152
|
-
: unknown
|
|
153
|
-
: unknown;
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Declare a domain enum for use in TS-authoring contracts.
|
|
157
|
-
*
|
|
158
|
-
* - The codec is an explicit required argument — the `codecId` and
|
|
159
|
-
* `nativeType` are taken from the passed `ColumnTypeDescriptor` (e.g.
|
|
160
|
-
* `{ codecId: 'pg/text@1', nativeType: 'text' }` from a field preset
|
|
161
|
-
* output or a direct inline object).
|
|
162
|
-
* - `const` generics on the members spread preserve the ordered literal
|
|
163
|
-
* value tuple so `Role.values` is `readonly ['user','admin']`, not
|
|
164
|
-
* `string[]`.
|
|
165
|
-
* - Well-formedness assertions at construction: non-empty member list;
|
|
166
|
-
* unique names; unique values.
|
|
167
|
-
*
|
|
168
|
-
* The returned handle wires into `field.namedType(handle)` to set
|
|
169
|
-
* `valueSet` refs on both the domain field and the storage column.
|
|
170
|
-
*
|
|
171
|
-
* @example
|
|
172
|
-
* ```ts
|
|
173
|
-
* const Role = enumType('Role', { codecId: 'pg/text@1', nativeType: 'text' },
|
|
174
|
-
* member('User', 'user'),
|
|
175
|
-
* member('Admin', 'admin'),
|
|
176
|
-
* );
|
|
177
|
-
* // Role.values → readonly ['user', 'admin']
|
|
178
|
-
* // Role.members.User → 'user'
|
|
179
|
-
* ```
|
|
180
|
-
*/
|
|
181
|
-
export function enumType<
|
|
182
|
-
CodecTypes extends CodecTypeMap = Record<string, never>,
|
|
183
|
-
const Name extends string = string,
|
|
184
|
-
const Codec extends Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'> = Pick<
|
|
185
|
-
ColumnTypeDescriptor,
|
|
186
|
-
'codecId' | 'nativeType'
|
|
187
|
-
>,
|
|
188
|
-
const Members extends readonly [
|
|
189
|
-
EnumMember<string, CodecInput<CodecTypes, Codec>>,
|
|
190
|
-
...EnumMember<string, CodecInput<CodecTypes, Codec>>[],
|
|
191
|
-
] = readonly [EnumMember<string, CodecInput<CodecTypes, Codec>>],
|
|
192
|
-
>(
|
|
193
|
-
name: Name,
|
|
194
|
-
codec: Codec,
|
|
195
|
-
...members: Members
|
|
196
|
-
): EnumTypeHandle<
|
|
197
|
-
Name,
|
|
198
|
-
MembersToValues<[...Members]>,
|
|
199
|
-
MembersToNames<[...Members]>,
|
|
200
|
-
MembersAccessorMap<[...Members]>
|
|
201
|
-
>;
|
|
202
|
-
export function enumType(
|
|
203
|
-
name: string,
|
|
204
|
-
codec: Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
|
|
205
|
-
...members: EnumMember<string, unknown>[]
|
|
206
|
-
): EnumTypeHandle;
|
|
207
|
-
export function enumType(
|
|
208
|
-
name: string,
|
|
209
|
-
codec: Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
|
|
210
|
-
...members: EnumMember<string, unknown>[]
|
|
211
|
-
): EnumTypeHandle {
|
|
212
|
-
if (members.length === 0) {
|
|
213
|
-
throw new Error(`enumType("${name}"): must have at least one member.`);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const seenNames = new Set<string>();
|
|
217
|
-
const seenValues = new Set<string>();
|
|
218
|
-
for (const m of members) {
|
|
219
|
-
if (seenNames.has(m.name)) {
|
|
220
|
-
throw new Error(
|
|
221
|
-
`enumType("${name}"): duplicate member name "${m.name}". Member names must be unique.`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
seenNames.add(m.name);
|
|
225
|
-
|
|
226
|
-
const loweredValue = String(m.value);
|
|
227
|
-
if (seenValues.has(loweredValue)) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
`enumType("${name}"): duplicate member value "${loweredValue}". Member values must be unique.`,
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
seenValues.add(loweredValue);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const values = Object.freeze(members.map((m) => m.value));
|
|
236
|
-
const names = Object.freeze(members.map((m) => m.name));
|
|
237
|
-
const enumMembers = Object.freeze(members.map((m) => ({ name: m.name, value: m.value })));
|
|
238
|
-
|
|
239
|
-
const membersAccessor = Object.freeze(Object.fromEntries(members.map((m) => [m.name, m.value])));
|
|
240
|
-
|
|
241
|
-
const valueSet = new Set(values);
|
|
242
|
-
const valueToName = new Map(members.map((m) => [m.value, m.name]));
|
|
243
|
-
const valueToOrdinal = new Map(values.map((v, i) => [v, i]));
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
[ENUM_TYPE_HANDLE_BRAND]: true,
|
|
247
|
-
enumName: name,
|
|
248
|
-
codecId: codec.codecId,
|
|
249
|
-
nativeType: codec.nativeType,
|
|
250
|
-
enumMembers,
|
|
251
|
-
values,
|
|
252
|
-
names,
|
|
253
|
-
members: membersAccessor,
|
|
254
|
-
has: (v: unknown) => valueSet.has(v),
|
|
255
|
-
nameOf: (v: unknown) => valueToName.get(v),
|
|
256
|
-
ordinalOf: (v: unknown) => valueToOrdinal.get(v) ?? -1,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* The signature of an `enumType` whose codec typemap is already bound — the
|
|
262
|
-
* shape a target-bound wrapper (e.g. `@prisma-next/postgres/contract-builder`)
|
|
263
|
-
* exposes. The member values are constrained to the codec's input type drawn
|
|
264
|
-
* from `CodecTypes` (so a `pg/text@1` codec rejects numeric members, etc.),
|
|
265
|
-
* while `Name`, `Codec`, and the member tuple still infer from the call.
|
|
266
|
-
*/
|
|
267
|
-
export type BoundEnumType<CodecTypes extends CodecTypeMap> = <
|
|
268
|
-
const Name extends string,
|
|
269
|
-
const Codec extends Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
|
|
270
|
-
const Members extends readonly [
|
|
271
|
-
EnumMember<string, CodecInput<CodecTypes, Codec>>,
|
|
272
|
-
...EnumMember<string, CodecInput<CodecTypes, Codec>>[],
|
|
273
|
-
],
|
|
274
|
-
>(
|
|
275
|
-
name: Name,
|
|
276
|
-
codec: Codec,
|
|
277
|
-
...members: Members
|
|
278
|
-
) => EnumTypeHandle<
|
|
279
|
-
Name,
|
|
280
|
-
MembersToValues<[...Members]>,
|
|
281
|
-
MembersToNames<[...Members]>,
|
|
282
|
-
MembersAccessorMap<[...Members]>
|
|
283
|
-
>;
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Bind `enumType` to a target's codec typemap. The returned function is the
|
|
287
|
-
* same runtime `enumType`, retyped so member values are constrained to the
|
|
288
|
-
* codec's input type. Target packages call this with their pack's
|
|
289
|
-
* `ExtractCodecTypesFromPack<Pack>` to expose a codec-aware `enumType`.
|
|
290
|
-
*/
|
|
291
|
-
export function bindEnumType<CodecTypes extends CodecTypeMap>(): BoundEnumType<CodecTypes> {
|
|
292
|
-
return enumType;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Returns true when the value is an `EnumTypeHandle` produced by
|
|
297
|
-
* `enumType()`. Used in the lowering pipeline to detect enum handles
|
|
298
|
-
* in field state without importing the BRAND symbol at every call site.
|
|
299
|
-
*/
|
|
300
|
-
export function isEnumTypeHandle(value: unknown): value is EnumTypeHandle {
|
|
301
|
-
return (
|
|
302
|
-
typeof value === 'object' &&
|
|
303
|
-
value !== null &&
|
|
304
|
-
Reflect.get(value, ENUM_TYPE_HANDLE_BRAND) === true
|
|
305
|
-
);
|
|
306
|
-
}
|
|
1
|
+
export type {
|
|
2
|
+
BoundEnumType,
|
|
3
|
+
CodecInput,
|
|
4
|
+
CodecTypeMap,
|
|
5
|
+
EnumMember,
|
|
6
|
+
EnumTypeHandle,
|
|
7
|
+
} from '@prisma-next/contract-authoring';
|
|
8
|
+
export {
|
|
9
|
+
bindEnumType,
|
|
10
|
+
ENUM_TYPE_HANDLE_BRAND,
|
|
11
|
+
enumType,
|
|
12
|
+
isEnumTypeHandle,
|
|
13
|
+
member,
|
|
14
|
+
} from '@prisma-next/contract-authoring';
|