@prisma-next/mongo-contract-ts 0.14.0-dev.23 → 0.14.0-dev.25

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,6 +1,7 @@
1
1
  import { computeProfileHash, computeStorageHash } from '@prisma-next/contract/hashing';
2
2
  import {
3
3
  type ContractEmbedRelation,
4
+ type ContractEnum,
4
5
  type ContractField,
5
6
  type ContractFieldType,
6
7
  type ContractModelBase,
@@ -9,8 +10,10 @@ import {
9
10
  type ControlPolicy,
10
11
  type CrossReference,
11
12
  crossRef,
13
+ type JsonValue,
12
14
  type ProfileHashBase,
13
15
  type StorageHashBase,
16
+ type ValueSetRef,
14
17
  } from '@prisma-next/contract/types';
15
18
  import {
16
19
  createEntityHelpersFromNamespace,
@@ -53,6 +56,7 @@ import { mongoContractCanonicalizationHooks } from '@prisma-next/mongo-contract/
53
56
  import { canonicalStringify } from '@prisma-next/utils/canonical-stringify';
54
57
  import { blindCast } from '@prisma-next/utils/casts';
55
58
  import { ifDefined } from '@prisma-next/utils/defined';
59
+ import type { EnumTypeHandle } from './enum-type';
56
60
 
57
61
  // `canonicalStringify` rejects non-plain objects so a `Map` or class
58
62
  // instance cannot silently collapse to `{}`. The storage-shape values
@@ -139,13 +143,15 @@ export interface FieldBuilder<
139
143
  Type extends ContractFieldType = ContractFieldType,
140
144
  Nullable extends boolean = boolean,
141
145
  Many extends boolean = boolean,
146
+ Handle extends EnumTypeHandle | undefined = EnumTypeHandle | undefined,
142
147
  > {
143
148
  readonly __kind: 'field';
144
149
  readonly __type: Type;
145
150
  readonly __nullable: Nullable;
146
151
  readonly __many: Many;
147
- optional(): FieldBuilder<Type, true, Many>;
148
- many(): FieldBuilder<Type, Nullable, true>;
152
+ readonly __enumHandle: Handle;
153
+ optional(): FieldBuilder<Type, true, Many, Handle>;
154
+ many(): FieldBuilder<Type, Nullable, true, Handle>;
149
155
  }
150
156
 
151
157
  export interface ValueObjectBuilder<
@@ -220,7 +226,12 @@ export interface ModelBuilder<
220
226
  ): FieldReference<Name, FieldName>;
221
227
  }
222
228
 
223
- type AnyFieldBuilder = FieldBuilder<ContractFieldType, boolean, boolean>;
229
+ type AnyFieldBuilder = FieldBuilder<
230
+ ContractFieldType,
231
+ boolean,
232
+ boolean,
233
+ EnumTypeHandle | undefined
234
+ >;
224
235
  type AnyReferenceRelationBuilder = RelationBuilder<string, '1:1' | '1:N' | 'N:1', RelationOn>;
225
236
  type AnyEmbedRelationBuilder = RelationBuilder<string, '1:1' | '1:N', undefined>;
226
237
  type AnyRelationBuilder = AnyReferenceRelationBuilder | AnyEmbedRelationBuilder;
@@ -486,6 +497,34 @@ type DefinitionValueObjects<Definition> = Definition extends {
486
497
  ? ValueObjects
487
498
  : Record<never, never>;
488
499
 
500
+ type DefinitionEnums<Definition> = Definition extends {
501
+ readonly enums?: infer E;
502
+ }
503
+ ? Present<E> extends Record<string, EnumTypeHandle>
504
+ ? string extends keyof Present<E>
505
+ ? Record<never, never>
506
+ : Present<E>
507
+ : Record<never, never>
508
+ : Record<never, never>;
509
+
510
+ type EnumHandleAccessorType<Handle> =
511
+ Handle extends EnumTypeHandle<infer _Name, infer Values, infer Names, infer MembersMap>
512
+ ? {
513
+ readonly values: Values;
514
+ readonly names: Names;
515
+ readonly members: MembersMap;
516
+ has(v: Values[number]): boolean;
517
+ nameOf(v: Values[number]): string | undefined;
518
+ ordinalOf(v: Values[number]): number;
519
+ }
520
+ : never;
521
+
522
+ type BuiltEnumAccessors<Definition> = {
523
+ readonly [K in keyof DefinitionEnums<Definition>]: EnumHandleAccessorType<
524
+ DefinitionEnums<Definition>[K]
525
+ >;
526
+ };
527
+
489
528
  type DefinitionRoots<Definition> = Definition extends {
490
529
  readonly roots?: infer Roots extends Record<string, ModelNameInput>;
491
530
  }
@@ -527,10 +566,34 @@ type MaybeValueObjectsSection<ValueObjects extends Record<string, AnyValueObject
527
566
  readonly valueObjects: ContractValueObjectsFromRecord<ValueObjects>;
528
567
  };
