@prisma-next/sql-contract-ts 0.13.0-dev.1 → 0.13.0-dev.10

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/package.json CHANGED
@@ -1,27 +1,27 @@
1
1
  {
2
2
  "name": "@prisma-next/sql-contract-ts",
3
- "version": "0.13.0-dev.1",
3
+ "version": "0.13.0-dev.10",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "SQL-specific TypeScript contract authoring surface for Prisma Next",
8
8
  "dependencies": {
9
- "@prisma-next/config": "0.13.0-dev.1",
10
- "@prisma-next/contract": "0.13.0-dev.1",
11
- "@prisma-next/contract-authoring": "0.13.0-dev.1",
12
- "@prisma-next/framework-components": "0.13.0-dev.1",
13
- "@prisma-next/sql-contract": "0.13.0-dev.1",
14
- "@prisma-next/utils": "0.13.0-dev.1",
9
+ "@prisma-next/config": "0.13.0-dev.10",
10
+ "@prisma-next/contract": "0.13.0-dev.10",
11
+ "@prisma-next/contract-authoring": "0.13.0-dev.10",
12
+ "@prisma-next/framework-components": "0.13.0-dev.10",
13
+ "@prisma-next/sql-contract": "0.13.0-dev.10",
14
+ "@prisma-next/utils": "0.13.0-dev.10",
15
15
  "arktype": "^2.2.0",
16
16
  "pathe": "^2.0.3",
17
17
  "ts-toolbelt": "^9.6.0"
18
18
  },
19
19
  "devDependencies": {
20
- "@prisma-next/test-utils": "0.13.0-dev.1",
21
- "@prisma-next/tsconfig": "0.13.0-dev.1",
20
+ "@prisma-next/test-utils": "0.13.0-dev.10",
21
+ "@prisma-next/tsconfig": "0.13.0-dev.10",
22
22
  "@types/pg": "8.20.0",
23
23
  "pg": "8.21.0",
24
- "@prisma-next/tsdown": "0.13.0-dev.1",
24
+ "@prisma-next/tsdown": "0.13.0-dev.10",
25
25
  "tsdown": "0.22.1",
26
26
  "typescript": "5.9.3",
27
27
  "vitest": "4.1.8"
@@ -6,7 +6,6 @@ import {
6
6
  import {
7
7
  asNamespaceId,
8
8
  type ColumnDefault,
9
- type ColumnDefaultLiteralInputValue,
10
9
  type Contract,
11
10
  type ContractEnum,
12
11
  type ContractField,
@@ -63,16 +62,15 @@ type DomainFieldRef =
63
62
  | { readonly kind: 'scalar'; readonly many?: boolean }
64
63
  | { readonly kind: 'valueObject'; readonly name: string; readonly many?: boolean };
65
64
 
66
- function encodeDefaultLiteralValue(
67
- value: ColumnDefaultLiteralInputValue,
68
- codecId: string,
69
- codecLookup?: CodecLookup,
70
- ): JsonValue {
65
+ function encodeViaCodec(value: unknown, codecId: string, codecLookup?: CodecLookup): JsonValue {
71
66
  const codec = codecLookup?.get(codecId);
72
67
  if (codec) {
73
68
  return codec.encodeJson(value);
74
69
  }
75
- return value as JsonValue;
70
+ return blindCast<
71
+ JsonValue,
72
+ 'no codec lookup at build time: literal/enum member value is already JSON-safe'
73
+ >(value);
76
74
  }
77
75
 
78
76
  function encodeColumnDefault(
@@ -85,7 +83,7 @@ function encodeColumnDefault(
85
83
  }
86
84
  return {
87
85
  kind: 'literal',
88
- value: encodeDefaultLiteralValue(defaultInput.value, codecId, codecLookup),
86
+ value: encodeViaCodec(defaultInput.value, codecId, codecLookup),
89
87
  };
90
88
  }
91
89
 
@@ -771,7 +769,10 @@ export function buildSqlContractFromDefinition(
771
769
  }
772
770
  domainSlot[enumName] = {
773
771
  codecId: handle.codecId,
774
- members: handle.enumMembers,
772
+ members: handle.enumMembers.map((m) => ({
773
+ name: m.name,
774
+ value: encodeViaCodec(m.value, handle.codecId, codecLookup),
775
+ })),
775
776
  };
776
777
 
777
778
  let storageSlot = storageValueSetsByNs[nsId];
@@ -781,7 +782,7 @@ export function buildSqlContractFromDefinition(
781
782
  }
782
783
  storageSlot[enumName] = {
783
784
  kind: 'value-set',
784
- values: handle.values,
785
+ values: handle.values.map((v) => encodeViaCodec(v, handle.codecId, codecLookup)),
785
786
  };
786
787
  }
787
788
 
@@ -62,6 +62,7 @@ type ContractDefinition<
62
62
  StorageHash extends string | undefined,
63
63
  ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined,
64
64
  Namespaces extends readonly string[] | undefined = undefined,
65
+ Enums extends Record<string, EnumTypeHandle> = Record<string, EnumTypeHandle>,
65
66
  > = {
66
67
  readonly family: Family;
67
68
  readonly target: Target;
@@ -75,7 +76,7 @@ type ContractDefinition<
75
76
  readonly types?: Types;
76
77
  readonly models?: Models;
77
78
  readonly codecLookup?: CodecLookup;
78
- readonly enums?: Record<string, EnumTypeHandle>;
79
+ readonly enums?: Enums;
79
80
  };
80
81
 
81
82
  type ContractScaffold<
@@ -86,6 +87,7 @@ type ContractScaffold<
86
87
  StorageHash extends string | undefined,
87
88
  ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined,
88
89
  Namespaces extends readonly string[] | undefined = undefined,
90
+ Enums extends Record<string, EnumTypeHandle> = Record<string, EnumTypeHandle>,
89
91
  > = {
90
92
  readonly family: Family;
91
93
  readonly target: Target;
@@ -99,7 +101,7 @@ type ContractScaffold<
99
101
  readonly types?: never;
100
102
  readonly models?: never;
101
103
  readonly codecLookup?: CodecLookup;
102
- readonly enums?: Record<string, EnumTypeHandle>;
104
+ readonly enums?: Enums;
103
105
  };
104
106
 
105
107
  type ContractFactory<
@@ -108,9 +110,11 @@ type ContractFactory<
108
110
  Types extends Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
109
111
  Models extends Record<string, ModelLike>,
110
112
  ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
113
+ Enums extends Record<string, EnumTypeHandle> = Record<string, EnumTypeHandle>,
111
114
  > = (helpers: ComposedAuthoringHelpers<Family, Target, ExtensionPacks>) => {
112
115
  readonly types?: Types;
113
116
  readonly models?: Models;
117
+ readonly enums?: Enums;
114
118
  };
115
119
 
116
120
  function validateTargetPackRef(
@@ -324,6 +328,20 @@ type BoundDefinitionInput<
324
328
  readonly enums?: Record<string, EnumTypeHandle>;
325
329
  };
326
330
 
331
+ // A bare `Record<string, EnumTypeHandle>` (no literal keys) is the widened
332
+ // default for a side that declared no enums; drop it so the merge keeps only
333
+ // literally-authored enum handles.
334
+ type LiteralEnums<E extends Record<string, EnumTypeHandle>> = string extends keyof E
335
+ ? Record<never, never>
336
+ : E;
337
+
338
+ // Merges enum handles authored on the scaffold definition with those returned
339
+ // from the factory callback. Either side may be the widened default (empty).
340
+ export type MergeEnums<
341
+ ScaffoldEnums extends Record<string, EnumTypeHandle>,
342
+ FactoryEnums extends Record<string, EnumTypeHandle>,
343
+ > = LiteralEnums<ScaffoldEnums> & LiteralEnums<FactoryEnums>;
344
+
327
345
  // Merges a bound input with the pre-bound family/target to produce a full ContractDefinition.
328
346
  type WithFamilyTarget<
329
347
  Input,
@@ -375,6 +393,7 @@ export function buildBoundContract<
375
393
  const Built extends {
376
394
  readonly types?: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
377
395
  readonly models?: Record<string, ModelLike>;
396
+ readonly enums?: Record<string, EnumTypeHandle>;
378
397
  },
379
398
  >(
380
399
  family: F,
@@ -399,6 +418,7 @@ export function buildBoundContract(
399
418
  ) => {
400
419
  readonly types?: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
401
420
  readonly models?: Record<string, ModelLike>;
421
+ readonly enums?: Record<string, EnumTypeHandle>;
402
422
  })
403
423
  | undefined,
404
424
  ) {
@@ -412,10 +432,12 @@ export function buildBoundContract(
412
432
  extensionPacks: definition.extensionPacks,
413
433
  }),
414
434
  );
435
+ const mergedEnums = { ...(definition.enums ?? {}), ...built.enums };
415
436
  return buildContractFromDsl({
416
437
  ...full,
417
438
  ...ifDefined('types', built.types),
418
439
  ...ifDefined('models', built.models),
440
+ ...ifDefined('enums', Object.keys(mergedEnums).length > 0 ? mergedEnums : undefined),
419
441
  });
420
442
  }
421
443
 
@@ -437,6 +459,7 @@ export function defineContract<
437
459
  const StorageHash extends string | undefined = undefined,
438
460
  const ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined = undefined,
439
461
  const Namespaces extends readonly string[] | undefined = undefined,
462
+ const Enums extends Record<string, EnumTypeHandle> = Record<string, EnumTypeHandle>,
440
463
  >(
441
464
  definition: ContractDefinition<
442
465
  Family,
@@ -447,7 +470,8 @@ export function defineContract<
447
470
  Naming,
448
471
  StorageHash,
449
472
  ForeignKeyDefaults,
450
- Namespaces
473
+ Namespaces,
474
+ Enums
451
475
  >,
452
476
  ): SqlContractResult<
453
477
  ContractDefinition<
@@ -459,7 +483,8 @@ export function defineContract<
459
483
  Naming,
460
484
  StorageHash,
461
485
  ForeignKeyDefaults,
462
- Namespaces
486
+ Namespaces,
487
+ Enums
463
488
  >
464
489
  >;
465
490
  export function defineContract<
@@ -477,6 +502,8 @@ export function defineContract<
477
502
  const StorageHash extends string | undefined = undefined,
478
503
  const ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined = undefined,
479
504
  const Namespaces extends readonly string[] | undefined = undefined,
505
+ const ScaffoldEnums extends Record<string, EnumTypeHandle> = Record<string, EnumTypeHandle>,
506
+ const FactoryEnums extends Record<string, EnumTypeHandle> = Record<string, EnumTypeHandle>,
480
507
  >(
481
508
  definition: ContractScaffold<
482
509
  Family,
@@ -485,9 +512,10 @@ export function defineContract<
485
512
  Naming,
486
513
  StorageHash,
487
514
  ForeignKeyDefaults,
488
- Namespaces
515
+ Namespaces,
516
+ ScaffoldEnums
489
517
  >,
490
- factory: ContractFactory<Family, Target, Types, Models, ExtensionPacks>,
518
+ factory: ContractFactory<Family, Target, Types, Models, ExtensionPacks, FactoryEnums>,
491
519
  ): SqlContractResult<
492
520
  ContractDefinition<
493
521
  Family,
@@ -498,7 +526,8 @@ export function defineContract<
498
526
  Naming,
499
527
  StorageHash,
500
528
  ForeignKeyDefaults,
501
- Namespaces
529
+ Namespaces,
530
+ MergeEnums<ScaffoldEnums, FactoryEnums>
502
531
  >
503
532
  >;
504
533
  export function defineContract(
@@ -17,6 +17,7 @@ import type {
17
17
  } from '@prisma-next/sql-contract/types';
18
18
  import type { UnionToIntersection } from './authoring-type-utils';
19
19
  import type { AttributeStageIdFieldNames, FieldStateOf, ScalarFieldBuilder } from './contract-dsl';
20
+ import type { EnumTypeHandle } from './enum-type';
20
21
 
21
22
  export type ExtractCodecTypesFromPack<P> = P extends { __codecTypes?: infer C }
22
23
  ? C extends Record<string, { output: unknown }>
@@ -333,17 +334,39 @@ type ResolveNamedStorageType<Definition, TypeRef> =
333
334
  : StorageTypeInstance
334
335
  : StorageTypeInstance;
335
336
 
336
- type ResolveFieldDescriptor<Definition, FieldState> = [FieldDescriptorOf<FieldState>] extends [
337
- never,
338
- ]
339
- ? ResolveNamedStorageType<Definition, FieldTypeRefOf<FieldState>>
340
- : FieldDescriptorOf<FieldState>;
337
+ // An enum-typed field carries its `EnumTypeHandle` (an object with a `codecId`
338
+ // and `nativeType`, but no `kind`) as the field's `typeRef`. It is neither a
339
+ // string nor a registered `StorageType`, so the named-type lookup cannot reach
340
+ // it; `EnumFieldHandle` short-circuits the resolvers to read codec + native type
341
+ // straight off the handle, with no column type-ref (the enum name is carried
342
+ // elsewhere). The `[...] extends [never]` guard excludes plain column fields,
343
+ // whose `typeRef` is `never`.
344
+ type EnumFieldHandle<FieldState> = [FieldTypeRefOf<FieldState>] extends [never]
345
+ ? never
346
+ : FieldTypeRefOf<FieldState> extends EnumTypeHandle
347
+ ? FieldTypeRefOf<FieldState>
348
+ : never;
349
+
350
+ type EnumHandleDescriptor<Handle> = Handle extends {
351
+ readonly codecId: infer CodecId extends string;
352
+ readonly nativeType: infer NativeType extends string;
353
+ }
354
+ ? { readonly codecId: CodecId; readonly nativeType: NativeType }
355
+ : never;
341
356
 
342
- type ResolveFieldColumnTypeRef<Definition, FieldState> = [FieldTypeRefOf<FieldState>] extends [
357
+ type ResolveFieldDescriptor<Definition, FieldState> = [EnumFieldHandle<FieldState>] extends [never]
358
+ ? [FieldDescriptorOf<FieldState>] extends [never]
359
+ ? ResolveNamedStorageType<Definition, FieldTypeRefOf<FieldState>>
360
+ : FieldDescriptorOf<FieldState>
361
+ : EnumHandleDescriptor<EnumFieldHandle<FieldState>>;
362
+
363
+ type ResolveFieldColumnTypeRef<Definition, FieldState> = [EnumFieldHandle<FieldState>] extends [
343
364
  never,
344
365
  ]
345
- ? DescriptorTypeRef<FieldDescriptorOf<FieldState>>
346
- : ResolveNamedStorageTypeKey<Definition, FieldTypeRefOf<FieldState>>;
366
+ ? [FieldTypeRefOf<FieldState>] extends [never]
367
+ ? DescriptorTypeRef<FieldDescriptorOf<FieldState>>
368
+ : ResolveNamedStorageTypeKey<Definition, FieldTypeRefOf<FieldState>>
369
+ : undefined;
347
370
 
348
371
  type ResolveFieldColumnTypeParams<Definition, FieldState> = [
349
372
  ResolveFieldColumnTypeRef<Definition, FieldState>,
@@ -558,6 +581,37 @@ type BuiltStorageTables<Definition> = {
558
581
  : Record<string, never>);
559
582
  };
560
583
 
584
+ type DefinitionEnums<Definition> = Definition extends {
585
+ readonly enums?: infer E;
586
+ }
587
+ ? Present<E> extends Record<string, EnumTypeHandle>
588
+ ? // A bare `Record<string, EnumTypeHandle>` (no literal keys) is the
589
+ // widened default for a contract authored without enums; treat it as
590
+ // empty so `db.enums` carries only literally-authored enums.
591
+ string extends keyof Present<E>
592
+ ? Record<never, never>
593
+ : Present<E>
594
+ : Record<never, never>
595
+ : Record<never, never>;
596
+
597
+ type EnumHandleAccessorType<Handle> =
598
+ Handle extends EnumTypeHandle<infer _Name, infer Values, infer Names, infer MembersMap>
599
+ ? {
600
+ readonly values: Values;
601
+ readonly names: Names;
602
+ readonly members: MembersMap;
603
+ has(v: Values[number]): boolean;
604
+ nameOf(v: Values[number]): string | undefined;
605
+ ordinalOf(v: Values[number]): number;
606
+ }
607
+ : never;
608
+
609
+ type BuiltEnumAccessors<Definition> = {
610
+ readonly [K in keyof DefinitionEnums<Definition>]: EnumHandleAccessorType<
611
+ DefinitionEnums<Definition>[K]
612
+ >;
613
+ };
614
+
561
615
  type BuiltDocumentScopedTypes<Definition> = {
562
616
  readonly [K in keyof DefinitionTypes<Definition> as DefinitionTypes<Definition>[K] extends StorageTypeInstance
563
617
  ? K
@@ -610,29 +664,53 @@ type BuiltStorage<Definition> = {
610
664
  };
611
665
  };
612
666
 
613
- type FieldOutputType<
667
+ // The enum value union for an enum-typed field, or `never` for a non-enum
668
+ // field. The field's `typeRef` carries the authored `EnumTypeHandle`, whose
669
+ // `Values` tuple preserves the literal member values (text or numeric).
670
+ type EnumValueUnion<FieldState> = [FieldTypeRefOf<FieldState>] extends [
671
+ EnumTypeHandle<string, infer Values>,
672
+ ]
673
+ ? readonly unknown[] extends Values
674
+ ? never
675
+ : Values[number]
676
+ : never;
677
+
678
+ // The codec's `output` / `input` JS type for a field's column, before
679
+ // nullability. `unknown` when the codec is not in the definition's codec map.
680
+ type CodecChannelType<
614
681
  Definition,
615
682
  ModelName extends ModelNames<Definition>,
616
683
  FieldName extends ModelFieldNames<Definition, ModelName>,
684
+ Channel extends 'output' | 'input',
685
+ > = ModelStorageColumn<Definition, ModelName, FieldName>['codecId'] extends infer Id extends
686
+ keyof CodecTypesFromDefinition<Definition>
687
+ ? CodecTypesFromDefinition<Definition>[Id] extends { readonly [K in Channel]: infer T }
688
+ ? T
689
+ : unknown
690
+ : unknown;
691
+
692
+ // A field's read/write JS type: the enum value union when the field is
693
+ // enum-typed, otherwise the codec channel type, with column nullability applied.
694
+ type FieldChannelType<
695
+ Definition,
696
+ ModelName extends ModelNames<Definition>,
697
+ FieldName extends ModelFieldNames<Definition, ModelName>,
698
+ Channel extends 'output' | 'input',
617
699
  > =
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> = {
700
+ | ([EnumValueUnion<ModelFieldState<Definition, ModelName, FieldName>>] extends [never]
701
+ ? CodecChannelType<Definition, ModelName, FieldName, Channel>
702
+ : EnumValueUnion<ModelFieldState<Definition, ModelName, FieldName>>)
703
+ | (FieldNullableOf<ModelFieldState<Definition, ModelName, FieldName>> extends true
704
+ ? null
705
+ : never);
706
+
707
+ type FieldChannelTypes<Definition, Channel extends 'output' | 'input'> = {
631
708
  readonly [ModelName in ModelNames<Definition>]: {
632
- readonly [FieldName in ModelFieldNames<Definition, ModelName>]: FieldOutputType<
709
+ readonly [FieldName in ModelFieldNames<Definition, ModelName>]: FieldChannelType<
633
710
  Definition,
634
711
  ModelName,
635
- FieldName
712
+ FieldName,
713
+ Channel
636
714
  >;
637
715
  };
638
716
  };
@@ -646,11 +724,12 @@ export type SqlContractResult<Definition> = ContractWithTypeMaps<
646
724
  ? Record<string, never>
647
725
  : DefinitionExtensionPacks<Definition>;
648
726
  readonly capabilities: DerivedCapabilities<Definition>;
727
+ readonly enumAccessors: BuiltEnumAccessors<Definition>;
649
728
  },
650
729
  TypeMaps<
651
730
  CodecTypesFromDefinition<Definition>,
652
731
  Record<string, never>,
653
- Record<string, never>,
654
- FieldOutputTypes<Definition>
732
+ FieldChannelTypes<Definition, 'output'>,
733
+ FieldChannelTypes<Definition, 'input'>
655
734
  >
656
735
  >;
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';