@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.
- package/dist/exports/provider.d.mts.map +1 -1
- package/dist/exports/provider.mjs +24 -7
- package/dist/exports/provider.mjs.map +1 -1
- package/dist/index.d.mts +7 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{interpreter-CNVgUGau.mjs → interpreter-DCiVHDlW.mjs} +73 -80
- package/dist/interpreter-DCiVHDlW.mjs.map +1 -0
- package/package.json +11 -9
- package/src/interpreter.ts +100 -113
- package/src/provider.ts +36 -6
- package/src/psl-helpers.ts +18 -13
- package/dist/interpreter-CNVgUGau.mjs.map +0 -1
package/src/interpreter.ts
CHANGED
|
@@ -16,19 +16,25 @@ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
|
16
16
|
import {
|
|
17
17
|
applyPolymorphicScopeToMongoIndex,
|
|
18
18
|
buildMongoNamespace,
|
|
19
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
*
|
|
53
|
-
*
|
|
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
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
149
|
-
for (const attr of
|
|
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 "${
|
|
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 =
|
|
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 "${
|
|
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(
|
|
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 "${
|
|
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 "${
|
|
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(
|
|
196
|
-
baseDeclarations.set(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
251
|
-
const mappedDiscriminatorField =
|
|
252
|
-
? (resolveFieldMappings(
|
|
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
|
|
314
|
-
if (!
|
|
315
|
-
const hasExplicitMap = getMapName(
|
|
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(
|
|
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:
|
|
492
|
+
const collation: CollationOptions = { locale };
|
|
502
493
|
const strength = parseNumericArg(getNamedArgument(attr, 'collationStrength'));
|
|
503
|
-
if (strength != null) collation
|
|
494
|
+
if (strength != null) collation.strength = strength;
|
|
504
495
|
const caseLevel = parseBooleanArg(getNamedArgument(attr, 'collationCaseLevel'));
|
|
505
|
-
if (caseLevel != null) collation
|
|
496
|
+
if (caseLevel != null) collation.caseLevel = caseLevel;
|
|
506
497
|
const caseFirst = stripQuotesHelper(getNamedArgument(attr, 'collationCaseFirst'));
|
|
507
|
-
if (caseFirst != null) collation
|
|
498
|
+
if (caseFirst != null) collation.caseFirst = caseFirst;
|
|
508
499
|
const numericOrdering = parseBooleanArg(getNamedArgument(attr, 'collationNumericOrdering'));
|
|
509
|
-
if (numericOrdering != null) collation
|
|
500
|
+
if (numericOrdering != null) collation.numericOrdering = numericOrdering;
|
|
510
501
|
const alternate = stripQuotesHelper(getNamedArgument(attr, 'collationAlternate'));
|
|
511
|
-
if (alternate != null) collation
|
|
502
|
+
if (alternate != null) collation.alternate = alternate;
|
|
512
503
|
const maxVariable = stripQuotesHelper(getNamedArgument(attr, 'collationMaxVariable'));
|
|
513
|
-
if (maxVariable != null) collation
|
|
504
|
+
if (maxVariable != null) collation.maxVariable = maxVariable;
|
|
514
505
|
const backwards = parseBooleanArg(getNamedArgument(attr, 'collationBackwards'));
|
|
515
|
-
if (backwards != null) collation
|
|
506
|
+
if (backwards != null) collation.backwards = backwards;
|
|
516
507
|
const normalization = parseBooleanArg(getNamedArgument(attr, 'collationNormalization'));
|
|
517
|
-
if (normalization != null) collation
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
|
853
|
-
const 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:
|
|
853
|
+
namespaces: Object.values(topLevel.namespaces),
|
|
857
854
|
sourceId,
|
|
855
|
+
sourceFile,
|
|
858
856
|
diagnostics,
|
|
859
857
|
});
|
|
860
|
-
|
|
861
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1129
|
+
const collectionInputs: Record<string, MongoCollectionInput> = {};
|
|
1135
1130
|
for (const [name, coll] of Object.entries(resolvedCollections)) {
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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:
|
|
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]:
|
|
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 {
|
|
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 =
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
74
|
+
symbolTable,
|
|
75
|
+
sourceFile,
|
|
76
|
+
sourceId: schemaPath,
|
|
77
|
+
seedDiagnostics,
|
|
48
78
|
scalarTypeDescriptors: context.scalarTypeDescriptors,
|
|
49
79
|
codecLookup: context.codecLookup,
|
|
50
80
|
});
|
package/src/psl-helpers.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
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 {
|
|
5
|
+
export { parseQuotedStringLiteral };
|
|
5
6
|
|
|
6
|
-
export function
|
|
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
|
|
80
|
+
attributes: readonly ResolvedAttribute[],
|
|
76
81
|
name: string,
|
|
77
|
-
):
|
|
82
|
+
): ResolvedAttribute | undefined {
|
|
78
83
|
return attributes.find((attr) => attr.name === name);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
export function getMapName(attributes: readonly
|
|
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
|
|
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:
|
|
103
|
-
let referencesArg:
|
|
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
|
|
122
|
-
...(fields
|
|
123
|
-
...(references
|
|
126
|
+
...ifDefined('relationName', relationName),
|
|
127
|
+
...ifDefined('fields', fields),
|
|
128
|
+
...ifDefined('references', references),
|
|
124
129
|
};
|
|
125
130
|
}
|
|
126
131
|
|