529
568
 
569
+ // Project EnumTypeHandle to the namespace enum-entry shape.
570
+ // Uses enumMembers (which carries Values[number] literals) rather than
571
+ // ContractEnum.members (which uses JsonValue and erases literals).
572
+ type EnumHandleToEntry<Handle> =
573
+ Handle extends EnumTypeHandle<string, infer Values, infer _Names, infer _MembersMap>
574
+ ? {
575
+ readonly codecId: string;
576
+ readonly members: readonly { readonly name: string; readonly value: Values[number] }[];
577
+ }
578
+ : never;
579
+
580
+ type ContractEnumsFromRecord<Enums extends Record<string, EnumTypeHandle>> = {
581
+ readonly [K in keyof Enums as Enums[K] extends EnumTypeHandle<infer Name>
582
+ ? Name
583
+ : never]: EnumHandleToEntry<Enums[K]>;
584
+ };
585
+
586
+ type MaybeEnumsSection<Enums extends Record<string, EnumTypeHandle>> = keyof Enums extends never
587
+ ? EmptyObject
588
+ : {
589
+ readonly enum: ContractEnumsFromRecord<Enums>;
590
+ };
591
+
530
592
  type MongoDomainNamespaceFromDefinition<Definition> = Simplify<
531
593
  {
532
594
  readonly models: ContractModelsFromRecord<DefinitionModels<Definition>>;
533
- } & MaybeValueObjectsSection<DefinitionValueObjects<Definition>>
595
+ } & MaybeValueObjectsSection<DefinitionValueObjects<Definition>> &
596
+ MaybeEnumsSection<DefinitionEnums<Definition>>
534
597
  >;
535
598
 
