@prisma-next/mongo-contract-psl 0.14.0-dev.5 → 0.14.0-dev.7

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.
@@ -24,12 +24,16 @@ import {
24
24
  import { mongoContractCanonicalizationHooks } from '@prisma-next/mongo-contract/canonicalization-hooks';
25
25
  import type { CollationOptions } from '@prisma-next/mongo-value/mongodb-types';
26
26
  import type {
27
- ParsePslDocumentResult,
28
- PslField,
29
- PslModel,
30
- PslNamespace,
27
+ CompositeTypeSymbol,
28
+ FieldSymbol,
29
+ ModelSymbol,
30
+ NamespaceSymbol,
31
31
  PslSpan,
32
+ ResolvedAttribute,
33
+ SymbolTable,
32
34
  } from '@prisma-next/psl-parser';
35
+ import { nodePslSpan } from '@prisma-next/psl-parser';
36
+ import type { SourceFile } from '@prisma-next/psl-parser/syntax';
33
37
  import { blindCast } from '@prisma-next/utils/casts';
34
38
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
35
39
  import { deriveJsonSchema, derivePolymorphicJsonSchema } from './derive-json-schema';
@@ -45,41 +49,30 @@ import {
45
49
  } from './psl-helpers';
46
50
 
47
51
  export interface InterpretPslDocumentToMongoContractInput {
48
- readonly document: ParsePslDocumentResult;
52
+ readonly symbolTable: SymbolTable;
53
+ readonly sourceFile: SourceFile;
54
+ readonly sourceId: string;
49
55
  readonly scalarTypeDescriptors: ReadonlyMap<string, string>;
50
56
  readonly codecLookup?: CodecLookup;
57
+ readonly seedDiagnostics?: readonly ContractSourceDiagnostic[];
51
58
  }
52
59
 
53
60
  /**
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).
61
+ * Mongo's PSL surface binds the database from the connection string, so every
62
+ * explicit namespace block is invalid, including `namespace unbound { … }`.
68
63
  */
69
64
  function validateNamespaceBlocksForMongoTarget(input: {
70
- readonly namespaces: readonly PslNamespace[];
65
+ readonly namespaces: readonly NamespaceSymbol[];
71
66
  readonly sourceId: string;
67
+ readonly sourceFile: SourceFile;
72
68
  readonly diagnostics: ContractSourceDiagnostic[];
73
69
  }): void {
74
70
  for (const namespace of input.namespaces) {
75
- if (namespace.name === UNSPECIFIED_PSL_NAMESPACE_NAME) {
76
- continue;
77
- }
78
71
  input.diagnostics.push({
79
72
  code: 'PSL_UNSUPPORTED_NAMESPACE_BLOCK',
80
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).`,
81
74
  sourceId: input.sourceId,
82
- span: namespace.span,
75
+ span: nodePslSpan(namespace.node.syntax, input.sourceFile),
83
76
  });
84
77
  }
85
78
  }
@@ -101,16 +94,16 @@ function fkRelationPairKey(declaringModel: string, targetModel: string): string
101
94
  return `${declaringModel}::${targetModel}`;
102
95
  }
103
96
 
104
- function resolveFieldMappings(model: PslModel): FieldMappings {
97
+ function resolveFieldMappings(model: ModelSymbol): FieldMappings {
105
98
  const pslNameToMapped = new Map<string, string>();
106
- for (const field of model.fields) {
99
+ for (const field of Object.values(model.fields)) {
107
100
  const mapped = getMapName(field.attributes) ?? field.name;
108
101
  pslNameToMapped.set(field.name, mapped);
109
102
  }
110
103
  return { pslNameToMapped };
111
104
  }
112
105
 
113
- function resolveCollectionName(model: PslModel): string {
106
+ function resolveCollectionName(model: ModelSymbol): string {
114
107
  return getMapName(model.attributes) ?? lowerFirst(model.name);
115
108
  }
116
109
 
@@ -123,12 +116,12 @@ interface MongoModelEntry {
123
116
  readonly base?: CrossReference;
124
117
  }
125
118
 
126
- type DiscriminatorDeclaration = { readonly fieldName: string; readonly span: PslModel['span'] };
119
+ type DiscriminatorDeclaration = { readonly fieldName: string; readonly span: PslSpan };
127
120
  type BaseDeclaration = {
128
121
  readonly baseName: string;
129
122
  readonly value: string;
130
123
  readonly collectionName: string;
131
- readonly span: PslModel['span'];
124
+ readonly span: PslSpan;
132
125
  };
133
126
 
134
127
  function mongoCrossRef(modelName: string): CrossReference {
@@ -136,7 +129,7 @@ function mongoCrossRef(modelName: string): CrossReference {
136
129
  }
137
130
 
138
131
  function collectPolymorphismDeclarations(
139
- document: ParsePslDocumentResult,
132
+ models: readonly ModelSymbol[],
140
133
  sourceId: string,
141
134
  diagnostics: ContractSourceDiagnostic[],
142
135
  ): {
@@ -145,32 +138,31 @@ function collectPolymorphismDeclarations(
145
138
  } {
146
139
  const discriminatorDeclarations = new Map<string, DiscriminatorDeclaration>();
147
140
  const baseDeclarations = new Map<string, BaseDeclaration>();
148
- const allPslModels = document.ast.namespaces.flatMap((ns) => ns.models);
149
141
 
150
- for (const pslModel of allPslModels) {
151
- for (const attr of pslModel.attributes) {
142
+ for (const model of models) {
143
+ for (const attr of model.attributes) {
152
144
  if (attr.name === 'discriminator') {
153
145
  const fieldName = getPositionalArgument(attr);
154
146
  if (!fieldName) {
155
147
  diagnostics.push({
156
148
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
157
- message: `Model "${pslModel.name}" @@discriminator requires a field name argument`,
149
+ message: `Model "${model.name}" @@discriminator requires a field name argument`,
158
150
  sourceId,
159
151
  span: attr.span,
160
152
  });
161
153
  continue;
162
154
  }
163
- const discField = pslModel.fields.find((f) => f.name === fieldName);
155
+ const discField = model.fields[fieldName];
164
156
  if (discField && discField.typeName !== 'String') {
165
157
  diagnostics.push({
166
158
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
167
- 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}"`,
168
160
  sourceId,
169
161
  span: attr.span,
170
162
  });
171
163
  continue;
172
164
  }
173
- discriminatorDeclarations.set(pslModel.name, { fieldName, span: attr.span });
165
+ discriminatorDeclarations.set(model.name, { fieldName, span: attr.span });
174
166
  }
