@prisma-next/mongo-contract-psl 0.13.0 → 0.14.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.
@@ -16,19 +16,25 @@ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
16
16
  import {
17
17
  applyPolymorphicScopeToMongoIndex,
18
18
  buildMongoNamespace,
19
- MongoCollection,
19
+ type MongoCollectionInput,
20
20
  MongoIndex,
21
21
  type MongoIndexKeyDirection,
22
22
  MongoStorage,
23
23
  } from '@prisma-next/mongo-contract';
24
24
  import { mongoContractCanonicalizationHooks } from '@prisma-next/mongo-contract/canonicalization-hooks';
25
+ import type { CollationOptions } from '@prisma-next/mongo-value/mongodb-types';
25
26
  import type {
26
- ParsePslDocumentResult,
27
- PslField,
28
- PslModel,
29
- PslNamespace,
27
+ CompositeTypeSymbol,
28
+ FieldSymbol,
29
+ ModelSymbol,
30
+ NamespaceSymbol,
30
31
  PslSpan,
32
+ ResolvedAttribute,
33
+ SymbolTable,
31
34
  } from '@prisma-next/psl-parser';
35
+ import { nodePslSpan } from '@prisma-next/psl-parser';
36
+ import type { SourceFile } from '@prisma-next/psl-parser/syntax';
37
+ import { blindCast } from '@prisma-next/utils/casts';
32
38
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
33
39
  import { deriveJsonSchema, derivePolymorphicJsonSchema } from './derive-json-schema';