536
599
  type MongoContractBaseFromDefinition<Definition> = Simplify<{
@@ -548,14 +611,133 @@ type MongoContractBaseFromDefinition<Definition> = Simplify<{
548
611
  readonly profileHash: ProfileHashBase<string>;
549
612
  readonly meta: Record<string, never>;
550
613
  readonly defaultControlPolicy?: ControlPolicy;
614
+ readonly enumAccessors?: BuiltEnumAccessors<Definition>;
551
615
  }>;
552
616
 
553
617
  type CodecTypesFromDefinition<Definition> = MongoCodecTypes &
554
618
  MergeExtensionCodecTypesSafe<DefinitionExtensionPacks<Definition>>;
555
619
 
620
+ // The enum value union for a field builder — `EnumTypeHandle['values'][number]`
621
+ // when the builder carries an enum handle, `never` otherwise.
622
+ type BuilderEnumValueUnion<TBuilder> =
623
+ TBuilder extends FieldBuilder<
624
+ ContractFieldType,
625
+ boolean,
626
+ boolean,
627
+ infer Handle extends EnumTypeHandle | undefined
628
+ >
629
+ ? [Handle] extends [EnumTypeHandle<string, infer Values>]
630
+ ? readonly unknown[] extends Values
631
+ ? never
632
+ : Values[number]
633
+ : never
634
+ : never;
635
+
636
+ // The base codec/enum/value-object type for a builder field on a given channel,
637
+ // before nullable/many modifiers. Enum fields resolve to the value union on both
638
+ // channels; scalar fields resolve to the codec's channel-specific type.
639
+ type BuilderBaseChannelType<
640
+ TBuilder,
641
+ TValueObjects extends Record<string, AnyValueObjectBuilder>,
642
+ TCodecTypes extends Record<string, { output: unknown; input: unknown }>,
643
+ Channel extends 'output' | 'input',
644
+ > =
645
+ TBuilder extends FieldBuilder<
646
+ infer Type extends ContractFieldType,
647
+ boolean,
648
+ boolean,
649
+ EnumTypeHandle | undefined
650
+ >
651
+ ? [BuilderEnumValueUnion<TBuilder>] extends [never]
652
+ ? Type extends {
653
+ readonly kind: 'scalar';
654
+ readonly codecId: infer CId extends keyof TCodecTypes;
655
+ }
656
+ ? TCodecTypes[CId][Channel]
657
+ : Type extends { readonly kind: 'valueObject'; readonly name: infer VOName extends string }
658
+ ? VOName extends keyof TValueObjects
659
+ ? {
660
+ -readonly [K in keyof ExtractValueObjectFields<
661
+ TValueObjects[VOName]
662
+ >]: BuilderFieldChannelType<
663
+ ExtractValueObjectFields<TValueObjects[VOName]>[K],
664
+ TValueObjects,
665
+ TCodecTypes,
666
+ Channel
667
+ >;
668
+ }
669
+ : unknown
670
+ : unknown
671
+ : BuilderEnumValueUnion<TBuilder>
672
+ : never;
673
+
674
+ type ExtractValueObjectFields<TBuilder> =
675
+ TBuilder extends NamedValueObjectBuilder<string, infer Fields> ? Fields : Record<never, never>;
676
+
677
+ // Runs once per `defineContract` call to build the precomputed `FieldOutputTypes`/`FieldInputTypes`
678
+ // maps. Consumers index those maps in O(1) via `InferModelRow` — this is NOT re-evaluated per query.
679
+ // Recursion is bounded to value-object nesting depth (each level resolves its fields exactly once).
680
+ //
681
+ // The JS type for one field builder on a given channel, with nullable/many applied.
682
+ // Compose many first (array wrapping), then add nullability. This avoids the
683
+ // TypeScript operator-precedence trap where `A | B extends infer X` infers X
684
+ // only from B, not from `A | B`.
685
+ type BuilderFieldChannelType<
686
+ TBuilder,
687
+ TValueObjects extends Record<string, AnyValueObjectBuilder>,
688
+ TCodecTypes extends Record<string, { output: unknown; input: unknown }>,
689
+ Channel extends 'output' | 'input',
690
+ > =
691
+ TBuilder extends FieldBuilder<
692
+ ContractFieldType,
693
+ infer Nullable extends boolean,
694
+ infer Many extends boolean,
695
+ EnumTypeHandle | undefined
696
+ >
697
+ ?
698
+ | (Many extends true
699
+ ? BuilderBaseChannelType<TBuilder, TValueObjects, TCodecTypes, Channel>[]
700
+ : BuilderBaseChannelType<TBuilder, TValueObjects, TCodecTypes, Channel>)
701
+ | (Nullable extends true ? null : never)
702
+ : never;
703
+
704
+ type ExtractModelFields<TBuilder> =
705
+ TBuilder extends NamedModelBuilder<string, infer Fields> ? Fields : Record<never, never>;
706
+
707
+ type FieldChannelTypesFromDefinition<Definition, Channel extends 'output' | 'input'> = {
708
+ readonly [K in typeof UNBOUND_NAMESPACE_ID]: {
709
+ readonly [ModelKey in keyof DefinitionModels<Definition> as ExtractModelName<
710
+ DefinitionModels<Definition>[ModelKey]
711
+ >]: {
712
+ readonly [FieldName in keyof ExtractModelFields<
713
+ DefinitionModels<Definition>[ModelKey]
714
+ >]: BuilderFieldChannelType<
715
+ ExtractModelFields<DefinitionModels<Definition>[ModelKey]>[FieldName],
716
+ DefinitionValueObjects<Definition>,
717
+ CodecTypesFromDefinition<Definition>,
718
+ Channel
719
+ >;
720
+ };
721
+ };
722
+ };
723
+
724
+ type FieldOutputTypesFromDefinition<Definition> = FieldChannelTypesFromDefinition<
725
+ Definition,
726
+ 'output'
727
+ >;
728
+
729
+ type FieldInputTypesFromDefinition<Definition> = FieldChannelTypesFromDefinition<
730
+ Definition,
731
+ 'input'
732
+ >;
733
+
556
734
  export type MongoContractResult<Definition> = MongoContractWithTypeMaps<
557
735
  MongoContractBaseFromDefinition<Definition>,
558
- MongoTypeMaps<CodecTypesFromDefinition<Definition>>
736
+ MongoTypeMaps<
737
+ CodecTypesFromDefinition<Definition>,
738
+ FieldOutputTypesFromDefinition<Definition>,
739
+ FieldInputTypesFromDefinition<Definition>
740
+ >
559
741
  >;
560
742
 
561
743
  type ExtractEntitiesNamespaceFromPack<Pack> = ExtractAuthoringNamespaceFromPack<
@@ -663,6 +845,7 @@ export type ContractDefinition<
663
845
  > = ContractScaffold<Family, Target, ExtensionPacks, Roots> & {
664
846
  readonly models?: Models;
665
847
  readonly valueObjects?: ValueObjects;
848
+ readonly enums?: Record<string, EnumTypeHandle>;
666
849
  };
667
850
 
668
851
  export type ContractFactory<
@@ -692,25 +875,31 @@ function createFieldBuilder<
692
875
  Type extends ContractFieldType,
693
876
  Nullable extends boolean,
694
877
  Many extends boolean,
695
- >(spec: FieldBuilderSpec<Type, Nullable, Many>): FieldBuilder<Type, Nullable, Many> {
878
+ Handle extends EnumTypeHandle | undefined = undefined,
879
+ >(
880
+ spec: FieldBuilderSpec<Type, Nullable, Many>,
881
+ enumHandle?: Handle,
882
+ ): FieldBuilder<Type, Nullable, Many, Handle> {
696
883
  return {
697
884
  __kind: 'field',
698
885
  __type: spec.type,
699
886
  __nullable: spec.nullable,
700
887
  __many: spec.many,
888
+ __enumHandle: blindCast<
889
+ Handle,
890
+ 'optional param widens to Handle | undefined; Handle defaults to undefined when no enum handle is passed'
891
+ >(enumHandle),
701
892
  optional() {
702
- return createFieldBuilder<Type, true, Many>({
703
- type: spec.type,
704
- nullable: true,
705
- many: spec.many,
706
- });
893
+ return createFieldBuilder<Type, true, Many, Handle>(
894
+ { type: spec.type, nullable: true, many: spec.many },
895
+ enumHandle,
896
+ );
707
897
  },
708
898
  many() {
709
- return createFieldBuilder<Type, Nullable, true>({
710
- type: spec.type,
711
- nullable: spec.nullable,
712
- many: true,
713
- });
899
+ return createFieldBuilder<Type, Nullable, true, Handle>(
900
+ { type: spec.type, nullable: spec.nullable, many: true },
901
+ enumHandle,
902
+ );
714
903
  },
715
904
  };
716
905
  }
@@ -791,6 +980,22 @@ export const field = {
791
980
  many: false,
792
981
  });
793
982
  },