175
167
  if (attr.name === 'base') {
176
168
  const baseName = getPositionalArgument(attr, 0);
@@ -178,7 +170,7 @@ function collectPolymorphismDeclarations(
178
170
  if (!baseName || !rawValue) {
179
171
  diagnostics.push({
180
172
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
181
- 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`,
182
174
  sourceId,
183
175
  span: attr.span,
184
176
  });
@@ -188,14 +180,14 @@ function collectPolymorphismDeclarations(
188
180
  if (value === undefined) {
189
181
  diagnostics.push({
190
182
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
191
- 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`,
192
184
  sourceId,
193
185
  span: attr.span,
194
186
  });
195
187
  continue;
196
188
  }
197
- const collectionName = resolveCollectionName(pslModel);
198
- 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 });
199
191
  }
200
192
  }
201
193
  }
@@ -207,7 +199,7 @@ function resolvePolymorphism(input: {
207
199
  models: Record<string, MongoModelEntry>;
208
200
  roots: Record<string, CrossReference>;
209
201
  collections: Record<string, Record<string, unknown>>;
210
- document: ParsePslDocumentResult;
202
+ allModels: readonly ModelSymbol[];
211
203
  discriminatorDeclarations: Map<string, DiscriminatorDeclaration>;
212
204
  baseDeclarations: Map<string, BaseDeclaration>;
213
205
  modelNames: ReadonlySet<string>;
@@ -225,11 +217,10 @@ function resolvePolymorphism(input: {
225
217
  baseDeclarations,
226
218
  modelNames,
227
219
  sourceId,
228
- document,
220
+ allModels: allModelViews,
229
221
  indexSpans,
230
222
  modelIndexesByName,
231
223
  } = input;
232
- const allPslModels = document.ast.namespaces.flatMap((ns) => ns.models);
233
224
  let patched = input.models;
234
225
  let roots = input.roots;
235
226
  let collections = input.collections;
@@ -249,9 +240,9 @@ function resolvePolymorphism(input: {
249
240
  const model = patched[modelName];
250
241
  if (!model) continue;
251
242
 
252
- const pslModel = allPslModels.find((m) => m.name === modelName);
253
- const mappedDiscriminatorField = pslModel
254
- ? (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)
255
246
  : decl.fieldName;
256
247
 
257
248
  if (!Object.hasOwn(model.fields, mappedDiscriminatorField)) {
@@ -312,9 +303,9 @@ function resolvePolymorphism(input: {
312
303
  }
313
304
 
314
305
  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;
306
+ const variantModelView = allModelViews.find((m) => m.name === variantName);
307
+ if (!variantModelView) continue;
308
+ const hasExplicitMap = getMapName(variantModelView.attributes) !== undefined;
318
309
 
319
310
  if (hasExplicitMap && baseModel && baseDecl.collectionName !== baseModel.storage.collection) {
320
311
  diagnostics.push({
@@ -339,7 +330,7 @@ function resolvePolymorphism(input: {
339
330
  };
340
331
  }
341
332
 
342
- const variantCollectionName = resolveCollectionName(variantPslModel);
333
+ const variantCollectionName = resolveCollectionName(variantModelView);
343
334
  if (roots[variantCollectionName]?.model === variantName) {
344
335
  if (variantCollectionName === baseCollection && baseModel) {
345
336
  roots = { ...roots, [variantCollectionName]: mongoCrossRef(baseDecl.baseName) };
@@ -483,9 +474,7 @@ function parseJsonArg(raw: string | undefined): Record<string, unknown> | undefi
483
474
  return undefined;
484
475
  }
485
476
 
486
- function parseCollation(
487
- attr: import('@prisma-next/psl-parser').PslAttribute,
488
- ): CollationOptions | null | undefined {
477
+ function parseCollation(attr: ResolvedAttribute): CollationOptions | null | undefined {
489
478
  const locale = stripQuotesHelper(getNamedArgument(attr, 'collationLocale'));
490
479
  if (!locale) {
491
480
  const hasAnyCollationArg =
@@ -545,7 +534,7 @@ function parseProjectionList(
545
534
  }
546
535
 
547
536
  function collectIndexes(
548
- pslModel: PslModel,
537
+ pslModel: ModelSymbol,
549
538
  fieldMappings: FieldMappings,
550
539
  modelNames: ReadonlySet<string>,
551
540
  sourceId: string,
@@ -561,12 +550,12 @@ function collectIndexes(
561
550
  // rather than fieldMappings.pslNameToMapped because the latter contains
562
551
  // every PSL field including relation fields.
563
552
  const indexableFieldNames = new Set<string>();
564
- for (const f of pslModel.fields) {
553
+ for (const f of Object.values(pslModel.fields)) {
565
554
  if (modelNames.has(f.typeName)) continue;
566
555
  indexableFieldNames.add(f.name);
567
556
  }
568
557
 
569
- for (const field of pslModel.fields) {
558
+ for (const field of Object.values(pslModel.fields)) {
570
559
  if (modelNames.has(field.typeName)) continue;
571
560
  const uniqueAttr = getAttribute(field.attributes, 'unique');
572
561
  if (!uniqueAttr) continue;
@@ -800,7 +789,7 @@ function collectIndexes(
800
789
  return indexes;
801
790
  }
802
791
 
803
- function isRelationField(field: PslField, modelNames: ReadonlySet<string>): boolean {
792
+ function isRelationField(field: FieldSymbol, modelNames: ReadonlySet<string>): boolean {
804
793
  return modelNames.has(field.typeName);
805
794
  }
806
795
 
@@ -808,14 +797,14 @@ function isRelationField(field: PslField, modelNames: ReadonlySet<string>): bool
808
797
  const MONGO_OBJECT_ID_PSL_TYPE = 'ObjectId';
809
798
 
810
799
  function resolveFieldCodecId(
811
- field: PslField,
800
+ field: FieldSymbol,
812
801
  scalarTypeDescriptors: ReadonlyMap<string, string>,
813
802
  ): string | undefined {
814
803
  return scalarTypeDescriptors.get(field.typeName);
815
804
  }
816
805
 
817
806
  function resolveNonRelationField(
818
- field: PslField,
807
+ field: FieldSymbol,
819
808
  ownerName: string,
820
809
  compositeTypeNames: ReadonlySet<string>,
821
810
  scalarTypeDescriptors: ReadonlyMap<string, string>,
@@ -830,6 +819,11 @@ function resolveNonRelationField(
830
819
  return field.list ? { ...result, many: true } : result;
831
820
  }
832
821
 
822
+ // Avoid cascading unsupported-type diagnostics after invalid qualification.
823
+ if (field.malformedType) {
824
+ return undefined;
825
+ }
826
+
833
827
  const codecId = resolveFieldCodecId(field, scalarTypeDescriptors);
834
828
  if (!codecId) {
835
829
  diagnostics.push({
@@ -851,21 +845,18 @@ function resolveNonRelationField(
851
845
  export function interpretPslDocumentToMongoContract(
852
846
  input: InterpretPslDocumentToMongoContractInput,
853
847
  ): Result<Contract, ContractSourceDiagnostics> {
854
- const { document, scalarTypeDescriptors, codecLookup } = input;
855
- const sourceId = document.ast.sourceId;
856
- 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;
857
852
  validateNamespaceBlocksForMongoTarget({
858
- namespaces: document.ast.namespaces,
853
+ namespaces: Object.values(topLevel.namespaces),
859
854
  sourceId,
855
+ sourceFile,
860
856
  diagnostics,
861
857
  });
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);
858
+ const allModels: ModelSymbol[] = Object.values(topLevel.models);
859
+ const allCompositeTypes: CompositeTypeSymbol[] = Object.values(topLevel.compositeTypes);
869
860
  const modelNames = new Set(allModels.map((m) => m.name));
870
861
  const compositeTypeNames = new Set(allCompositeTypes.map((ct) => ct.name));
871
862
 
@@ -882,7 +873,7 @@ export function interpretPslDocumentToMongoContract(
882
873
  readonly targetModelName: string;
883
874
  readonly relationName?: string;
884
875
  readonly cardinality: '1:1' | '1:N';
885
- readonly field: PslField;
876
+ readonly field: FieldSymbol;
886
877
  }
887
878
  const backrelationCandidates: BackrelationCandidate[] = [];
888
879
 
@@ -893,7 +884,7 @@ export function interpretPslDocumentToMongoContract(
893
884
  const fields: Record<string, ContractField> = {};
894
885
  const relations: Record<string, ContractReferenceRelation> = {};
895
886
 
896
- for (const field of pslModel.fields) {
887
+ for (const field of Object.values(pslModel.fields)) {
897
888
  if (isRelationField(field, modelNames)) {
898
889
  const relation = parseRelationAttribute(field.attributes);
899
890
 
@@ -956,7 +947,9 @@ export function interpretPslDocumentToMongoContract(
956
947
  }
957
948
 
958
949
  const isVariantModel = pslModel.attributes.some((attr) => attr.name === 'base');
959
- 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
+ );
960
953
  // Variant models inherit the base's identity and are validated through their base.
961
954
  if (!isVariantModel) {
962
955
  if (!hasIdField) {
@@ -1011,7 +1004,7 @@ export function interpretPslDocumentToMongoContract(
1011
1004
  const valueObjects: Record<string, ContractValueObject> = {};
1012
1005
  for (const compositeType of allCompositeTypes) {
1013
1006
  const fields: Record<string, ContractField> = {};
1014
- for (const field of compositeType.fields) {
1007
+ for (const field of Object.values(compositeType.fields)) {
1015
1008
  const resolved = resolveNonRelationField(
1016
1009
  field,
1017
1010
  compositeType.name,
@@ -1078,7 +1071,7 @@ export function interpretPslDocumentToMongoContract(
1078
1071
  }
1079
1072
 
1080
1073
  const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations(
1081
- document,
1074
+ allModels,
1082
1075
  sourceId,
1083
1076
  diagnostics,
1084
1077
  );
@@ -1086,7 +1079,7 @@ export function interpretPslDocumentToMongoContract(
1086
1079
  models,
1087
1080
  roots,
1088
1081
  collections,
1089
- document,
1082
+ allModels,
1090
1083
  discriminatorDeclarations,
1091
1084
  baseDeclarations,
1092
1085
  modelNames,
package/src/provider.ts CHANGED
@@ -1,14 +1,30 @@
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: {
@@ -39,13 +55,26 @@ export function mongoContract(schemaPath: string, options?: MongoContractOptions
39
55
  });
40
56
  }
41
57
 
42
- const document = parsePslDocument({
43
- schema,
44
- 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,
45
64
  });
46
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
+
47
73
  const interpreted = interpretPslDocumentToMongoContract({
48
- document,
74
+ symbolTable,
75
+ sourceFile,
76
+ sourceId: schemaPath,
77
+ seedDiagnostics,
49
78
  scalarTypeDescriptors: context.scalarTypeDescriptors,
50
79
  codecLookup: context.codecLookup,
51
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