@prisma-next/mongo-contract-psl 0.14.0-dev.3 → 0.14.0-dev.31

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.
@@ -5,12 +5,24 @@ import type {
5
5
  import { computeProfileHash, computeStorageHash } from '@prisma-next/contract/hashing';
6
6
  import {
7
7
  type Contract,
8
+ type ContractEnum,
8
9
  type ContractField,
9
10
  type ContractReferenceRelation,
10
11
  type ContractValueObject,
11
12
  type CrossReference,
12
13
  crossRef,
14
+ type JsonValue,
15
+ type ValueSetRef,
13
16
  } from '@prisma-next/contract/types';
17
+ import type { EnumTypeHandle } from '@prisma-next/contract-authoring';
18
+ import type {
19
+ AuthoringContributions,
20
+ AuthoringEntityContext,
21
+ } from '@prisma-next/framework-components/authoring';
22
+ import {
23
+ instantiateAuthoringEntityType,
24
+ isAuthoringEntityTypeDescriptor,
25
+ } from '@prisma-next/framework-components/authoring';
14
26
  import type { CodecLookup } from '@prisma-next/framework-components/codec';
15
27
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
16
28
  import {
@@ -24,13 +36,19 @@ import {
24
36
  import { mongoContractCanonicalizationHooks } from '@prisma-next/mongo-contract/canonicalization-hooks';
25
37
  import type { CollationOptions } from '@prisma-next/mongo-value/mongodb-types';
26
38
  import type {
27
- ParsePslDocumentResult,
28
- PslField,
29
- PslModel,
30
- PslNamespace,
39
+ CompositeTypeSymbol,
40
+ FieldSymbol,
41
+ ModelSymbol,
42
+ NamespaceSymbol,
43
+ PslExtensionBlock,
31
44
  PslSpan,
45
+ ResolvedAttribute,
46
+ SymbolTable,
32
47
  } from '@prisma-next/psl-parser';
48
+ import { nodePslSpan } from '@prisma-next/psl-parser';
49
+ import type { SourceFile } from '@prisma-next/psl-parser/syntax';
33
50
  import { blindCast } from '@prisma-next/utils/casts';
51
+ import { ifDefined } from '@prisma-next/utils/defined';
34
52
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
35
53
  import { deriveJsonSchema, derivePolymorphicJsonSchema } from './derive-json-schema';
36
54
  import {
@@ -45,41 +63,31 @@ import {
45
63
  } from './psl-helpers';
46
64
 
47
65
  export interface InterpretPslDocumentToMongoContractInput {
48
- readonly document: ParsePslDocumentResult;
66
+ readonly symbolTable: SymbolTable;
67
+ readonly sourceFile: SourceFile;
68
+ readonly sourceId: string;
49
69
  readonly scalarTypeDescriptors: ReadonlyMap<string, string>;
50
70
  readonly codecLookup?: CodecLookup;
71
+ readonly seedDiagnostics?: readonly ContractSourceDiagnostic[];
72
+ readonly authoringContributions?: AuthoringContributions;
51
73
  }
52
74
 
53
75
  /**
54
- * Name of the framework-parser synthesised bucket for top-level
55
- * declarations. Re-declared locally so the interpreter does not have to
56
- * import from `@prisma-next/framework-components/psl-ast`.
57
- */
58
- const UNSPECIFIED_PSL_NAMESPACE_NAME = '__unspecified__';
59
-
60
- /**
61
- * Mongo FR16c validation: Mongo's authoring DSL exposes the connection's
62
- * database as the only namespace surface today, so the PSL interpreter
63
- * rejects every explicit `namespace { … }` block. The implicit
64
- * `__unspecified__` bucket (top-level declarations) is the only
65
- * namespace Mongo accepts. `namespace unbound { … }` is rejected too —
66
- * Mongo has no late-binding namespace concept on the PSL surface (the
67
- * database name comes from the connection string, not from PSL).
76
+ * Mongo's PSL surface binds the database from the connection string, so every
77
+ * explicit namespace block is invalid, including `namespace unbound { … }`.
68
78
  */
69
79
  function validateNamespaceBlocksForMongoTarget(input: {
70
- readonly namespaces: readonly PslNamespace[];
80
+ readonly namespaces: readonly NamespaceSymbol[];
71
81
  readonly sourceId: string;
82
+ readonly sourceFile: SourceFile;
72
83
  readonly diagnostics: ContractSourceDiagnostic[];
73
84
  }): void {
74
85
  for (const namespace of input.namespaces) {
75
- if (namespace.name === UNSPECIFIED_PSL_NAMESPACE_NAME) {
76
- continue;
77
- }
78
86
  input.diagnostics.push({
79
87
  code: 'PSL_UNSUPPORTED_NAMESPACE_BLOCK',
80
88
  message: `Mongo does not support \`namespace ${namespace.name} { … }\` blocks (the database is bound by the connection string; declare models at the document top level instead).`,
81
89
  sourceId: input.sourceId,
82
- span: namespace.span,
90
+ span: nodePslSpan(namespace.node.syntax, input.sourceFile),
83
91
  });
84
92
  }
85
93
  }
@@ -101,16 +109,16 @@ function fkRelationPairKey(declaringModel: string, targetModel: string): string
101
109
  return `${declaringModel}::${targetModel}`;
102
110
  }
103
111
 
104
- function resolveFieldMappings(model: PslModel): FieldMappings {
112
+ function resolveFieldMappings(model: ModelSymbol): FieldMappings {
105
113
  const pslNameToMapped = new Map<string, string>();
106
- for (const field of model.fields) {
114
+ for (const field of Object.values(model.fields)) {
107
115
  const mapped = getMapName(field.attributes) ?? field.name;
108
116
  pslNameToMapped.set(field.name, mapped);
109
117
  }
110
118
  return { pslNameToMapped };
111
119
  }
112
120
 
113
- function resolveCollectionName(model: PslModel): string {
121
+ function resolveCollectionName(model: ModelSymbol): string {
114
122
  return getMapName(model.attributes) ?? lowerFirst(model.name);
115
123
  }
116
124
 
@@ -123,12 +131,12 @@ interface MongoModelEntry {
123
131
  readonly base?: CrossReference;
124
132
  }
125
133
 
126
- type DiscriminatorDeclaration = { readonly fieldName: string; readonly span: PslModel['span'] };
134
+ type DiscriminatorDeclaration = { readonly fieldName: string; readonly span: PslSpan };
127
135
  type BaseDeclaration = {
128
136
  readonly baseName: string;
129
137
  readonly value: string;
130
138
  readonly collectionName: string;
131
- readonly span: PslModel['span'];
139
+ readonly span: PslSpan;
132
140
  };
133
141
 
134
142
  function mongoCrossRef(modelName: string): CrossReference {
@@ -136,7 +144,7 @@ function mongoCrossRef(modelName: string): CrossReference {
136
144
  }
137
145
 
138
146
  function collectPolymorphismDeclarations(
139
- document: ParsePslDocumentResult,
147
+ models: readonly ModelSymbol[],
140
148
  sourceId: string,
141
149
  diagnostics: ContractSourceDiagnostic[],
142
150
  ): {
@@ -145,32 +153,31 @@ function collectPolymorphismDeclarations(
145
153
  } {
146
154
  const discriminatorDeclarations = new Map<string, DiscriminatorDeclaration>();
147
155
  const baseDeclarations = new Map<string, BaseDeclaration>();
148
- const allPslModels = document.ast.namespaces.flatMap((ns) => ns.models);
149
156
 
150
- for (const pslModel of allPslModels) {
151
- for (const attr of pslModel.attributes) {
157
+ for (const model of models) {
158
+ for (const attr of model.attributes) {
152
159
  if (attr.name === 'discriminator') {
153
160
  const fieldName = getPositionalArgument(attr);
154
161
  if (!fieldName) {
155
162
  diagnostics.push({
156
163
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
157
- message: `Model "${pslModel.name}" @@discriminator requires a field name argument`,
164
+ message: `Model "${model.name}" @@discriminator requires a field name argument`,
158
165
  sourceId,
159
166
  span: attr.span,
160
167
  });
161
168
  continue;
162
169
  }
163
- const discField = pslModel.fields.find((f) => f.name === fieldName);
170
+ const discField = model.fields[fieldName];
164
171
  if (discField && discField.typeName !== 'String') {
165
172
  diagnostics.push({
166
173
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
167
- message: `Discriminator field "${fieldName}" on model "${pslModel.name}" must be of type String, but is "${discField.typeName}"`,
174
+ message: `Discriminator field "${fieldName}" on model "${model.name}" must be of type String, but is "${discField.typeName}"`,
168
175
  sourceId,
169
176
  span: attr.span,
170
177
  });
171
178
  continue;
172
179
  }
173
- discriminatorDeclarations.set(pslModel.name, { fieldName, span: attr.span });
180
+ discriminatorDeclarations.set(model.name, { fieldName, span: attr.span });
174
181
  }
175
182
  if (attr.name === 'base') {
176
183
  const baseName = getPositionalArgument(attr, 0);
@@ -178,7 +185,7 @@ function collectPolymorphismDeclarations(
178
185
  if (!baseName || !rawValue) {
179
186
  diagnostics.push({
180
187
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
181
- message: `Model "${pslModel.name}" @@base requires two arguments: base model name and discriminator value`,
188
+ message: `Model "${model.name}" @@base requires two arguments: base model name and discriminator value`,
182
189
  sourceId,
183
190
  span: attr.span,
184
191
  });
@@ -188,14 +195,14 @@ function collectPolymorphismDeclarations(
188
195
  if (value === undefined) {
189
196
  diagnostics.push({
190
197
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
191
- message: `Model "${pslModel.name}" @@base discriminator value must be a quoted string literal`,
198
+ message: `Model "${model.name}" @@base discriminator value must be a quoted string literal`,
192
199
  sourceId,
193
200
  span: attr.span,
194
201
  });
195
202
  continue;
196
203
  }
197
- const collectionName = resolveCollectionName(pslModel);
198
- baseDeclarations.set(pslModel.name, { baseName, value, collectionName, span: attr.span });
204
+ const collectionName = resolveCollectionName(model);
205
+ baseDeclarations.set(model.name, { baseName, value, collectionName, span: attr.span });
199
206
  }
200
207
  }
201
208
  }
@@ -207,7 +214,7 @@ function resolvePolymorphism(input: {
207
214
  models: Record<string, MongoModelEntry>;
208
215
  roots: Record<string, CrossReference>;
209
216
  collections: Record<string, Record<string, unknown>>;
210
- document: ParsePslDocumentResult;
217
+ allModels: readonly ModelSymbol[];
211
218
  discriminatorDeclarations: Map<string, DiscriminatorDeclaration>;
212
219
  baseDeclarations: Map<string, BaseDeclaration>;
213
220
  modelNames: ReadonlySet<string>;
@@ -225,11 +232,10 @@ function resolvePolymorphism(input: {
225
232
  baseDeclarations,
226
233
  modelNames,
227
234
  sourceId,
228
- document,
235
+ allModels: allModelViews,
229
236
  indexSpans,
230
237
  modelIndexesByName,
231
238
  } = input;
232
- const allPslModels = document.ast.namespaces.flatMap((ns) => ns.models);
233
239
  let patched = input.models;
234
240
  let roots = input.roots;
235
241
  let collections = input.collections;
@@ -249,9 +255,9 @@ function resolvePolymorphism(input: {
249
255
  const model = patched[modelName];
250
256
  if (!model) continue;
251
257
 
252
- const pslModel = allPslModels.find((m) => m.name === modelName);
253
- const mappedDiscriminatorField = pslModel
254
- ? (resolveFieldMappings(pslModel).pslNameToMapped.get(decl.fieldName) ?? decl.fieldName)
258
+ const modelView = allModelViews.find((m) => m.name === modelName);
259
+ const mappedDiscriminatorField = modelView
260
+ ? (resolveFieldMappings(modelView).pslNameToMapped.get(decl.fieldName) ?? decl.fieldName)
255
261
  : decl.fieldName;
256
262
 
257
263
  if (!Object.hasOwn(model.fields, mappedDiscriminatorField)) {
@@ -312,9 +318,9 @@ function resolvePolymorphism(input: {
312
318
  }
313
319
 
314
320
  const baseModel = patched[baseDecl.baseName];
315
- const variantPslModel = allPslModels.find((m) => m.name === variantName);
316
- if (!variantPslModel) continue;
317
- const hasExplicitMap = getMapName(variantPslModel.attributes) !== undefined;
321
+ const variantModelView = allModelViews.find((m) => m.name === variantName);
322
+ if (!variantModelView) continue;
323
+ const hasExplicitMap = getMapName(variantModelView.attributes) !== undefined;
318
324
 
319
325
  if (hasExplicitMap && baseModel && baseDecl.collectionName !== baseModel.storage.collection) {
320
326
  diagnostics.push({
@@ -339,7 +345,7 @@ function resolvePolymorphism(input: {
339
345
  };
340
346
  }
341
347
 
342
- const variantCollectionName = resolveCollectionName(variantPslModel);
348
+ const variantCollectionName = resolveCollectionName(variantModelView);
343
349
  if (roots[variantCollectionName]?.model === variantName) {
344
350
  if (variantCollectionName === baseCollection && baseModel) {
345
351
  roots = { ...roots, [variantCollectionName]: mongoCrossRef(baseDecl.baseName) };
@@ -483,9 +489,7 @@ function parseJsonArg(raw: string | undefined): Record<string, unknown> | undefi
483
489
  return undefined;
484
490
  }
485
491
 
486
- function parseCollation(
487
- attr: import('@prisma-next/psl-parser').PslAttribute,
488
- ): CollationOptions | null | undefined {
492
+ function parseCollation(attr: ResolvedAttribute): CollationOptions | null | undefined {
489
493
  const locale = stripQuotesHelper(getNamedArgument(attr, 'collationLocale'));
490
494
  if (!locale) {
491
495
  const hasAnyCollationArg =
@@ -545,7 +549,7 @@ function parseProjectionList(
545
549
  }
546
550
 
547
551
  function collectIndexes(
548
- pslModel: PslModel,
552
+ pslModel: ModelSymbol,
549
553
  fieldMappings: FieldMappings,
550
554
  modelNames: ReadonlySet<string>,
551
555
  sourceId: string,
@@ -561,12 +565,12 @@ function collectIndexes(
561
565
  // rather than fieldMappings.pslNameToMapped because the latter contains
562
566
  // every PSL field including relation fields.
563
567
  const indexableFieldNames = new Set<string>();
564
- for (const f of pslModel.fields) {
568
+ for (const f of Object.values(pslModel.fields)) {
565
569
  if (modelNames.has(f.typeName)) continue;
566
570
  indexableFieldNames.add(f.name);
567
571
  }
568
572
 
569
- for (const field of pslModel.fields) {
573
+ for (const field of Object.values(pslModel.fields)) {
570
574
  if (modelNames.has(field.typeName)) continue;
571
575
  const uniqueAttr = getAttribute(field.attributes, 'unique');
572
576
  if (!uniqueAttr) continue;
@@ -800,7 +804,7 @@ function collectIndexes(
800
804
  return indexes;
801
805
  }
802
806
 
803
- function isRelationField(field: PslField, modelNames: ReadonlySet<string>): boolean {
807
+ function isRelationField(field: FieldSymbol, modelNames: ReadonlySet<string>): boolean {
804
808
  return modelNames.has(field.typeName);
805
809
  }
806
810
 
@@ -808,17 +812,18 @@ function isRelationField(field: PslField, modelNames: ReadonlySet<string>): bool
808
812
  const MONGO_OBJECT_ID_PSL_TYPE = 'ObjectId';
809
813
 
810
814
  function resolveFieldCodecId(
811
- field: PslField,
815
+ field: FieldSymbol,
812
816
  scalarTypeDescriptors: ReadonlyMap<string, string>,
813
817
  ): string | undefined {
814
818
  return scalarTypeDescriptors.get(field.typeName);
815
819
  }
816
820
 
817
821
  function resolveNonRelationField(
818
- field: PslField,
822
+ field: FieldSymbol,
819
823
  ownerName: string,
820
824
  compositeTypeNames: ReadonlySet<string>,
821
825
  scalarTypeDescriptors: ReadonlyMap<string, string>,
826
+ codecIdByEnumName: ReadonlyMap<string, string>,
822
827
  sourceId: string,
823
828
  diagnostics: ContractSourceDiagnostic[],
824
829
  ): ContractField | undefined {
@@ -830,6 +835,29 @@ function resolveNonRelationField(
830
835
  return field.list ? { ...result, many: true } : result;
831
836
  }
832
837
 
838
+ // If this field's declared type is a known enum name, treat the field as a scalar
839
+ // with that enum's codec and stamp the domain valueSet ref.
840
+ const enumCodecId = codecIdByEnumName.get(field.typeName);
841
+ if (enumCodecId !== undefined) {
842
+ const valueSet: ValueSetRef = {
843
+ plane: 'domain',
844
+ entityKind: 'enum',
845
+ namespaceId: UNBOUND_NAMESPACE_ID,
846
+ entityName: field.typeName,
847
+ };
848
+ const result: ContractField = {
849
+ type: { kind: 'scalar', codecId: enumCodecId },
850
+ nullable: field.optional,
851
+ valueSet,
852
+ };
853
+ return field.list ? { ...result, many: true } : result;
854
+ }
855
+
856
+ // Avoid cascading unsupported-type diagnostics after invalid qualification.
857
+ if (field.malformedType) {
858
+ return undefined;
859
+ }
860
+
833
861
  const codecId = resolveFieldCodecId(field, scalarTypeDescriptors);
834
862
  if (!codecId) {
835
863
  diagnostics.push({
@@ -848,27 +876,105 @@ function resolveNonRelationField(
848
876
  return field.list ? { ...result, many: true } : result;
849
877
  }
850
878
 
879
+ function processEnumDeclarations(input: {
880
+ readonly enumBlocks: readonly PslExtensionBlock[];
881
+ readonly sourceId: string;
882
+ readonly authoringContributions: AuthoringContributions | undefined;
883
+ readonly entityContext: AuthoringEntityContext;
884
+ readonly diagnostics: ContractSourceDiagnostic[];
885
+ }): Record<string, ContractEnum> {
886
+ const builtEnums: Record<string, ContractEnum> = {};
887
+
888
+ if (input.enumBlocks.length === 0) return builtEnums;
889
+
890
+ const enumDescriptor =
891
+ input.authoringContributions?.entityTypes?.['enum'] !== undefined &&
892
+ isAuthoringEntityTypeDescriptor(input.authoringContributions.entityTypes['enum'])
893
+ ? input.authoringContributions.entityTypes['enum']
894
+ : undefined;
895
+
896
+ if (!enumDescriptor) {
897
+ for (const decl of input.enumBlocks) {
898
+ input.diagnostics.push({
899
+ code: 'PSL_ENUM_MISSING_FACTORY',
900
+ message: `enum "${decl.name}" requires an "enum" entityType factory in the active authoring contributions`,
901
+ sourceId: input.sourceId,
902
+ span: decl.span,
903
+ });
904
+ }
905
+ return builtEnums;
906
+ }
907
+
908
+ for (const decl of input.enumBlocks) {
909
+ const handle = instantiateAuthoringEntityType<EnumTypeHandle | undefined>(
910
+ 'enum',
911
+ enumDescriptor,
912
+ [decl],
913
+ input.entityContext,
914
+ );
915
+
916
+ if (handle === undefined || handle === null) continue;
917
+
918
+ builtEnums[decl.name] = {
919
+ codecId: handle.codecId,
920
+ members: handle.enumMembers.map((m) => ({
921
+ name: m.name,
922
+ value: blindCast<JsonValue, 'factory-validated enum members are JsonValue-compatible'>(
923
+ m.value,
924
+ ),
925
+ })),
926
+ };
927
+ }
928
+
929
+ return builtEnums;
930
+ }
931
+
851
932
  export function interpretPslDocumentToMongoContract(
852
933
  input: InterpretPslDocumentToMongoContractInput,
853
934
  ): Result<Contract, ContractSourceDiagnostics> {
854
- const { document, scalarTypeDescriptors, codecLookup } = input;
855
- const sourceId = document.ast.sourceId;
856
- const diagnostics: ContractSourceDiagnostic[] = [];
935
+ const { symbolTable, sourceFile, scalarTypeDescriptors, codecLookup } = input;
936
+ const sourceId = input.sourceId;
937
+ const diagnostics: ContractSourceDiagnostic[] = [...(input.seedDiagnostics ?? [])];
938
+ const topLevel = symbolTable.topLevel;
857
939
  validateNamespaceBlocksForMongoTarget({
858
- namespaces: document.ast.namespaces,
940
+ namespaces: Object.values(topLevel.namespaces),
859
941
  sourceId,
942
+ sourceFile,
860
943
  diagnostics,
861
944
  });
862
- // Mongo lowers only the implicit `__unspecified__` bucket today —
863
- // explicit `namespace { … }` blocks were rejected above. The IR
864
- // collection map remains flat (and the Mongo target represents
865
- // databases via `MongoTargetDatabase`, populated at compose time by
866
- // the connection string rather than authoring-time PSL).
867
- const allModels = document.ast.namespaces.flatMap((ns) => ns.models);
868
- const allCompositeTypes = document.ast.namespaces.flatMap((ns) => ns.compositeTypes);
945
+ const allModels: ModelSymbol[] = Object.values(topLevel.models);
946
+ const allCompositeTypes: CompositeTypeSymbol[] = Object.values(topLevel.compositeTypes);
869
947
  const modelNames = new Set(allModels.map((m) => m.name));
870
948
  const compositeTypeNames = new Set(allCompositeTypes.map((ct) => ct.name));
871
949
 
950
+ const topLevelEnumBlocks = Object.values(topLevel.blocks)
951
+ .filter((b) => b.keyword === 'enum')
952
+ .map((b) => b.block);
953
+
954
+ const builtEnums = processEnumDeclarations({
955
+ enumBlocks: topLevelEnumBlocks,
956
+ sourceId,
957
+ authoringContributions: input.authoringContributions,
958
+ entityContext: {
959
+ family: 'mongo',
960
+ target: 'mongo',
961
+ ...ifDefined('codecLookup', codecLookup),
962
+ sourceId,
963
+ diagnostics: {
964
+ push: (d) => {
965
+ diagnostics.push(
966
+ blindCast<ContractSourceDiagnostic, 'sink diagnostics are span-compatible'>(d),
967
+ );
968
+ },
969
+ },
970
+ },
971
+ diagnostics,
972
+ });
973
+
974
+ const codecIdByEnumName: Map<string, string> = new Map(
975
+ Object.entries(builtEnums).map(([name, e]) => [name, e.codecId]),
976
+ );
977
+
872
978
  const models: Record<string, MongoModelEntry> = {};
873
979
  const collections: Record<string, Record<string, unknown>> = {};
874
980
  const roots: Record<string, CrossReference> = {};
@@ -882,7 +988,7 @@ export function interpretPslDocumentToMongoContract(
882
988
  readonly targetModelName: string;
883
989
  readonly relationName?: string;
884
990
  readonly cardinality: '1:1' | '1:N';
885
- readonly field: PslField;
991
+ readonly field: FieldSymbol;
886
992
  }
887
993
  const backrelationCandidates: BackrelationCandidate[] = [];
888
994
 
@@ -893,7 +999,7 @@ export function interpretPslDocumentToMongoContract(
893
999
  const fields: Record<string, ContractField> = {};
894
1000
  const relations: Record<string, ContractReferenceRelation> = {};
895
1001
 
896
- for (const field of pslModel.fields) {
1002
+ for (const field of Object.values(pslModel.fields)) {
897
1003
  if (isRelationField(field, modelNames)) {
898
1004
  const relation = parseRelationAttribute(field.attributes);
899
1005
 
@@ -902,9 +1008,7 @@ export function interpretPslDocumentToMongoContract(
902
1008
  modelName: pslModel.name,
903
1009
  fieldName: field.name,
904
1010
  targetModelName: field.typeName,
905
- ...(relation?.relationName !== undefined
906
- ? { relationName: relation.relationName }
907
- : {}),
1011
+ ...ifDefined('relationName', relation?.relationName),
908
1012
  cardinality: field.list ? '1:N' : '1:1',
909
1013
  field,
910
1014
  });
@@ -933,7 +1037,7 @@ export function interpretPslDocumentToMongoContract(
933
1037
  declaringModel: pslModel.name,
934
1038
  fieldName: field.name,
935
1039
  targetModel: field.typeName,
936
- ...(relation.relationName !== undefined ? { relationName: relation.relationName } : {}),
1040
+ ...ifDefined('relationName', relation.relationName),
937
1041
  localFields: localMapped,
938
1042
  targetFields: targetMapped,
939
1043
  });
@@ -946,6 +1050,7 @@ export function interpretPslDocumentToMongoContract(
946
1050
  pslModel.name,
947
1051
  compositeTypeNames,
948
1052
  scalarTypeDescriptors,
1053
+ codecIdByEnumName,
949
1054
  sourceId,
950
1055
  diagnostics,
951
1056
  );
@@ -956,7 +1061,9 @@ export function interpretPslDocumentToMongoContract(
956
1061
  }
957
1062
 
958
1063
  const isVariantModel = pslModel.attributes.some((attr) => attr.name === 'base');
959
- const hasIdField = pslModel.fields.some((f) => getAttribute(f.attributes, 'id') !== undefined);
1064
+ const hasIdField = Object.values(pslModel.fields).some(
1065
+ (f) => getAttribute(f.attributes, 'id') !== undefined,
1066
+ );
960
1067
  // Variant models inherit the base's identity and are validated through their base.
961
1068
  if (!isVariantModel) {
962
1069
  if (!hasIdField) {
@@ -1011,12 +1118,13 @@ export function interpretPslDocumentToMongoContract(
1011
1118
  const valueObjects: Record<string, ContractValueObject> = {};
1012
1119
  for (const compositeType of allCompositeTypes) {
1013
1120
  const fields: Record<string, ContractField> = {};
1014
- for (const field of compositeType.fields) {
1121
+ for (const field of Object.values(compositeType.fields)) {
1015
1122
  const resolved = resolveNonRelationField(
1016
1123
  field,
1017
1124
  compositeType.name,
1018
1125
  compositeTypeNames,
1019
1126
  scalarTypeDescriptors,
1127
+ codecIdByEnumName,
1020
1128
  sourceId,
1021
1129
  diagnostics,
1022
1130
  );
@@ -1078,7 +1186,7 @@ export function interpretPslDocumentToMongoContract(
1078
1186
  }
1079
1187
 
1080
1188
  const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations(
1081
- document,
1189
+ allModels,
1082
1190
  sourceId,
1083
1191
  diagnostics,
1084
1192
  );
@@ -1086,7 +1194,7 @@ export function interpretPslDocumentToMongoContract(
1086
1194
  models,
1087
1195
  roots,
1088
1196
  collections,
1089
- document,
1197
+ allModels,
1090
1198
  discriminatorDeclarations,
1091
1199
  baseDeclarations,
1092
1200
  modelNames,
@@ -1125,9 +1233,15 @@ export function interpretPslDocumentToMongoContract(
1125
1233
  variantEntries,
1126
1234
  valueObjects,
1127
1235
  codecLookup,
1236
+ builtEnums,
1128
1237
  );
1129
1238
  } else {
1130
- coll['validator'] = deriveJsonSchema(modelEntry.fields, valueObjects, codecLookup);
1239
+ coll['validator'] = deriveJsonSchema(
1240
+ modelEntry.fields,
1241
+ valueObjects,
1242
+ codecLookup,
1243
+ builtEnums,
1244
+ );
1131
1245
  }
1132
1246
  }
1133
1247
 
@@ -1173,6 +1287,8 @@ export function interpretPslDocumentToMongoContract(
1173
1287
  }) as Contract['storage'];
1174
1288
  const capabilities: Record<string, Record<string, boolean>> = {};
1175
1289
 
1290
+ const hasEnums = Object.keys(builtEnums).length > 0;
1291
+
1176
1292
  return ok({
1177
1293
  targetFamily,
1178
1294
  target,
@@ -1182,6 +1298,7 @@ export function interpretPslDocumentToMongoContract(
1182
1298
  [UNBOUND_NAMESPACE_ID]: {
1183
1299
  models: polyResult.models,
1184
1300
  ...(Object.keys(valueObjects).length > 0 ? { valueObjects } : {}),
1301
+ ...(hasEnums ? { enum: builtEnums } : {}),
1185
1302
  },
1186
1303
  },
1187
1304
  },
package/src/provider.ts CHANGED
@@ -1,17 +1,34 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import type { ContractConfig } from '@prisma-next/config/config-types';
3
- import { parsePslDocument } from '@prisma-next/psl-parser';
2
+ import type { ContractConfig, ContractSourceDiagnostic } from '@prisma-next/config/config-types';
3
+ import { buildSymbolTable, rangeToPslSpan } from '@prisma-next/psl-parser';
4
+ import type { ParseDiagnostic, SourceFile } from '@prisma-next/psl-parser/syntax';
5
+ import { parse } from '@prisma-next/psl-parser/syntax';
4
6
  import { ifDefined } from '@prisma-next/utils/defined';
5
7
  import { notOk, ok } from '@prisma-next/utils/result';
8
+
6
9
  import { interpretPslDocumentToMongoContract } from './interpreter';
7
10
 
8
11
  export interface MongoContractOptions {
9
12
  readonly output?: string;
10
13
  }
11
14
 
15
+ function mapParseDiagnostics(
16
+ diagnostics: readonly ParseDiagnostic[],
17
+ sourceFile: SourceFile,
18
+ sourceId: string,
19
+ ): ContractSourceDiagnostic[] {
20
+ return diagnostics.map((diagnostic) => ({
21
+ code: diagnostic.code,
22
+ message: diagnostic.message,
23
+ sourceId,
24
+ span: rangeToPslSpan(diagnostic.range, sourceFile),
25
+ }));
26
+ }
27
+
12
28
  export function mongoContract(schemaPath: string, options?: MongoContractOptions): ContractConfig {
13
29
  return {
14
30
  source: {
31
+ sourceFormat: 'psl',
15
32
  inputs: [schemaPath],
16
33
  load: async (context) => {
17
34
  const [absoluteSchemaPath] = context.resolvedInputs;
@@ -38,15 +55,29 @@ export function mongoContract(schemaPath: string, options?: MongoContractOptions
38
55
  });
39
56
  }
40
57
 
41
- const document = parsePslDocument({
42
- schema,
43
- sourceId: schemaPath,
58
+ const { document, sourceFile, diagnostics: parseDiagnostics } = parse(schema);
59
+ const { table: symbolTable, diagnostics: symbolTableDiagnostics } = buildSymbolTable({
60
+ document,
61
+ sourceFile,
62
+ scalarTypes: [...context.scalarTypeDescriptors.keys()],
63
+ pslBlockDescriptors: context.authoringContributions.pslBlockDescriptors,
44
64
  });
45
65
 
66
+ // Do not short-circuit on provider-level diagnostics; recovered CST can
67
+ // still produce interpreter diagnostics in the same response.
68
+ const seedDiagnostics = [
69
+ ...mapParseDiagnostics(parseDiagnostics, sourceFile, schemaPath),
70
+ ...mapParseDiagnostics(symbolTableDiagnostics, sourceFile, schemaPath),
71
+ ];
72
+
46
73
  const interpreted = interpretPslDocumentToMongoContract({
47
- document,
74
+ symbolTable,
75
+ sourceFile,
76
+ sourceId: schemaPath,
77
+ seedDiagnostics,
48
78
  scalarTypeDescriptors: context.scalarTypeDescriptors,
49
79
  codecLookup: context.codecLookup,
80
+ authoringContributions: context.authoringContributions,
50
81
  });
51
82
  if (!interpreted.ok) {
52
83
  return interpreted;