983
+ namedType<const Handle extends EnumTypeHandle>(handle: Handle) {
984
+ return createFieldBuilder(
985
+ {
986
+ type: blindCast<
987
+ { readonly kind: 'scalar'; readonly codecId: Handle['codecId'] },
988
+ 'literal narrowing: kind is inferred as string without the cast'
989
+ >({
990
+ kind: 'scalar',
991
+ codecId: handle.codecId,
992
+ }),
993
+ nullable: false,
994
+ many: false,
995
+ },
996
+ handle,
997
+ );
998
+ },
794
999
  } as const;
795
1000
 
796
1001
  export function index<const Fields extends MongoIndexFields>(
@@ -1193,15 +1398,26 @@ function isContractScaffold(
1193
1398
  }
1194
1399
 
1195
1400
  function buildContractField(builder: AnyFieldBuilder): ContractField {
1401
+ const valueSet: ValueSetRef | undefined = builder.__enumHandle
1402
+ ? {
1403
+ plane: 'domain',
1404
+ entityKind: 'enum',
1405
+ namespaceId: UNBOUND_NAMESPACE_ID,
1406
+ entityName: builder.__enumHandle.enumName,
1407
+ }
1408
+ : undefined;
1409
+
1196
1410
  return builder.__many
1197
1411
  ? {
1198
1412
  type: builder.__type,
1199
1413
  nullable: builder.__nullable,
1200
1414
  many: true,
1415
+ ...ifDefined('valueSet', valueSet),
1201
1416
  }
1202
1417
  : {
1203
1418
  type: builder.__type,
1204
1419
  nullable: builder.__nullable,
1420
+ ...ifDefined('valueSet', valueSet),
1205
1421
  };
1206
1422
  }
1207
1423
 
@@ -1343,20 +1559,16 @@ function toStorageCollectionOptions(
1343
1559
  ...(opts.capped
1344
1560
  ? { capped: { size: opts.size ?? 0, ...(opts.max != null && { max: opts.max }) } }
1345
1561
  : {}),
1346
- ...(opts.storageEngine !== undefined && { storageEngine: opts.storageEngine }),
1347
- ...(opts.indexOptionDefaults !== undefined && {
1348
- indexOptionDefaults: opts.indexOptionDefaults,
1349
- }),
1350
- ...(opts.collation !== undefined && { collation: opts.collation }),
1351
- ...(opts.timeseries !== undefined && { timeseries: opts.timeseries }),
1562
+ ...ifDefined('storageEngine', opts.storageEngine),
1563
+ ...ifDefined('indexOptionDefaults', opts.indexOptionDefaults),
1564
+ ...ifDefined('collation', opts.collation),
1565
+ ...ifDefined('timeseries', opts.timeseries),
1352
1566
  ...(opts.clusteredIndex !== undefined && {
1353
1567
  clusteredIndex:
1354
1568
  opts.clusteredIndex.name !== undefined ? { name: opts.clusteredIndex.name } : {},
1355
1569
  }),
1356
- ...(opts.expireAfterSeconds !== undefined && { expireAfterSeconds: opts.expireAfterSeconds }),
1357
- ...(opts.changeStreamPreAndPostImages !== undefined && {
1358
- changeStreamPreAndPostImages: opts.changeStreamPreAndPostImages,
1359
- }),
1570
+ ...ifDefined('expireAfterSeconds', opts.expireAfterSeconds),
1571
+ ...ifDefined('changeStreamPreAndPostImages', opts.changeStreamPreAndPostImages),
1360
1572
  };
1361
1573
  return new MongoCollectionOptions(input);
1362
1574
  }