34
40
  import {
@@ -43,41 +49,30 @@ import {
43
49
  } from './psl-helpers';
44
50
 
45
51
  export interface InterpretPslDocumentToMongoContractInput {
46
- readonly document: ParsePslDocumentResult;
52
+ readonly symbolTable: SymbolTable;
53
+ readonly sourceFile: SourceFile;
54
+ readonly sourceId: string;
47
55
  readonly scalarTypeDescriptors: ReadonlyMap<string, string>;
48
56
  readonly codecLookup?: CodecLookup;
57
+ readonly seedDiagnostics?: readonly ContractSourceDiagnostic[];
49
58
  }
50
59
 
51
60
  /**
52
- * Name of the framework-parser synthesised bucket for top-level
53
- * declarations. Re-declared locally so the interpreter does not have to
54
- * import from `@prisma-next/framework-components/psl-ast`.
55
- */
56
- const UNSPECIFIED_PSL_NAMESPACE_NAME = '__unspecified__';
57
-
58
- /**
59
- * Mongo FR16c validation: Mongo's authoring DSL exposes the connection's
60
- * database as the only namespace surface today, so the PSL interpreter
61
- * rejects every explicit `namespace { … }` block. The implicit
62
- * `__unspecified__` bucket (top-level declarations) is the only
63
- * namespace Mongo accepts. `namespace unbound { … }` is rejected too —
64
- * Mongo has no late-binding namespace concept on the PSL surface (the
65
- * database name comes from the connection string, not from PSL).
61
+ * Mongo's PSL surface binds the database from the connection string, so every
62
+ * explicit namespace block is invalid, including `namespace unbound { … }`.
66
63
  */
67
64
  function validateNamespaceBlocksForMongoTarget(input: {
68
- readonly namespaces: readonly PslNamespace[];
65
+ readonly namespaces: readonly NamespaceSymbol[];
69
66
  readonly sourceId: string;
67
+ readonly sourceFile: SourceFile;
70
68
  readonly diagnostics: ContractSourceDiagnostic[];
71
69
  }): void {
72
70
  for (const namespace of input.namespaces) {
73
- if (namespace.name === UNSPECIFIED_PSL_NAMESPACE_NAME) {
74
- continue;
75
- }
76
71
  input.diagnostics.push({
77
72
  code: 'PSL_UNSUPPORTED_NAMESPACE_BLOCK',
78
73
  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).`,
79
74
  sourceId: input.sourceId,
80
- span: namespace.span,
75
+ span: nodePslSpan(namespace.node.syntax, input.sourceFile),
81
76
  });
82
77
  }
83
78
  }
@@ -99,16 +94,16 @@ function fkRelationPairKey(declaringModel: string, targetModel: string): string
99
94
  return `${declaringModel}::${targetModel}`;
100
95
  }
101
96
 
102
- function resolveFieldMappings(model: PslModel): FieldMappings {
97
+ function resolveFieldMappings(model: ModelSymbol): FieldMappings {
103
98
  const pslNameToMapped = new Map<string, string>();
104
- for (const field of model.fields) {
99
+ for (const field of Object.values(model.fields)) {
105
100
  const mapped = getMapName(field.attributes) ?? field.name;
106
101
  pslNameToMapped.set(field.name, mapped);
107
102
  }
108
103
  return { pslNameToMapped };
109
104
  }
110
105
 
111
- function resolveCollectionName(model: PslModel): string {
106
+ function resolveCollectionName(model: ModelSymbol): string {
112
107
  return getMapName(model.attributes) ?? lowerFirst(model.name);
113
108
  }
114
109
 
@@ -121,12 +116,12 @@ interface MongoModelEntry {
121
116
  readonly base?: CrossReference;
122
117
  }
123
118
 
124
- type DiscriminatorDeclaration = { readonly fieldName: string; readonly span: PslModel['span'] };
119
+ type DiscriminatorDeclaration = { readonly fieldName: string; readonly span: PslSpan };
125
120
  type BaseDeclaration = {
126
121
  readonly baseName: string;
127
122
  readonly value: string;
128
123
  readonly collectionName: string;
129
- readonly span: PslModel['span'];
124
+ readonly span: PslSpan;
130
125
  };
131
126
 
132
127
  function mongoCrossRef(modelName: string): CrossReference {
@@ -134,7 +129,7 @@ function mongoCrossRef(modelName: string): CrossReference {
134
129
  }
135
130
 
136
131
  function collectPolymorphismDeclarations(
137
- document: ParsePslDocumentResult,
132
+ models: readonly ModelSymbol[],
138
133
  sourceId: string,
139
134
  diagnostics: ContractSourceDiagnostic[],
140
135
  ): {
@@ -143,32 +138,31 @@ function collectPolymorphismDeclarations(
143
138
  } {
144
139
  const discriminatorDeclarations = new Map<string, DiscriminatorDeclaration>();
145
140
  const baseDeclarations = new Map<string, BaseDeclaration>();
146
- const allPslModels = document.ast.namespaces.flatMap((ns) => ns.models);
147
141
 
148
- for (const pslModel of allPslModels) {
149
- for (const attr of pslModel.attributes) {
142
+ for (const model of models) {
143
+ for (const attr of model.attributes) {
150
144
  if (attr.name === 'discriminator') {
151
145
  const fieldName = getPositionalArgument(attr);
152
146
  if (!fieldName) {
153
147
  diagnostics.push({
154
148
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
155
- message: `Model "${pslModel.name}" @@discriminator requires a field name argument`,
149
+ message: `Model "${model.name}" @@discriminator requires a field name argument`,
156
150
  sourceId,
157
151
  span: attr.span,
158
152
  });
159
153
  continue;
160
154
  }
161
- const discField = pslModel.fields.find((f) => f.name === fieldName);
155
+ const discField = model.fields[fieldName];
162
156
  if (discField && discField.typeName !== 'String') {
163
157
  diagnostics.push({
164
158
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
165
- message: `Discriminator field "${fieldName}" on model "${pslModel.name}" must be of type String, but is "${discField.typeName}"`,
159
+ message: `Discriminator field "${fieldName}" on model "${model.name}" must be of type String, but is "${discField.typeName}"`,
166
160
  sourceId,
167
161
  span: attr.span,
168
162
  });
169
163
  continue;
170
164
  }
171
- discriminatorDeclarations.set(pslModel.name, { fieldName, span: attr.span });
165
+ discriminatorDeclarations.set(model.name, { fieldName, span: attr.span });
172
166
  }
173
167
  if (attr.name === 'base') {
174
168
  const baseName = getPositionalArgument(attr, 0);
@@ -176,7 +170,7 @@ function collectPolymorphismDeclarations(
176
170
  if (!baseName || !rawValue) {
177
171
  diagnostics.push({
178
172
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
179
- message: `Model "${pslModel.name}" @@base requires two arguments: base model name and discriminator value`,
173
+ message: `Model "${model.name}" @@base requires two arguments: base model name and discriminator value`,
180
174
  sourceId,
181
175
  span: attr.span,
182
176
  });
@@ -186,14 +180,14 @@ function collectPolymorphismDeclarations(
186
180
  if (value === undefined) {
187
181
  diagnostics.push({
188
182
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
189
- message: `Model "${pslModel.name}" @@base discriminator value must be a quoted string literal`,
183
+ message: `Model "${model.name}" @@base discriminator value must be a quoted string literal`,
190
184
  sourceId,
191
185
  span: attr.span,
192
186
  });
193
187
  continue;
194
188
  }
195
- const collectionName = resolveCollectionName(pslModel);
196
- baseDeclarations.set(pslModel.name, { baseName, value, collectionName, span: attr.span });
189
+ const collectionName = resolveCollectionName(model);
190
+ baseDeclarations.set(model.name, { baseName, value, collectionName, span: attr.span });
197
191
  }
198
192
  }
199
193
  }
@@ -205,7 +199,7 @@ function resolvePolymorphism(input: {
205
199
  models: Record<string, MongoModelEntry>;
206
200
  roots: Record<string, CrossReference>;
207
201
  collections: Record<string, Record<string, unknown>>;
208
- document: ParsePslDocumentResult;
202
+ allModels: readonly ModelSymbol[];
209
203
  discriminatorDeclarations: Map<string, DiscriminatorDeclaration>;
210
204
  baseDeclarations: Map<string, BaseDeclaration>;
211
205
  modelNames: ReadonlySet<string>;
@@ -223,11 +217,10 @@ function resolvePolymorphism(input: {
223
217
  baseDeclarations,
224
218
  modelNames,
225
219
  sourceId,
226
- document,
220
+ allModels: allModelViews,
227
221
  indexSpans,
228
222
  modelIndexesByName,
229
223
  } = input;
230
- const allPslModels = document.ast.namespaces.flatMap((ns) => ns.models);
231
224
  let patched = input.models;
232
225
  let roots = input.roots;
233
226
  let collections = input.collections;
@@ -247,9 +240,9 @@ function resolvePolymorphism(input: {
247
240
  const model = patched[modelName];
248
241
  if (!model) continue;
249
242
 
250
- const pslModel = allPslModels.find((m) => m.name === modelName);
251
- const mappedDiscriminatorField = pslModel
252
- ? (resolveFieldMappings(pslModel).pslNameToMapped.get(decl.fieldName) ?? decl.fieldName)
243
+ const modelView = allModelViews.find((m) => m.name === modelName);
244
+ const mappedDiscriminatorField = modelView
245
+ ? (resolveFieldMappings(modelView).pslNameToMapped.get(decl.fieldName) ?? decl.fieldName)
253
246
  : decl.fieldName;
254
247
 
255
248
  if (!Object.hasOwn(model.fields, mappedDiscriminatorField)) {
@@ -310,9 +303,9 @@ function resolvePolymorphism(input: {
310
303
  }
311
304
 
312
305
  const baseModel = patched[baseDecl.baseName];
313
- const variantPslModel = allPslModels.find((m) => m.name === variantName);
314
- if (!variantPslModel) continue;
315
- const hasExplicitMap = getMapName(variantPslModel.attributes) !== undefined;
306
+ const variantModelView = allModelViews.find((m) => m.name === variantName);
307
+ if (!variantModelView) continue;
308
+ const hasExplicitMap = getMapName(variantModelView.attributes) !== undefined;
316
309
 
317
310
  if (hasExplicitMap && baseModel && baseDecl.collectionName !== baseModel.storage.collection) {
318
311
  diagnostics.push({
@@ -337,7 +330,7 @@ function resolvePolymorphism(input: {
337
330
  };
338
331
  }
339
332
 
340
- const variantCollectionName = resolveCollectionName(variantPslModel);
333
+ const variantCollectionName = resolveCollectionName(variantModelView);
341
334
  if (roots[variantCollectionName]?.model === variantName) {
342
335
  if (variantCollectionName === baseCollection && baseModel) {
343
336
  roots = { ...roots, [variantCollectionName]: mongoCrossRef(baseDecl.baseName) };
@@ -481,9 +474,7 @@ function parseJsonArg(raw: string | undefined): Record<string, unknown> | undefi
481
474
  return undefined;
482
475
  }
483
476
 
484
- function parseCollation(
485
- attr: import('@prisma-next/psl-parser').PslAttribute,
486
- ): Record<string, unknown> | null | undefined {
477
+ function parseCollation(attr: ResolvedAttribute): CollationOptions | null | undefined {
487
478
  const locale = stripQuotesHelper(getNamedArgument(attr, 'collationLocale'));
488
479
  if (!locale) {
489
480
  const hasAnyCollationArg =
@@ -498,23 +489,23 @@ function parseCollation(
498
489
  return hasAnyCollationArg ? null : undefined;
499
490
  }
500
491
 
501
- const collation: Record<string, unknown> = { locale };
492
+ const collation: CollationOptions = { locale };
502
493
  const strength = parseNumericArg(getNamedArgument(attr, 'collationStrength'));
503
- if (strength != null) collation['strength'] = strength;
494
+ if (strength != null) collation.strength = strength;
504
495
  const caseLevel = parseBooleanArg(getNamedArgument(attr, 'collationCaseLevel'));
505
- if (caseLevel != null) collation['caseLevel'] = caseLevel;
496
+ if (caseLevel != null) collation.caseLevel = caseLevel;
506
497
  const caseFirst = stripQuotesHelper(getNamedArgument(attr, 'collationCaseFirst'));
507
- if (caseFirst != null) collation['caseFirst'] = caseFirst;
498
+ if (caseFirst != null) collation.caseFirst = caseFirst;
508
499
  const numericOrdering = parseBooleanArg(getNamedArgument(attr, 'collationNumericOrdering'));
509
- if (numericOrdering != null) collation['numericOrdering'] = numericOrdering;
500
+ if (numericOrdering != null) collation.numericOrdering = numericOrdering;
510
501
  const alternate = stripQuotesHelper(getNamedArgument(attr, 'collationAlternate'));
511
- if (alternate != null) collation['alternate'] = alternate;
502
+ if (alternate != null) collation.alternate = alternate;
512
503
  const maxVariable = stripQuotesHelper(getNamedArgument(attr, 'collationMaxVariable'));
513
- if (maxVariable != null) collation['maxVariable'] = maxVariable;
504
+ if (maxVariable != null) collation.maxVariable = maxVariable;
514
505
  const backwards = parseBooleanArg(getNamedArgument(attr, 'collationBackwards'));
515
- if (backwards != null) collation['backwards'] = backwards;
506
+ if (backwards != null) collation.backwards = backwards;
516
507
  const normalization = parseBooleanArg(getNamedArgument(attr, 'collationNormalization'));
517
- if (normalization != null) collation['normalization'] = normalization;
508
+ if (normalization != null) collation.normalization = normalization;
518
509
  return collation;
519
510
  }
520
511
 
@@ -543,7 +534,7 @@ function parseProjectionList(
543
534
  }
544
535
 
545
536
  function collectIndexes(
546
- pslModel: PslModel,
537
+ pslModel: ModelSymbol,
547
538
  fieldMappings: FieldMappings,
548
539
  modelNames: ReadonlySet<string>,
549
540
  sourceId: string,
@@ -559,12 +550,12 @@ function collectIndexes(
559
550
  // rather than fieldMappings.pslNameToMapped because the latter contains
560
551
  // every PSL field including relation fields.
561
552
  const indexableFieldNames = new Set<string>();
562
- for (const f of pslModel.fields) {
553
+ for (const f of Object.values(pslModel.fields)) {
563
554
  if (modelNames.has(f.typeName)) continue;
564
555
  indexableFieldNames.add(f.name);
565
556
  }
566
557
 
567
- for (const field of pslModel.fields) {
558
+ for (const field of Object.values(pslModel.fields)) {
568
559
  if (modelNames.has(field.typeName)) continue;
569
560
  const uniqueAttr = getAttribute(field.attributes, 'unique');
570
561
  if (!uniqueAttr) continue;
@@ -798,7 +789,7 @@ function collectIndexes(
798
789
  return indexes;
799
790
  }
800
791
 
801
- function isRelationField(field: PslField, modelNames: ReadonlySet<string>): boolean {
792
+ function isRelationField(field: FieldSymbol, modelNames: ReadonlySet<string>): boolean {
802
793
  return modelNames.has(field.typeName);
803
794
  }
804
795
 
@@ -806,14 +797,14 @@ function isRelationField(field: PslField, modelNames: ReadonlySet<string>): bool
806
797
  const MONGO_OBJECT_ID_PSL_TYPE = 'ObjectId';
807
798
 
808
799
  function resolveFieldCodecId(
809
- field: PslField,
800
+ field: FieldSymbol,
810
801
  scalarTypeDescriptors: ReadonlyMap<string, string>,
811
802
  ): string | undefined {
812
803
  return scalarTypeDescriptors.get(field.typeName);
813
804
  }
814
805
 
815
806
  function resolveNonRelationField(
816
- field: PslField,
807
+ field: FieldSymbol,
817
808
  ownerName: string,
818
809
  compositeTypeNames: ReadonlySet<string>,
819
810
  scalarTypeDescriptors: ReadonlyMap<string, string>,
@@ -828,6 +819,11 @@ function resolveNonRelationField(
828
819
  return field.list ? { ...result, many: true } : result;
829
820
  }
830
821
 
822
+ // Avoid cascading unsupported-type diagnostics after invalid qualification.
823
+ if (field.malformedType) {
824
+ return undefined;
825
+ }
826
+
831
827
  const codecId = resolveFieldCodecId(field, scalarTypeDescriptors);
832
828
  if (!codecId) {
833
829
  diagnostics.push({
@@ -849,21 +845,18 @@ function resolveNonRelationField(
849
845
  export function interpretPslDocumentToMongoContract(
850
846
  input: InterpretPslDocumentToMongoContractInput,
851
847
  ): Result<Contract, ContractSourceDiagnostics> {
852
- const { document, scalarTypeDescriptors, codecLookup } = input;
853
- const sourceId = document.ast.sourceId;
854
- const diagnostics: ContractSourceDiagnostic[] = [];
848
+ const { symbolTable, sourceFile, scalarTypeDescriptors, codecLookup } = input;
849
+ const sourceId = input.sourceId;
850
+ const diagnostics: ContractSourceDiagnostic[] = [...(input.seedDiagnostics ?? [])];
851
+ const topLevel = symbolTable.topLevel;
855
852
  validateNamespaceBlocksForMongoTarget({
856
- namespaces: document.ast.namespaces,
853
+ namespaces: Object.values(topLevel.namespaces),
857
854
  sourceId,
855
+ sourceFile,
858
856
  diagnostics,
859
857
  });
860
- // Mongo lowers only the implicit `__unspecified__` bucket today —
861
- // explicit `namespace { … }` blocks were rejected above. The IR
862
- // collection map remains flat (and the Mongo target represents
863
- // databases via `MongoTargetDatabase`, populated at compose time by
864
- // the connection string rather than authoring-time PSL).
865
- const allModels = document.ast.namespaces.flatMap((ns) => ns.models);
866
- const allCompositeTypes = document.ast.namespaces.flatMap((ns) => ns.compositeTypes);
858
+ const allModels: ModelSymbol[] = Object.values(topLevel.models);
859
+ const allCompositeTypes: CompositeTypeSymbol[] = Object.values(topLevel.compositeTypes);
867
860
  const modelNames = new Set(allModels.map((m) => m.name));
868
861
  const compositeTypeNames = new Set(allCompositeTypes.map((ct) => ct.name));
869
862
 
@@ -880,7 +873,7 @@ export function interpretPslDocumentToMongoContract(
880
873
  readonly targetModelName: string;
881
874
  readonly relationName?: string;
882
875
  readonly cardinality: '1:1' | '1:N';
883
- readonly field: PslField;
876
+ readonly field: FieldSymbol;
884
877
  }
885
878
  const backrelationCandidates: BackrelationCandidate[] = [];
886
879
 
@@ -891,7 +884,7 @@ export function interpretPslDocumentToMongoContract(
891
884
  const fields: Record<string, ContractField> = {};
892
885
  const relations: Record<string, ContractReferenceRelation> = {};
893
886
 
894
- for (const field of pslModel.fields) {
887
+ for (const field of Object.values(pslModel.fields)) {
895
888
  if (isRelationField(field, modelNames)) {
896
889
  const relation = parseRelationAttribute(field.attributes);
897
890
 
@@ -954,7 +947,9 @@ export function interpretPslDocumentToMongoContract(
954
947
  }
955
948
 
956
949
  const isVariantModel = pslModel.attributes.some((attr) => attr.name === 'base');
957
- const hasIdField = pslModel.fields.some((f) => getAttribute(f.attributes, 'id') !== undefined);
950
+ const hasIdField = Object.values(pslModel.fields).some(
951
+ (f) => getAttribute(f.attributes, 'id') !== undefined,
952
+ );
958
953
  // Variant models inherit the base's identity and are validated through their base.
959
954
  if (!isVariantModel) {
960
955
  if (!hasIdField) {
@@ -1009,7 +1004,7 @@ export function interpretPslDocumentToMongoContract(
1009
1004
  const valueObjects: Record<string, ContractValueObject> = {};
1010
1005
  for (const compositeType of allCompositeTypes) {
1011
1006
  const fields: Record<string, ContractField> = {};
1012
- for (const field of compositeType.fields) {
1007
+ for (const field of Object.values(compositeType.fields)) {
1013
1008
  const resolved = resolveNonRelationField(
1014
1009
  field,
1015
1010
  compositeType.name,
@@ -1076,7 +1071,7 @@ export function interpretPslDocumentToMongoContract(
1076
1071
  }
1077
1072
 
1078
1073
  const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations(
1079
- document,
1074
+ allModels,
1080
1075
  sourceId,
1081
1076
  diagnostics,
1082
1077
  );
@@ -1084,7 +1079,7 @@ export function interpretPslDocumentToMongoContract(
1084
1079
  models,
1085
1080
  roots,
1086
1081
  collections,
1087
- document,
1082
+ allModels,
1088
1083
  discriminatorDeclarations,
1089
1084
  baseDeclarations,
1090
1085
  modelNames,
@@ -1131,34 +1126,29 @@ export function interpretPslDocumentToMongoContract(
1131
1126
 
1132
1127
  const target = 'mongo';
1133
1128
  const targetFamily = 'mongo';
1134
- const collectionsAsClasses: Record<string, MongoCollection> = {};
1129
+ const collectionInputs: Record<string, MongoCollectionInput> = {};
1135
1130
  for (const [name, coll] of Object.entries(resolvedCollections)) {
1136
- const input: {
1137
- indexes?: ReadonlyArray<MongoIndex>;
1138
- validator?: unknown;
1139
- options?: unknown;
1140
- } = {};
1141
- if (coll['indexes'] !== undefined) {
1142
- input.indexes = coll['indexes'] as ReadonlyArray<MongoIndex>;
1143
- }
1144
- if (coll['validator'] !== undefined) {
1145
- input.validator = coll['validator'];
1146
- }
1147
- if (coll['options'] !== undefined) {
1148
- input.options = coll['options'];
1149
- }
1150
- // input.validator/options are arktype-validated JSON shapes; MongoCollection
1151
- // constructor normalises them into MongoValidator / MongoCollectionOptions
1152
- // instances. The narrow cast is bounded to the field-typed input record.
1153
- collectionsAsClasses[name] = new MongoCollection(
1154
- input as ConstructorParameters<typeof MongoCollection>[0],
1155
- );
1131
+ const raw: Record<string, unknown> = {};
1132
+ if (coll['indexes'] != null) raw['indexes'] = coll['indexes'];
1133
+ if (coll['validator'] != null) raw['validator'] = coll['validator'];
1134
+ if (coll['options'] != null) raw['options'] = coll['options'];
1135
+ collectionInputs[name] = blindCast<
1136
+ MongoCollectionInput,
1137
+ 'arktype-validated JSON shapes satisfy MongoCollectionInput by construction'
1138
+ >(raw);
1156
1139
  }
1140
+ const unboundNamespace = buildMongoNamespace({
1141
+ id: UNBOUND_NAMESPACE_ID,
1142
+ entries: { collection: collectionInputs },
1143
+ });
1144
+ // Hash the constructed (normalized) collection map, not the raw input
1145
+ // literals — persisted storageHash values were computed over the
1146
+ // constructed form.
1157
1147
  const storageWithoutHash = {
1158
1148
  namespaces: {
1159
1149
  [UNBOUND_NAMESPACE_ID]: {
1160
1150
  id: UNBOUND_NAMESPACE_ID,
1161
- entries: { collection: collectionsAsClasses },
1151
+ entries: { collection: unboundNamespace.entries.collection },
1162
1152
  },
1163
1153
  },
1164
1154
  };
@@ -1171,10 +1161,7 @@ export function interpretPslDocumentToMongoContract(
1171
1161
  const storage = new MongoStorage({
1172
1162
  storageHash,
1173
1163
  namespaces: {
1174
- [UNBOUND_NAMESPACE_ID]: buildMongoNamespace({
1175
- id: UNBOUND_NAMESPACE_ID,
1176
- entries: { collection: collectionsAsClasses },
1177
- }),
1164
+ [UNBOUND_NAMESPACE_ID]: unboundNamespace,
1178
1165
  },
1179
1166
  }) as Contract['storage'];
1180
1167
  const capabilities: Record<string, Record<string, boolean>> = {};
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,13 +55,26 @@ 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,
50
80
  });
@@ -1,9 +1,14 @@
1
- import type { PslAttribute, PslAttributeArgument } from '@prisma-next/psl-parser';
2
- import { getPositionalArgument, parseQuotedStringLiteral } from '@prisma-next/psl-parser';
1
+ import type { ResolvedAttribute, ResolvedAttributeArg } from '@prisma-next/psl-parser';
2
+ import { parseQuotedStringLiteral } from '@prisma-next/psl-parser';
3
+ import { ifDefined } from '@prisma-next/utils/defined';
3
4
 
4
- export { getPositionalArgument, parseQuotedStringLiteral };
5
+ export { parseQuotedStringLiteral };
5
6
 
6
- export function getNamedArgument(attr: PslAttribute, name: string): string | undefined {
7
+ export function getPositionalArgument(attr: ResolvedAttribute, index = 0): string | undefined {
8
+ return attr.args.filter((arg) => arg.kind === 'positional')[index]?.value;
9
+ }
10
+
11
+ export function getNamedArgument(attr: ResolvedAttribute, name: string): string | undefined {
7
12
  const arg = attr.args.find((a) => a.kind === 'named' && a.name === name);
8
13
  return arg?.value;
9
14
  }
@@ -72,13 +77,13 @@ export function lowerFirst(value: string): string {
72
77
  }
73
78
 
74
79
  export function getAttribute(
75
- attributes: readonly PslAttribute[],
80
+ attributes: readonly ResolvedAttribute[],
76
81
  name: string,
77
- ): PslAttribute | undefined {
82
+ ): ResolvedAttribute | undefined {
78
83
  return attributes.find((attr) => attr.name === name);
79
84
  }
80
85
 
81
- export function getMapName(attributes: readonly PslAttribute[]): string | undefined {
86
+ export function getMapName(attributes: readonly ResolvedAttribute[]): string | undefined {
82
87
  const mapAttr = getAttribute(attributes, 'map');
83
88
  if (!mapAttr) return undefined;
84
89
  const arg = mapAttr.args[0];
@@ -93,14 +98,14 @@ export interface ParsedRelationAttribute {
93
98
  }
94
99
 
95
100
  export function parseRelationAttribute(
96
- attributes: readonly PslAttribute[],
101
+ attributes: readonly ResolvedAttribute[],
97
102
  ): ParsedRelationAttribute | undefined {
98
103
  const relationAttr = getAttribute(attributes, 'relation');
99
104
  if (!relationAttr) return undefined;
100
105
 
101
106
  let relationName: string | undefined;
102
- let fieldsArg: PslAttributeArgument | undefined;
103
- let referencesArg: PslAttributeArgument | undefined;
107
+ let fieldsArg: ResolvedAttributeArg | undefined;
108
+ let referencesArg: ResolvedAttributeArg | undefined;
104
109
 
105
110
  for (const arg of relationAttr.args) {
106
111
  if (arg.kind === 'positional') {
@@ -118,9 +123,9 @@ export function parseRelationAttribute(
118
123
  const references = referencesArg ? parseFieldList(referencesArg.value) : undefined;
119
124
 
120
125
  return {
121
- ...(relationName !== undefined ? { relationName } : {}),
122
- ...(fields !== undefined ? { fields } : {}),
123
- ...(references !== undefined ? { references } : {}),
126
+ ...ifDefined('relationName', relationName),
127
+ ...ifDefined('fields', fields),
128
+ ...ifDefined('references', references),
124
129
  };
125
130
  }
126
131