@prisma-next/sql-contract-ts 0.13.0-dev.4 → 0.13.0-dev.40

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.
@@ -1,10 +1,6 @@
1
1
  import type { ColumnTypeDescriptor } from '@prisma-next/framework-components/codec';
2
2
  import type { ExtensionPackRef } from '@prisma-next/framework-components/components';
3
- import {
4
- isPostgresEnumStorageEntry,
5
- type PostgresEnumStorageEntry,
6
- type StorageTypeInstance,
7
- } from '@prisma-next/sql-contract/types';
3
+ import type { StorageTypeInstance } from '@prisma-next/sql-contract/types';
8
4
  import { ifDefined } from '@prisma-next/utils/defined';
9
5
  import type {
10
6
  ContractDefinition,
@@ -59,15 +55,15 @@ type RuntimeModelSpec = {
59
55
  };
60
56
 
61
57
  type RuntimeCollection = {
62
- readonly storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
58
+ readonly storageTypes: Record<string, StorageTypeInstance>;
63
59
  readonly models: Record<string, RuntimeModel>;
64
60
  readonly modelSpecs: ReadonlyMap<string, RuntimeModelSpec>;
65
61
  };
66
62
 
67
63
  function buildStorageTypeReverseLookup(
68
- storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
69
- ): ReadonlyMap<StorageTypeInstance | PostgresEnumStorageEntry, string> {
70
- const lookup = new Map<StorageTypeInstance | PostgresEnumStorageEntry, string>();
64
+ storageTypes: Record<string, StorageTypeInstance>,
65
+ ): ReadonlyMap<StorageTypeInstance, string> {
66
+ const lookup = new Map<StorageTypeInstance, string>();
71
67
  for (const [key, instance] of Object.entries(storageTypes)) {
72
68
  lookup.set(instance, key);
73
69
  }
@@ -78,8 +74,8 @@ function resolveFieldDescriptor(
78
74
  modelName: string,
79
75
  fieldName: string,
80
76
  fieldState: FieldStateOf<ScalarFieldBuilder>,
81
- storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
82
- storageTypeReverseLookup: ReadonlyMap<StorageTypeInstance | PostgresEnumStorageEntry, string>,
77
+ storageTypes: Record<string, StorageTypeInstance>,
78
+ storageTypeReverseLookup: ReadonlyMap<StorageTypeInstance, string>,
83
79
  ): ColumnTypeDescriptor {
84
80
  if ('descriptor' in fieldState && fieldState.descriptor) {
85
81
  return fieldState.descriptor;
@@ -96,9 +92,7 @@ function resolveFieldDescriptor(
96
92
  const typeRef =
97
93
  typeof fieldState.typeRef === 'string'
98
94
  ? fieldState.typeRef
99
- : storageTypeReverseLookup.get(
100
- fieldState.typeRef as StorageTypeInstance | PostgresEnumStorageEntry,
101
- );
95
+ : storageTypeReverseLookup.get(fieldState.typeRef as StorageTypeInstance);
102
96
 
103
97
  if (!typeRef) {
104
98
  throw new Error(
@@ -113,11 +107,8 @@ function resolveFieldDescriptor(
113
107
  );
114
108
  }
115
109
 
116
- const codecId = isPostgresEnumStorageEntry(referencedType)
117
- ? referencedType.codecId
118
- : referencedType.codecId;
119
110
  return {
120
- codecId,
111
+ codecId: referencedType.codecId,
121
112
  nativeType: referencedType.nativeType,
122
113
  typeRef,
123
114
  };
@@ -705,8 +696,8 @@ function resolveForeignKeyNodes(
705
696
  function resolveModelNode(
706
697
  spec: RuntimeModelSpec,
707
698
  allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
708
- storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
709
- storageTypeReverseLookup: ReadonlyMap<StorageTypeInstance | PostgresEnumStorageEntry, string>,
699
+ storageTypes: Record<string, StorageTypeInstance>,
700
+ storageTypeReverseLookup: ReadonlyMap<StorageTypeInstance, string>,
710
701
  extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>,
711
702
  ): ModelNode {
712
703
  const fields: FieldNode[] = [];
@@ -783,10 +774,7 @@ function resolveModelNode(
783
774
  }
784
775
 
785
776
  function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection {
786
- const storageTypes = { ...(definition.types ?? {}) } as Record<
787
- string,
788
- StorageTypeInstance | PostgresEnumStorageEntry
789
- >;
777
+ const storageTypes = { ...(definition.types ?? {}) } as Record<string, StorageTypeInstance>;
790
778
  const models = { ...(definition.models ?? {}) } as Record<string, RuntimeModel>;
791
779
 
792
780
  emitTypedNamedTypeFallbackWarnings(models, storageTypes);
@@ -1,7 +1,9 @@
1
1
  import type {
2
2
  ColumnDefault,
3
3
  Contract,
4
+ ContractEnum,
4
5
  ContractRelation,
6
+ ContractValueObject,
5
7
  NamespaceId,
6
8
  StorageHashBase,
7
9
  } from '@prisma-next/contract/types';
@@ -17,6 +19,7 @@ import type {
17
19
  } from '@prisma-next/sql-contract/types';
18
20
  import type { UnionToIntersection } from './authoring-type-utils';
19
21
  import type { AttributeStageIdFieldNames, FieldStateOf, ScalarFieldBuilder } from './contract-dsl';
22
+ import type { EnumTypeHandle } from './enum-type';
20
23
 
21
24
  export type ExtractCodecTypesFromPack<P> = P extends { __codecTypes?: infer C }
22
25
  ? C extends Record<string, { output: unknown }>
@@ -333,17 +336,39 @@ type ResolveNamedStorageType<Definition, TypeRef> =
333
336
  : StorageTypeInstance
334
337
  : StorageTypeInstance;
335
338
 
336
- type ResolveFieldDescriptor<Definition, FieldState> = [FieldDescriptorOf<FieldState>] extends [
337
- never,
338
- ]
339
- ? ResolveNamedStorageType<Definition, FieldTypeRefOf<FieldState>>
340
- : FieldDescriptorOf<FieldState>;
339
+ // An enum-typed field carries its `EnumTypeHandle` (an object with a `codecId`
340
+ // and `nativeType`, but no `kind`) as the field's `typeRef`. It is neither a
341
+ // string nor a registered `StorageType`, so the named-type lookup cannot reach
342
+ // it; `EnumFieldHandle` short-circuits the resolvers to read codec + native type
343
+ // straight off the handle, with no column type-ref (the enum name is carried
344
+ // elsewhere). The `[...] extends [never]` guard excludes plain column fields,
345
+ // whose `typeRef` is `never`.
346
+ type EnumFieldHandle<FieldState> = [FieldTypeRefOf<FieldState>] extends [never]
347
+ ? never
348
+ : FieldTypeRefOf<FieldState> extends EnumTypeHandle
349
+ ? FieldTypeRefOf<FieldState>
350
+ : never;
351
+
352
+ type EnumHandleDescriptor<Handle> = Handle extends {
353
+ readonly codecId: infer CodecId extends string;
354
+ readonly nativeType: infer NativeType extends string;
355
+ }
356
+ ? { readonly codecId: CodecId; readonly nativeType: NativeType }
357
+ : never;
358
+
359
+ type ResolveFieldDescriptor<Definition, FieldState> = [EnumFieldHandle<FieldState>] extends [never]
360
+ ? [FieldDescriptorOf<FieldState>] extends [never]
361
+ ? ResolveNamedStorageType<Definition, FieldTypeRefOf<FieldState>>
362
+ : FieldDescriptorOf<FieldState>
363
+ : EnumHandleDescriptor<EnumFieldHandle<FieldState>>;
341
364
 
342
- type ResolveFieldColumnTypeRef<Definition, FieldState> = [FieldTypeRefOf<FieldState>] extends [
365
+ type ResolveFieldColumnTypeRef<Definition, FieldState> = [EnumFieldHandle<FieldState>] extends [
343
366
  never,
344
367
  ]
345
- ? DescriptorTypeRef<FieldDescriptorOf<FieldState>>
346
- : ResolveNamedStorageTypeKey<Definition, FieldTypeRefOf<FieldState>>;
368
+ ? [FieldTypeRefOf<FieldState>] extends [never]
369
+ ? DescriptorTypeRef<FieldDescriptorOf<FieldState>>
370
+ : ResolveNamedStorageTypeKey<Definition, FieldTypeRefOf<FieldState>>
371
+ : undefined;
347
372
 
348
373
  type ResolveFieldColumnTypeParams<Definition, FieldState> = [
349
374
  ResolveFieldColumnTypeRef<Definition, FieldState>,
@@ -558,6 +583,37 @@ type BuiltStorageTables<Definition> = {
558
583
  : Record<string, never>);
559
584
  };
560
585
 
586
+ type DefinitionEnums<Definition> = Definition extends {
587
+ readonly enums?: infer E;
588
+ }
589
+ ? Present<E> extends Record<string, EnumTypeHandle>
590
+ ? // A bare `Record<string, EnumTypeHandle>` (no literal keys) is the
591
+ // widened default for a contract authored without enums; treat it as
592
+ // empty so `db.enums` carries only literally-authored enums.
593
+ string extends keyof Present<E>
594
+ ? Record<never, never>
595
+ : Present<E>
596
+ : Record<never, never>
597
+ : Record<never, never>;
598
+
599
+ type EnumHandleAccessorType<Handle> =
600
+ Handle extends EnumTypeHandle<infer _Name, infer Values, infer Names, infer MembersMap>
601
+ ? {
602
+ readonly values: Values;
603
+ readonly names: Names;
604
+ readonly members: MembersMap;
605
+ has(v: Values[number]): boolean;
606
+ nameOf(v: Values[number]): string | undefined;
607
+ ordinalOf(v: Values[number]): number;
608
+ }
609
+ : never;
610
+
611
+ type BuiltEnumAccessors<Definition> = {
612
+ readonly [K in keyof DefinitionEnums<Definition>]: EnumHandleAccessorType<
613
+ DefinitionEnums<Definition>[K]
614
+ >;
615
+ };
616
+
561
617
  type BuiltDocumentScopedTypes<Definition> = {
562
618
  readonly [K in keyof DefinitionTypes<Definition> as DefinitionTypes<Definition>[K] extends StorageTypeInstance
563
619
  ? K
@@ -573,6 +629,17 @@ type BuiltDomain<Definition> =
573
629
  };
574
630
  };
575
631
 
632
+ // Per-namespace domain entry carrying the precise per-model field/storage shapes
633
+ // for DSL inference. Modelled as an index signature (rather than enumerating
634
+ // namespace ids) so that any namespace coordinate resolves the full model map,
635
+ // matching how the authoring path lumps every model under the default storage
636
+ // namespace.
637
+ type BuiltDomainNamespace<Definition> = {
638
+ readonly models: BuiltModels<Definition>;
639
+ readonly valueObjects?: Record<string, ContractValueObject>;
640
+ readonly enum?: Record<string, ContractEnum>;
641
+ };
642
+
576
643
  type DefaultStorageNamespaceId<Definition> =
577
644
  DefinitionTargetId<Definition> extends 'postgres' ? 'public' : '__unbound__';
578
645
 
@@ -610,47 +677,83 @@ type BuiltStorage<Definition> = {
610
677
  };
611
678
  };
612
679
 
613
- type FieldOutputType<
680
+ // The enum value union for an enum-typed field, or `never` for a non-enum
681
+ // field. The field's `typeRef` carries the authored `EnumTypeHandle`, whose
682
+ // `Values` tuple preserves the literal member values (text or numeric).
683
+ type EnumValueUnion<FieldState> = [FieldTypeRefOf<FieldState>] extends [
684
+ EnumTypeHandle<string, infer Values>,
685
+ ]
686
+ ? readonly unknown[] extends Values
687
+ ? never
688
+ : Values[number]
689
+ : never;
690
+
691
+ // The codec's `output` / `input` JS type for a field's column, before
692
+ // nullability. `unknown` when the codec is not in the definition's codec map.
693
+ type CodecChannelType<
614
694
  Definition,
615
695
  ModelName extends ModelNames<Definition>,
616
696
  FieldName extends ModelFieldNames<Definition, ModelName>,
697
+ Channel extends 'output' | 'input',
698
+ > = ModelStorageColumn<Definition, ModelName, FieldName>['codecId'] extends infer Id extends
699
+ keyof CodecTypesFromDefinition<Definition>
700
+ ? CodecTypesFromDefinition<Definition>[Id] extends { readonly [K in Channel]: infer T }
701
+ ? T
702
+ : unknown
703
+ : unknown;
704
+
705
+ // A field's read/write JS type: the enum value union when the field is
706
+ // enum-typed, otherwise the codec channel type, with column nullability applied.
707
+ type FieldChannelType<
708
+ Definition,
709
+ ModelName extends ModelNames<Definition>,
710
+ FieldName extends ModelFieldNames<Definition, ModelName>,
711
+ Channel extends 'output' | 'input',
617
712
  > =
618
- ModelStorageColumn<Definition, ModelName, FieldName> extends infer Col
619
- ? Col extends { readonly codecId: infer Id extends string }
620
- ? Id extends keyof CodecTypesFromDefinition<Definition>
621
- ? CodecTypesFromDefinition<Definition>[Id] extends { readonly output: infer O }
622
- ? Col extends { readonly nullable: true }
623
- ? O | null
624
- : O
625
- : unknown
626
- : unknown
627
- : unknown
628
- : unknown;
629
-
630
- type FieldOutputTypes<Definition> = {
631
- readonly [ModelName in ModelNames<Definition>]: {
632
- readonly [FieldName in ModelFieldNames<Definition, ModelName>]: FieldOutputType<
633
- Definition,
634
- ModelName,
635
- FieldName
636
- >;
713
+ | ([EnumValueUnion<ModelFieldState<Definition, ModelName, FieldName>>] extends [never]
714
+ ? CodecChannelType<Definition, ModelName, FieldName, Channel>
715
+ : EnumValueUnion<ModelFieldState<Definition, ModelName, FieldName>>)
716
+ | (FieldNullableOf<ModelFieldState<Definition, ModelName, FieldName>> extends true
717
+ ? null
718
+ : never);
719
+
720
+ // Nested by namespace coordinate (`{ [ns]: { [model]: { [field]: type } } }`)
721
+ // to mirror the emitter's namespace-nested `FieldOutputTypes` (and the
722
+ // `TypeMaps` constraint). The TS authoring path lumps every model under the
723
+ // target's default storage namespace (see `BuiltStorage`), so the per-model
724
+ // field-type map nests under that same coordinate.
725
+ type FieldChannelTypes<Definition, Channel extends 'output' | 'input'> = {
726
+ readonly [Ns in DefaultStorageNamespaceId<Definition>]: {
727
+ readonly [ModelName in ModelNames<Definition>]: {
728
+ readonly [FieldName in ModelFieldNames<Definition, ModelName>]: FieldChannelType<
729
+ Definition,
730
+ ModelName,
731
+ FieldName,
732
+ Channel
733
+ >;
734
+ };
637
735
  };
638
736
  };
639
737
 
640
738
  export type SqlContractResult<Definition> = ContractWithTypeMaps<
641
- Contract<BuiltStorage<Definition>, BuiltModels<Definition>> & {
739
+ Omit<Contract<BuiltStorage<Definition>>, 'domain'> & {
642
740
  readonly target: DefinitionTargetId<Definition>;
643
741
  readonly targetFamily: 'sql';
644
- } & { readonly domain: BuiltDomain<Definition> } & {
742
+ } & {
743
+ readonly domain: {
744
+ readonly namespaces: Readonly<Record<string, BuiltDomainNamespace<Definition>>>;
745
+ } & BuiltDomain<Definition>;
746
+ } & {
645
747
  readonly extensionPacks: keyof DefinitionExtensionPacks<Definition> extends never
646
748
  ? Record<string, never>
647
749
  : DefinitionExtensionPacks<Definition>;
648
750
  readonly capabilities: DerivedCapabilities<Definition>;
751
+ readonly enumAccessors: BuiltEnumAccessors<Definition>;
649
752
  },
650
753
  TypeMaps<
651
754
  CodecTypesFromDefinition<Definition>,
652
755
  Record<string, never>,
653
- Record<string, never>,
654
- FieldOutputTypes<Definition>
756
+ FieldChannelTypes<Definition, 'output'>,
757
+ FieldChannelTypes<Definition, 'input'>
655
758
  >
656
759
  >;
@@ -1,7 +1,4 @@
1
- import type {
2
- PostgresEnumStorageEntry,
3
- StorageTypeInstance,
4
- } from '@prisma-next/sql-contract/types';
1
+ import type { StorageTypeInstance } from '@prisma-next/sql-contract/types';
5
2
  import {
6
3
  type ContractModelBuilder,
7
4
  type ModelAttributesSpec,
@@ -28,7 +25,7 @@ type RuntimeModelSpec = {
28
25
  };
29
26
 
30
27
  type RuntimeCollection = {
31
- readonly storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
28
+ readonly storageTypes: Record<string, StorageTypeInstance>;
32
29
  readonly models: Record<string, RuntimeModel>;
33
30
  readonly modelSpecs: ReadonlyMap<string, RuntimeModelSpec>;
34
31
  };
@@ -136,7 +133,7 @@ function formatFallbackWarning(location: string, current: string, suggested: str
136
133
 
137
134
  export function emitTypedNamedTypeFallbackWarnings(
138
135
  models: Record<string, RuntimeModel>,
139
- storageTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
136
+ storageTypes: Record<string, StorageTypeInstance>,
140
137
  ): void {
141
138
  const warnings: string[] = [];
142
139
  const warnedFields = new Set<string>();
package/src/enum-type.ts CHANGED
@@ -8,24 +8,27 @@ import { blindCast } from '@prisma-next/utils/casts';
8
8
  /**
9
9
  * A single enum member produced by `member()`. The `Name` and `Value` generics
10
10
  * are preserved as literal types so `enumType()` can carry the ordered value
11
- * tuple in its return type.
11
+ * tuple in its return type. `Value` is whatever the codec dictates — its type
12
+ * is constrained at `enumType` against the codec's input type, not here.
12
13
  */
13
- export interface EnumMember<Name extends string, Value extends string> {
14
+ export interface EnumMember<Name extends string, Value> {
14
15
  readonly name: Name;
15
16
  readonly value: Value;
16
17
  }
17
18
 
18
19
  /**
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.
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.
22
25
  */
23
26
  export function member<const Name extends string>(name: Name): EnumMember<Name, Name>;
24
- export function member<const Name extends string, const Value extends string>(
27
+ export function member<const Name extends string, const Value>(
25
28
  name: Name,
26
29
  value: Value,
27
30
  ): EnumMember<Name, Value>;
28
- export function member<const Name extends string, const Value extends string = Name>(
31
+ export function member<const Name extends string, const Value = Name>(
29
32
  name: Name,
30
33
  value?: Value,
31
34
  ): EnumMember<Name, Value> {
@@ -42,15 +45,15 @@ export function member<const Name extends string, const Value extends string = N
42
45
  // Internal types for inferring the literal tuple from the members spread
43
46
  // ---------------------------------------------------------------------------
44
47
 
45
- type MembersToValues<Members extends readonly EnumMember<string, string>[]> = {
48
+ type MembersToValues<Members extends readonly EnumMember<string, unknown>[]> = {
46
49
  readonly [K in keyof Members]: Members[K] extends EnumMember<string, infer V> ? V : never;
47
50
  };
48
51
 
49
- type MembersToNames<Members extends readonly EnumMember<string, string>[]> = {
50
- readonly [K in keyof Members]: Members[K] extends EnumMember<infer N, string> ? N : never;
52
+ type MembersToNames<Members extends readonly EnumMember<string, unknown>[]> = {
53
+ readonly [K in keyof Members]: Members[K] extends EnumMember<infer N, unknown> ? N : never;
51
54
  };
52
55
 
53
- type MembersAccessorMap<Members extends readonly EnumMember<string, string>[]> = {
56
+ type MembersAccessorMap<Members extends readonly EnumMember<string, unknown>[]> = {
54
57
  readonly [M in Members[number] as M['name']]: M['value'];
55
58
  };
56
59
 
@@ -80,9 +83,9 @@ export const ENUM_TYPE_HANDLE_BRAND = Symbol('EnumTypeHandle');
80
83
  */
81
84
  export interface EnumTypeHandle<
82
85
  Name extends string = string,
83
- Values extends readonly string[] = readonly string[],
86
+ Values extends readonly unknown[] = readonly unknown[],
84
87
  Names extends readonly string[] = readonly string[],
85
- MembersMap extends Record<string, string> = Record<string, string>,
88
+ MembersMap extends Record<string, unknown> = Record<string, unknown>,
86
89
  > {
87
90
  /** Internal brand for lowering-pipeline detection. */
88
91
  readonly [ENUM_TYPE_HANDLE_BRAND]: true;
@@ -97,7 +100,7 @@ export interface EnumTypeHandle<
97
100
  readonly nativeType: string;
98
101
 
99
102
  /** Ordered member list for lowering (name + value pairs). */
100
- readonly enumMembers: readonly { readonly name: string; readonly value: string }[];
103
+ readonly enumMembers: readonly { readonly name: string; readonly value: Values[number] }[];
101
104
 
102
105
  /** Ordered literal value tuple. Declaration order is preserved. */
103
106
  readonly values: Values;
@@ -112,19 +115,43 @@ export interface EnumTypeHandle<
112
115
  readonly members: MembersMap;
113
116
 
114
117
  /** Returns `true` if `v` is a declared member value. */
115
- has(v: string): boolean;
118
+ has(v: Values[number]): boolean;
116
119
 
117
120
  /** Returns the member name for a value, or `undefined` if not found. */
118
- nameOf(v: string): string | undefined;
121
+ nameOf(v: Values[number]): string | undefined;
119
122
 
120
123
  /** Returns the zero-based declaration index of a value, or `-1` if not found. */
121
- ordinalOf(v: string): number;
124
+ ordinalOf(v: Values[number]): number;
122
125
  }
123
126
 
124
127
  // ---------------------------------------------------------------------------
125
128
  // enumType()
126
129
  // ---------------------------------------------------------------------------
127
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
+
128
155
  /**
129
156
  * Declare a domain enum for use in TS-authoring contracts.
130
157
  *
@@ -152,9 +179,16 @@ export interface EnumTypeHandle<
152
179
  * ```
153
180
  */
154
181
  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>[]],
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>>],
158
192
  >(
159
193
  name: Name,
160
194
  codec: Codec,
@@ -168,12 +202,12 @@ export function enumType<
168
202
  export function enumType(
169
203
  name: string,
170
204
  codec: Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
171
- ...members: EnumMember<string, string>[]
205
+ ...members: EnumMember<string, unknown>[]
172
206
  ): EnumTypeHandle;
173
207
  export function enumType(
174
208
  name: string,
175
209
  codec: Pick<ColumnTypeDescriptor, 'codecId' | 'nativeType'>,
176
- ...members: EnumMember<string, string>[]
210
+ ...members: EnumMember<string, unknown>[]
177
211
  ): EnumTypeHandle {
178
212
  if (members.length === 0) {
179
213
  throw new Error(`enumType("${name}"): must have at least one member.`);
@@ -189,12 +223,13 @@ export function enumType(
189
223
  }
190
224
  seenNames.add(m.name);
191
225
 
192
- if (seenValues.has(m.value)) {
226
+ const loweredValue = String(m.value);
227
+ if (seenValues.has(loweredValue)) {
193
228
  throw new Error(
194
- `enumType("${name}"): duplicate member value "${m.value}". Member values must be unique.`,
229
+ `enumType("${name}"): duplicate member value "${loweredValue}". Member values must be unique.`,
195
230
  );
196
231
  }
197
- seenValues.add(m.value);
232
+ seenValues.add(loweredValue);
198
233
  }
199
234
 
200
235
  const values = Object.freeze(members.map((m) => m.value));
@@ -216,12 +251,47 @@ export function enumType(
216
251
  values,
217
252
  names,
218
253
  members: membersAccessor,
219
- has: (v: string) => valueSet.has(v),
220
- nameOf: (v: string) => valueToName.get(v),
221
- ordinalOf: (v: string) => valueToOrdinal.get(v) ?? -1,
254
+ has: (v: unknown) => valueSet.has(v),
255
+ nameOf: (v: unknown) => valueToName.get(v),
256
+ ordinalOf: (v: unknown) => valueToOrdinal.get(v) ?? -1,
222
257
  };
223
258
  }
224
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
+
225
295
  /**
226
296
  * Returns true when the value is an `EnumTypeHandle` produced by
227
297
  * `enumType()`. Used in the lowering pipeline to detect enum handles
@@ -2,6 +2,7 @@ export type {
2
2
  ComposedAuthoringHelpers,
3
3
  ContractInput,
4
4
  ContractModelBuilder,
5
+ MergeEnums,
5
6
  ModelLike,
6
7
  ScalarFieldBuilder,
7
8
  } from '../contract-builder';
@@ -25,5 +26,12 @@ export type {
25
26
  UniqueConstraintNode,
26
27
  } from '../contract-definition';
27
28
  export type { TargetFieldRef } from '../contract-dsl';
28
- export type { EnumMember, EnumTypeHandle } from '../enum-type';
29
- export { enumType, member } from '../enum-type';
29
+ export type { ExtractCodecTypesFromPack } from '../contract-types';
30
+ export type {
31
+ BoundEnumType,
32
+ CodecInput,
33
+ CodecTypeMap,
34
+ EnumMember,
35
+ EnumTypeHandle,
36
+ } from '../enum-type';
37
+ export { bindEnumType, enumType, member } from '../enum-type';