@@ -1582,6 +1794,37 @@ function buildContractFromDefinition<
1582
1794
  },
1583
1795
  }) as unknown as MongoStorageShape<string>;
1584
1796
 
1797
+ const builtEnums: Record<string, ContractEnum> = {};
1798
+ for (const [enumName, handle] of Object.entries(definition.enums ?? {})) {
1799
+ if (enumName !== handle.enumName) {
1800
+ throw new Error(
1801
+ `enum declaration key "${enumName}" must match enumType name "${handle.enumName}". Aliases are not supported.`,
1802
+ );
1803
+ }
1804
+ builtEnums[enumName] = {
1805
+ codecId: handle.codecId,
1806
+ members: handle.enumMembers.map((m) => ({
1807
+ name: m.name,
1808
+ value: blindCast<
1809
+ JsonValue,
1810
+ 'enum member values are codec inputs (string/number/bool) and are always JsonValue-compatible'
1811
+ >(m.value),
1812
+ })),
1813
+ };
1814
+ }
1815
+ const hasEnums = Object.keys(builtEnums).length > 0;
1816
+
1817
+ for (const [modelName, modelBuilder] of Object.entries(definition.models ?? {})) {
1818
+ for (const [fieldName, fieldBuilder] of Object.entries(modelBuilder.__fields)) {
1819
+ const handle = fieldBuilder.__enumHandle;
1820
+ if (handle && !(handle.enumName in builtEnums)) {
1821
+ throw new Error(
1822
+ `Model "${modelName}" field "${fieldName}" references enum "${handle.enumName}" which is not declared in defineContract({ enums: { ... } }).`,
1823
+ );
1824
+ }
1825
+ }
1826
+ }
1827
+
1585
1828
  const builtContract = {
1586
1829
  target: definition.target.targetId,
1587
1830
  targetFamily: definition.family.familyId,
@@ -1592,6 +1835,7 @@ function buildContractFromDefinition<
1592
1835
  [UNBOUND_NAMESPACE_ID]: {
1593
1836
  models: builtModels,
1594
1837
  ...(Object.keys(builtValueObjects).length > 0 ? { valueObjects: builtValueObjects } : {}),
1838
+ ...(hasEnums ? { enum: builtEnums } : {}),
1595
1839
  },
1596
1840
  },
1597
1841
  },
@@ -1624,6 +1868,7 @@ type BoundDefinitionInput<
1624
1868
  readonly defaultControlPolicy?: ControlPolicy;
1625
1869
  readonly models?: Models;
1626
1870
  readonly valueObjects?: ValueObjects;
1871
+ readonly enums?: Record<string, EnumTypeHandle>;
1627
1872
  };
1628
1873
 
1629
1874
  // Merges a bound input with the pre-bound family/target to produce a full ContractDefinition.
@@ -0,0 +1,14 @@
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';
@@ -2,6 +2,7 @@ export type {
2
2
  ContractDefinition,
3
3
  ContractFactory,
4
4
  ContractScaffold,
5
+ ExtractCodecTypesFromPack,
5
6
  FieldBuilder,
6
7
  FieldReference,
7
8
  ModelBuilder,
@@ -18,3 +19,5 @@ export {
18
19
  rel,
19
20
  valueObject,
20
21
  } from '../contract-builder';
22
+ export type { BoundEnumType, CodecTypeMap, EnumMember, EnumTypeHandle } from '../enum-type';
23
+ export { bindEnumType, enumType, isEnumTypeHandle, member } from '../enum-type';