@prisma-next/mongo-contract-psl 0.14.0-dev.5 → 0.14.0-dev.50
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 +5 -0
- package/dist/exports/provider.d.mts.map +1 -1
- package/dist/exports/provider.mjs +26 -8
- package/dist/exports/provider.mjs.map +1 -1
- package/dist/index.d.mts +33 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{interpreter-Cj1vigKn.mjs → interpreter-D0vfRJ7f.mjs} +183 -85
- package/dist/interpreter-D0vfRJ7f.mjs.map +1 -0
- package/package.json +12 -10
- package/src/derive-json-schema.ts +31 -19
- package/src/exports/index.ts +5 -0
- package/src/interpreter.ts +256 -87
- package/src/provider.ts +39 -6
- package/src/psl-helpers.ts +18 -13
- package/dist/interpreter-Cj1vigKn.mjs.map +0 -1
package/src/interpreter.ts
CHANGED
|
@@ -5,12 +5,25 @@ 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 { errorEnumCodecNotInPackStack } from '@prisma-next/errors/control';
|
|
19
|
+
import type {
|
|
20
|
+
AuthoringContributions,
|
|
21
|
+
AuthoringEntityContext,
|
|
22
|
+
} from '@prisma-next/framework-components/authoring';
|
|
23
|
+
import {
|
|
24
|
+
instantiateAuthoringEntityType,
|
|
25
|
+
isAuthoringEntityTypeDescriptor,
|
|
26
|
+
} from '@prisma-next/framework-components/authoring';
|
|
14
27
|
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
15
28
|
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
16
29
|
import {
|
|
@@ -20,17 +33,25 @@ import {
|
|
|
20
33
|
MongoIndex,
|
|
21
34
|
type MongoIndexKeyDirection,
|
|
22
35
|
MongoStorage,
|
|
36
|
+
type MongoValueSetInput,
|
|
23
37
|
} from '@prisma-next/mongo-contract';
|
|
24
38
|
import { mongoContractCanonicalizationHooks } from '@prisma-next/mongo-contract/canonicalization-hooks';
|
|
25
39
|
import type { CollationOptions } from '@prisma-next/mongo-value/mongodb-types';
|
|
26
40
|
import type {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
CompositeTypeSymbol,
|
|
42
|
+
FieldSymbol,
|
|
43
|
+
ModelSymbol,
|
|
44
|
+
NamespaceSymbol,
|
|
45
|
+
PslExtensionBlock,
|
|
31
46
|
PslSpan,
|
|
47
|
+
ResolvedAttribute,
|
|
48
|
+
SymbolTable,
|
|
32
49
|
} from '@prisma-next/psl-parser';
|
|
50
|
+
import { nodePslSpan } from '@prisma-next/psl-parser';
|
|
51
|
+
import type { SourceFile } from '@prisma-next/psl-parser/syntax';
|
|
52
|
+
import { assertDefined } from '@prisma-next/utils/assertions';
|
|
33
53
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
54
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
34
55
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
35
56
|
import { deriveJsonSchema, derivePolymorphicJsonSchema } from './derive-json-schema';
|
|
36
57
|
import {
|
|
@@ -44,42 +65,49 @@ import {
|
|
|
44
65
|
parseRelationAttribute,
|
|
45
66
|
} from './psl-helpers';
|
|
46
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Encode an authored enum value to its codec-encoded JSON form via the codec resolved by id from the
|
|
70
|
+
* contract's codec lookup, so a non-identity `encodeJson` (permitted by the `mongoCodec` factory) is
|
|
71
|
+
* respected. Matches the TS builder's `encodeEnumValue`: the lookup is always threaded in production,
|
|
72
|
+
* and a codecId the lookup cannot resolve is a hard error — the enum uses a codec that is not part of
|
|
73
|
+
* the contract's pack stack.
|
|
74
|
+
*/
|
|
75
|
+
function encodeEnumValue(value: unknown, codecId: string, codecLookup: CodecLookup): JsonValue {
|
|
76
|
+
const codec = codecLookup.get(codecId);
|
|
77
|
+
if (!codec) {
|
|
78
|
+
throw errorEnumCodecNotInPackStack({ codecId });
|
|
79
|
+
}
|
|
80
|
+
return codec.encodeJson(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
47
83
|
export interface InterpretPslDocumentToMongoContractInput {
|
|
48
|
-
readonly
|
|
84
|
+
readonly symbolTable: SymbolTable;
|
|
85
|
+
readonly sourceFile: SourceFile;
|
|
86
|
+
readonly sourceId: string;
|
|
49
87
|
readonly scalarTypeDescriptors: ReadonlyMap<string, string>;
|
|
50
88
|
readonly codecLookup?: CodecLookup;
|
|
89
|
+
readonly seedDiagnostics?: readonly ContractSourceDiagnostic[];
|
|
90
|
+
readonly authoringContributions?: AuthoringContributions;
|
|
91
|
+
/** The target's default codec ids for an `enum` block that omits `@@type`. */
|
|
92
|
+
readonly enumInferenceCodecs?: { readonly text: string; readonly int: string };
|
|
51
93
|
}
|
|
52
94
|
|
|
53
95
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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).
|
|
96
|
+
* Mongo's PSL surface binds the database from the connection string, so every
|
|
97
|
+
* explicit namespace block is invalid, including `namespace unbound { … }`.
|
|
68
98
|
*/
|
|
69
99
|
function validateNamespaceBlocksForMongoTarget(input: {
|
|
70
|
-
readonly namespaces: readonly
|
|
100
|
+
readonly namespaces: readonly NamespaceSymbol[];
|
|
71
101
|
readonly sourceId: string;
|
|
102
|
+
readonly sourceFile: SourceFile;
|
|
72
103
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
73
104
|
}): void {
|
|
74
105
|
for (const namespace of input.namespaces) {
|
|
75
|
-
if (namespace.name === UNSPECIFIED_PSL_NAMESPACE_NAME) {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
106
|
input.diagnostics.push({
|
|
79
107
|
code: 'PSL_UNSUPPORTED_NAMESPACE_BLOCK',
|
|
80
108
|
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
109
|
sourceId: input.sourceId,
|
|
82
|
-
span: namespace.
|
|
110
|
+
span: nodePslSpan(namespace.node.syntax, input.sourceFile),
|
|
83
111
|
});
|
|
84
112
|
}
|
|
85
113
|
}
|
|
@@ -101,16 +129,16 @@ function fkRelationPairKey(declaringModel: string, targetModel: string): string
|
|
|
101
129
|
return `${declaringModel}::${targetModel}`;
|
|
102
130
|
}
|
|
103
131
|
|
|
104
|
-
function resolveFieldMappings(model:
|
|
132
|
+
function resolveFieldMappings(model: ModelSymbol): FieldMappings {
|
|
105
133
|
const pslNameToMapped = new Map<string, string>();
|
|
106
|
-
for (const field of model.fields) {
|
|
134
|
+
for (const field of Object.values(model.fields)) {
|
|
107
135
|
const mapped = getMapName(field.attributes) ?? field.name;
|
|
108
136
|
pslNameToMapped.set(field.name, mapped);
|
|
109
137
|
}
|
|
110
138
|
return { pslNameToMapped };
|
|
111
139
|
}
|
|
112
140
|
|
|
113
|
-
function resolveCollectionName(model:
|
|
141
|
+
function resolveCollectionName(model: ModelSymbol): string {
|
|
114
142
|
return getMapName(model.attributes) ?? lowerFirst(model.name);
|
|
115
143
|
}
|
|
116
144
|
|
|
@@ -123,12 +151,12 @@ interface MongoModelEntry {
|
|
|
123
151
|
readonly base?: CrossReference;
|
|
124
152
|
}
|
|
125
153
|
|
|
126
|
-
type DiscriminatorDeclaration = { readonly fieldName: string; readonly span:
|
|
154
|
+
type DiscriminatorDeclaration = { readonly fieldName: string; readonly span: PslSpan };
|
|
127
155
|
type BaseDeclaration = {
|
|
128
156
|
readonly baseName: string;
|
|
129
157
|
readonly value: string;
|
|
130
158
|
readonly collectionName: string;
|
|
131
|
-
readonly span:
|
|
159
|
+
readonly span: PslSpan;
|
|
132
160
|
};
|
|
133
161
|
|
|
134
162
|
function mongoCrossRef(modelName: string): CrossReference {
|
|
@@ -136,7 +164,7 @@ function mongoCrossRef(modelName: string): CrossReference {
|
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
function collectPolymorphismDeclarations(
|
|
139
|
-
|
|
167
|
+
models: readonly ModelSymbol[],
|
|
140
168
|
sourceId: string,
|
|
141
169
|
diagnostics: ContractSourceDiagnostic[],
|
|
142
170
|
): {
|
|
@@ -145,32 +173,31 @@ function collectPolymorphismDeclarations(
|
|
|
145
173
|
} {
|
|
146
174
|
const discriminatorDeclarations = new Map<string, DiscriminatorDeclaration>();
|
|
147
175
|
const baseDeclarations = new Map<string, BaseDeclaration>();
|
|
148
|
-
const allPslModels = document.ast.namespaces.flatMap((ns) => ns.models);
|
|
149
176
|
|
|
150
|
-
for (const
|
|
151
|
-
for (const attr of
|
|
177
|
+
for (const model of models) {
|
|
178
|
+
for (const attr of model.attributes) {
|
|
152
179
|
if (attr.name === 'discriminator') {
|
|
153
180
|
const fieldName = getPositionalArgument(attr);
|
|
154
181
|
if (!fieldName) {
|
|
155
182
|
diagnostics.push({
|
|
156
183
|
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
157
|
-
message: `Model "${
|
|
184
|
+
message: `Model "${model.name}" @@discriminator requires a field name argument`,
|
|
158
185
|
sourceId,
|
|
159
186
|
span: attr.span,
|
|
160
187
|
});
|
|
161
188
|
continue;
|
|
162
189
|
}
|
|
163
|
-
const discField =
|
|
190
|
+
const discField = model.fields[fieldName];
|
|
164
191
|
if (discField && discField.typeName !== 'String') {
|
|
165
192
|
diagnostics.push({
|
|
166
193
|
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
167
|
-
message: `Discriminator field "${fieldName}" on model "${
|
|
194
|
+
message: `Discriminator field "${fieldName}" on model "${model.name}" must be of type String, but is "${discField.typeName}"`,
|
|
168
195
|
sourceId,
|
|
169
196
|
span: attr.span,
|
|
170
197
|
});
|
|
171
198
|
continue;
|
|
172
199
|
}
|
|
173
|
-
discriminatorDeclarations.set(
|
|
200
|
+
discriminatorDeclarations.set(model.name, { fieldName, span: attr.span });
|
|
174
201
|
}
|
|
175
202
|
if (attr.name === 'base') {
|
|
176
203
|
const baseName = getPositionalArgument(attr, 0);
|
|
@@ -178,7 +205,7 @@ function collectPolymorphismDeclarations(
|
|
|
178
205
|
if (!baseName || !rawValue) {
|
|
179
206
|
diagnostics.push({
|
|
180
207
|
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
181
|
-
message: `Model "${
|
|
208
|
+
message: `Model "${model.name}" @@base requires two arguments: base model name and discriminator value`,
|
|
182
209
|
sourceId,
|
|
183
210
|
span: attr.span,
|
|
184
211
|
});
|
|
@@ -188,14 +215,14 @@ function collectPolymorphismDeclarations(
|
|
|
188
215
|
if (value === undefined) {
|
|
189
216
|
diagnostics.push({
|
|
190
217
|
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
191
|
-
message: `Model "${
|
|
218
|
+
message: `Model "${model.name}" @@base discriminator value must be a quoted string literal`,
|
|
192
219
|
sourceId,
|
|
193
220
|
span: attr.span,
|
|
194
221
|
});
|
|
195
222
|
continue;
|
|
196
223
|
}
|
|
197
|
-
const collectionName = resolveCollectionName(
|
|
198
|
-
baseDeclarations.set(
|
|
224
|
+
const collectionName = resolveCollectionName(model);
|
|
225
|
+
baseDeclarations.set(model.name, { baseName, value, collectionName, span: attr.span });
|
|
199
226
|
}
|
|
200
227
|
}
|
|
201
228
|
}
|
|
@@ -207,7 +234,7 @@ function resolvePolymorphism(input: {
|
|
|
207
234
|
models: Record<string, MongoModelEntry>;
|
|
208
235
|
roots: Record<string, CrossReference>;
|
|
209
236
|
collections: Record<string, Record<string, unknown>>;
|
|
210
|
-
|
|
237
|
+
allModels: readonly ModelSymbol[];
|
|
211
238
|
discriminatorDeclarations: Map<string, DiscriminatorDeclaration>;
|
|
212
239
|
baseDeclarations: Map<string, BaseDeclaration>;
|
|
213
240
|
modelNames: ReadonlySet<string>;
|
|
@@ -225,11 +252,10 @@ function resolvePolymorphism(input: {
|
|
|
225
252
|
baseDeclarations,
|
|
226
253
|
modelNames,
|
|
227
254
|
sourceId,
|
|
228
|
-
|
|
255
|
+
allModels: allModelViews,
|
|
229
256
|
indexSpans,
|
|
230
257
|
modelIndexesByName,
|
|
231
258
|
} = input;
|
|
232
|
-
const allPslModels = document.ast.namespaces.flatMap((ns) => ns.models);
|
|
233
259
|
let patched = input.models;
|
|
234
260
|
let roots = input.roots;
|
|
235
261
|
let collections = input.collections;
|
|
@@ -249,9 +275,9 @@ function resolvePolymorphism(input: {
|
|
|
249
275
|
const model = patched[modelName];
|
|
250
276
|
if (!model) continue;
|
|
251
277
|
|
|
252
|
-
const
|
|
253
|
-
const mappedDiscriminatorField =
|
|
254
|
-
? (resolveFieldMappings(
|
|
278
|
+
const modelView = allModelViews.find((m) => m.name === modelName);
|
|
279
|
+
const mappedDiscriminatorField = modelView
|
|
280
|
+
? (resolveFieldMappings(modelView).pslNameToMapped.get(decl.fieldName) ?? decl.fieldName)
|
|
255
281
|
: decl.fieldName;
|
|
256
282
|
|
|
257
283
|
if (!Object.hasOwn(model.fields, mappedDiscriminatorField)) {
|
|
@@ -312,9 +338,9 @@ function resolvePolymorphism(input: {
|
|
|
312
338
|
}
|
|
313
339
|
|
|
314
340
|
const baseModel = patched[baseDecl.baseName];
|
|
315
|
-
const
|
|
316
|
-
if (!
|
|
317
|
-
const hasExplicitMap = getMapName(
|
|
341
|
+
const variantModelView = allModelViews.find((m) => m.name === variantName);
|
|
342
|
+
if (!variantModelView) continue;
|
|
343
|
+
const hasExplicitMap = getMapName(variantModelView.attributes) !== undefined;
|
|
318
344
|
|
|
319
345
|
if (hasExplicitMap && baseModel && baseDecl.collectionName !== baseModel.storage.collection) {
|
|
320
346
|
diagnostics.push({
|
|
@@ -339,7 +365,7 @@ function resolvePolymorphism(input: {
|
|
|
339
365
|
};
|
|
340
366
|
}
|
|
341
367
|
|
|
342
|
-
const variantCollectionName = resolveCollectionName(
|
|
368
|
+
const variantCollectionName = resolveCollectionName(variantModelView);
|
|
343
369
|
if (roots[variantCollectionName]?.model === variantName) {
|
|
344
370
|
if (variantCollectionName === baseCollection && baseModel) {
|
|
345
371
|
roots = { ...roots, [variantCollectionName]: mongoCrossRef(baseDecl.baseName) };
|
|
@@ -483,9 +509,7 @@ function parseJsonArg(raw: string | undefined): Record<string, unknown> | undefi
|
|
|
483
509
|
return undefined;
|
|
484
510
|
}
|
|
485
511
|
|
|
486
|
-
function parseCollation(
|
|
487
|
-
attr: import('@prisma-next/psl-parser').PslAttribute,
|
|
488
|
-
): CollationOptions | null | undefined {
|
|
512
|
+
function parseCollation(attr: ResolvedAttribute): CollationOptions | null | undefined {
|
|
489
513
|
const locale = stripQuotesHelper(getNamedArgument(attr, 'collationLocale'));
|
|
490
514
|
if (!locale) {
|
|
491
515
|
const hasAnyCollationArg =
|
|
@@ -545,7 +569,7 @@ function parseProjectionList(
|
|
|
545
569
|
}
|
|
546
570
|
|
|
547
571
|
function collectIndexes(
|
|
548
|
-
pslModel:
|
|
572
|
+
pslModel: ModelSymbol,
|
|
549
573
|
fieldMappings: FieldMappings,
|
|
550
574
|
modelNames: ReadonlySet<string>,
|
|
551
575
|
sourceId: string,
|
|
@@ -561,12 +585,12 @@ function collectIndexes(
|
|
|
561
585
|
// rather than fieldMappings.pslNameToMapped because the latter contains
|
|
562
586
|
// every PSL field including relation fields.
|
|
563
587
|
const indexableFieldNames = new Set<string>();
|
|
564
|
-
for (const f of pslModel.fields) {
|
|
588
|
+
for (const f of Object.values(pslModel.fields)) {
|
|
565
589
|
if (modelNames.has(f.typeName)) continue;
|
|
566
590
|
indexableFieldNames.add(f.name);
|
|
567
591
|
}
|
|
568
592
|
|
|
569
|
-
for (const field of pslModel.fields) {
|
|
593
|
+
for (const field of Object.values(pslModel.fields)) {
|
|
570
594
|
if (modelNames.has(field.typeName)) continue;
|
|
571
595
|
const uniqueAttr = getAttribute(field.attributes, 'unique');
|
|
572
596
|
if (!uniqueAttr) continue;
|
|
@@ -800,7 +824,7 @@ function collectIndexes(
|
|
|
800
824
|
return indexes;
|
|
801
825
|
}
|
|
802
826
|
|
|
803
|
-
function isRelationField(field:
|
|
827
|
+
function isRelationField(field: FieldSymbol, modelNames: ReadonlySet<string>): boolean {
|
|
804
828
|
return modelNames.has(field.typeName);
|
|
805
829
|
}
|
|
806
830
|
|
|
@@ -808,17 +832,18 @@ function isRelationField(field: PslField, modelNames: ReadonlySet<string>): bool
|
|
|
808
832
|
const MONGO_OBJECT_ID_PSL_TYPE = 'ObjectId';
|
|
809
833
|
|
|
810
834
|
function resolveFieldCodecId(
|
|
811
|
-
field:
|
|
835
|
+
field: FieldSymbol,
|
|
812
836
|
scalarTypeDescriptors: ReadonlyMap<string, string>,
|
|
813
837
|
): string | undefined {
|
|
814
838
|
return scalarTypeDescriptors.get(field.typeName);
|
|
815
839
|
}
|
|
816
840
|
|
|
817
841
|
function resolveNonRelationField(
|
|
818
|
-
field:
|
|
842
|
+
field: FieldSymbol,
|
|
819
843
|
ownerName: string,
|
|
820
844
|
compositeTypeNames: ReadonlySet<string>,
|
|
821
845
|
scalarTypeDescriptors: ReadonlyMap<string, string>,
|
|
846
|
+
codecIdByEnumName: ReadonlyMap<string, string>,
|
|
822
847
|
sourceId: string,
|
|
823
848
|
diagnostics: ContractSourceDiagnostic[],
|
|
824
849
|
): ContractField | undefined {
|
|
@@ -830,6 +855,29 @@ function resolveNonRelationField(
|
|
|
830
855
|
return field.list ? { ...result, many: true } : result;
|
|
831
856
|
}
|
|
832
857
|
|
|
858
|
+
// If this field's declared type is a known enum name, treat the field as a scalar
|
|
859
|
+
// with that enum's codec and stamp the domain valueSet ref.
|
|
860
|
+
const enumCodecId = codecIdByEnumName.get(field.typeName);
|
|
861
|
+
if (enumCodecId !== undefined) {
|
|
862
|
+
const valueSet: ValueSetRef = {
|
|
863
|
+
plane: 'domain',
|
|
864
|
+
entityKind: 'enum',
|
|
865
|
+
namespaceId: UNBOUND_NAMESPACE_ID,
|
|
866
|
+
entityName: field.typeName,
|
|
867
|
+
};
|
|
868
|
+
const result: ContractField = {
|
|
869
|
+
type: { kind: 'scalar', codecId: enumCodecId },
|
|
870
|
+
nullable: field.optional,
|
|
871
|
+
valueSet,
|
|
872
|
+
};
|
|
873
|
+
return field.list ? { ...result, many: true } : result;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Avoid cascading unsupported-type diagnostics after invalid qualification.
|
|
877
|
+
if (field.malformedType) {
|
|
878
|
+
return undefined;
|
|
879
|
+
}
|
|
880
|
+
|
|
833
881
|
const codecId = resolveFieldCodecId(field, scalarTypeDescriptors);
|
|
834
882
|
if (!codecId) {
|
|
835
883
|
diagnostics.push({
|
|
@@ -848,27 +896,106 @@ function resolveNonRelationField(
|
|
|
848
896
|
return field.list ? { ...result, many: true } : result;
|
|
849
897
|
}
|
|
850
898
|
|
|
899
|
+
function processEnumDeclarations(input: {
|
|
900
|
+
readonly enumBlocks: readonly PslExtensionBlock[];
|
|
901
|
+
readonly sourceId: string;
|
|
902
|
+
readonly authoringContributions: AuthoringContributions | undefined;
|
|
903
|
+
readonly entityContext: AuthoringEntityContext;
|
|
904
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
905
|
+
}): Record<string, ContractEnum> {
|
|
906
|
+
const builtEnums: Record<string, ContractEnum> = {};
|
|
907
|
+
|
|
908
|
+
if (input.enumBlocks.length === 0) return builtEnums;
|
|
909
|
+
|
|
910
|
+
const enumDescriptor =
|
|
911
|
+
input.authoringContributions?.entityTypes?.['enum'] !== undefined &&
|
|
912
|
+
isAuthoringEntityTypeDescriptor(input.authoringContributions.entityTypes['enum'])
|
|
913
|
+
? input.authoringContributions.entityTypes['enum']
|
|
914
|
+
: undefined;
|
|
915
|
+
|
|
916
|
+
if (!enumDescriptor) {
|
|
917
|
+
for (const decl of input.enumBlocks) {
|
|
918
|
+
input.diagnostics.push({
|
|
919
|
+
code: 'PSL_ENUM_MISSING_FACTORY',
|
|
920
|
+
message: `enum "${decl.name}" requires an "enum" entityType factory in the active authoring contributions`,
|
|
921
|
+
sourceId: input.sourceId,
|
|
922
|
+
span: decl.span,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
return builtEnums;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
for (const decl of input.enumBlocks) {
|
|
929
|
+
const handle = instantiateAuthoringEntityType<EnumTypeHandle | undefined>(
|
|
930
|
+
'enum',
|
|
931
|
+
enumDescriptor,
|
|
932
|
+
[decl],
|
|
933
|
+
input.entityContext,
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
if (handle === undefined || handle === null) continue;
|
|
937
|
+
|
|
938
|
+
builtEnums[decl.name] = {
|
|
939
|
+
codecId: handle.codecId,
|
|
940
|
+
members: handle.enumMembers.map((m) => ({
|
|
941
|
+
name: m.name,
|
|
942
|
+
value: blindCast<JsonValue, 'factory-validated enum members are JsonValue-compatible'>(
|
|
943
|
+
m.value,
|
|
944
|
+
),
|
|
945
|
+
})),
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return builtEnums;
|
|
950
|
+
}
|
|
951
|
+
|
|
851
952
|
export function interpretPslDocumentToMongoContract(
|
|
852
953
|
input: InterpretPslDocumentToMongoContractInput,
|
|
853
954
|
): Result<Contract, ContractSourceDiagnostics> {
|
|
854
|
-
const {
|
|
855
|
-
const sourceId =
|
|
856
|
-
const diagnostics: ContractSourceDiagnostic[] = [];
|
|
955
|
+
const { symbolTable, sourceFile, scalarTypeDescriptors, codecLookup } = input;
|
|
956
|
+
const sourceId = input.sourceId;
|
|
957
|
+
const diagnostics: ContractSourceDiagnostic[] = [...(input.seedDiagnostics ?? [])];
|
|
958
|
+
const topLevel = symbolTable.topLevel;
|
|
857
959
|
validateNamespaceBlocksForMongoTarget({
|
|
858
|
-
namespaces:
|
|
960
|
+
namespaces: Object.values(topLevel.namespaces),
|
|
859
961
|
sourceId,
|
|
962
|
+
sourceFile,
|
|
860
963
|
diagnostics,
|
|
861
964
|
});
|
|
862
|
-
|
|
863
|
-
|
|
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);
|
|
965
|
+
const allModels: ModelSymbol[] = Object.values(topLevel.models);
|
|
966
|
+
const allCompositeTypes: CompositeTypeSymbol[] = Object.values(topLevel.compositeTypes);
|
|
869
967
|
const modelNames = new Set(allModels.map((m) => m.name));
|
|
870
968
|
const compositeTypeNames = new Set(allCompositeTypes.map((ct) => ct.name));
|
|
871
969
|
|
|
970
|
+
const topLevelEnumBlocks = Object.values(topLevel.blocks)
|
|
971
|
+
.filter((b) => b.keyword === 'enum')
|
|
972
|
+
.map((b) => b.block);
|
|
973
|
+
|
|
974
|
+
const builtEnums = processEnumDeclarations({
|
|
975
|
+
enumBlocks: topLevelEnumBlocks,
|
|
976
|
+
sourceId,
|
|
977
|
+
authoringContributions: input.authoringContributions,
|
|
978
|
+
entityContext: {
|
|
979
|
+
family: 'mongo',
|
|
980
|
+
target: 'mongo',
|
|
981
|
+
...ifDefined('enumInferenceCodecs', input.enumInferenceCodecs),
|
|
982
|
+
...ifDefined('codecLookup', codecLookup),
|
|
983
|
+
sourceId,
|
|
984
|
+
diagnostics: {
|
|
985
|
+
push: (d) => {
|
|
986
|
+
diagnostics.push(
|
|
987
|
+
blindCast<ContractSourceDiagnostic, 'sink diagnostics are span-compatible'>(d),
|
|
988
|
+
);
|
|
989
|
+
},
|
|
990
|
+
},
|
|
991
|
+
},
|
|
992
|
+
diagnostics,
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
const codecIdByEnumName: Map<string, string> = new Map(
|
|
996
|
+
Object.entries(builtEnums).map(([name, e]) => [name, e.codecId]),
|
|
997
|
+
);
|
|
998
|
+
|
|
872
999
|
const models: Record<string, MongoModelEntry> = {};
|
|
873
1000
|
const collections: Record<string, Record<string, unknown>> = {};
|
|
874
1001
|
const roots: Record<string, CrossReference> = {};
|
|
@@ -882,7 +1009,7 @@ export function interpretPslDocumentToMongoContract(
|
|
|
882
1009
|
readonly targetModelName: string;
|
|
883
1010
|
readonly relationName?: string;
|
|
884
1011
|
readonly cardinality: '1:1' | '1:N';
|
|
885
|
-
readonly field:
|
|
1012
|
+
readonly field: FieldSymbol;
|
|
886
1013
|
}
|
|
887
1014
|
const backrelationCandidates: BackrelationCandidate[] = [];
|
|
888
1015
|
|
|
@@ -893,7 +1020,7 @@ export function interpretPslDocumentToMongoContract(
|
|
|
893
1020
|
const fields: Record<string, ContractField> = {};
|
|
894
1021
|
const relations: Record<string, ContractReferenceRelation> = {};
|
|
895
1022
|
|
|
896
|
-
for (const field of pslModel.fields) {
|
|
1023
|
+
for (const field of Object.values(pslModel.fields)) {
|
|
897
1024
|
if (isRelationField(field, modelNames)) {
|
|
898
1025
|
const relation = parseRelationAttribute(field.attributes);
|
|
899
1026
|
|
|
@@ -902,9 +1029,7 @@ export function interpretPslDocumentToMongoContract(
|
|
|
902
1029
|
modelName: pslModel.name,
|
|
903
1030
|
fieldName: field.name,
|
|
904
1031
|
targetModelName: field.typeName,
|
|
905
|
-
...(relation?.relationName
|
|
906
|
-
? { relationName: relation.relationName }
|
|
907
|
-
: {}),
|
|
1032
|
+
...ifDefined('relationName', relation?.relationName),
|
|
908
1033
|
cardinality: field.list ? '1:N' : '1:1',
|
|
909
1034
|
field,
|
|
910
1035
|
});
|
|
@@ -933,7 +1058,7 @@ export function interpretPslDocumentToMongoContract(
|
|
|
933
1058
|
declaringModel: pslModel.name,
|
|
934
1059
|
fieldName: field.name,
|
|
935
1060
|
targetModel: field.typeName,
|
|
936
|
-
...(
|
|
1061
|
+
...ifDefined('relationName', relation.relationName),
|
|
937
1062
|
localFields: localMapped,
|
|
938
1063
|
targetFields: targetMapped,
|
|
939
1064
|
});
|
|
@@ -946,6 +1071,7 @@ export function interpretPslDocumentToMongoContract(
|
|
|
946
1071
|
pslModel.name,
|
|
947
1072
|
compositeTypeNames,
|
|
948
1073
|
scalarTypeDescriptors,
|
|
1074
|
+
codecIdByEnumName,
|
|
949
1075
|
sourceId,
|
|
950
1076
|
diagnostics,
|
|
951
1077
|
);
|
|
@@ -956,7 +1082,9 @@ export function interpretPslDocumentToMongoContract(
|
|
|
956
1082
|
}
|
|
957
1083
|
|
|
958
1084
|
const isVariantModel = pslModel.attributes.some((attr) => attr.name === 'base');
|
|
959
|
-
const hasIdField = pslModel.fields.some(
|
|
1085
|
+
const hasIdField = Object.values(pslModel.fields).some(
|
|
1086
|
+
(f) => getAttribute(f.attributes, 'id') !== undefined,
|
|
1087
|
+
);
|
|
960
1088
|
// Variant models inherit the base's identity and are validated through their base.
|
|
961
1089
|
if (!isVariantModel) {
|
|
962
1090
|
if (!hasIdField) {
|
|
@@ -1011,12 +1139,13 @@ export function interpretPslDocumentToMongoContract(
|
|
|
1011
1139
|
const valueObjects: Record<string, ContractValueObject> = {};
|
|
1012
1140
|
for (const compositeType of allCompositeTypes) {
|
|
1013
1141
|
const fields: Record<string, ContractField> = {};
|
|
1014
|
-
for (const field of compositeType.fields) {
|
|
1142
|
+
for (const field of Object.values(compositeType.fields)) {
|
|
1015
1143
|
const resolved = resolveNonRelationField(
|
|
1016
1144
|
field,
|
|
1017
1145
|
compositeType.name,
|
|
1018
1146
|
compositeTypeNames,
|
|
1019
1147
|
scalarTypeDescriptors,
|
|
1148
|
+
codecIdByEnumName,
|
|
1020
1149
|
sourceId,
|
|
1021
1150
|
diagnostics,
|
|
1022
1151
|
);
|
|
@@ -1078,7 +1207,7 @@ export function interpretPslDocumentToMongoContract(
|
|
|
1078
1207
|
}
|
|
1079
1208
|
|
|
1080
1209
|
const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations(
|
|
1081
|
-
|
|
1210
|
+
allModels,
|
|
1082
1211
|
sourceId,
|
|
1083
1212
|
diagnostics,
|
|
1084
1213
|
);
|
|
@@ -1086,7 +1215,7 @@ export function interpretPslDocumentToMongoContract(
|
|
|
1086
1215
|
models,
|
|
1087
1216
|
roots,
|
|
1088
1217
|
collections,
|
|
1089
|
-
|
|
1218
|
+
allModels,
|
|
1090
1219
|
discriminatorDeclarations,
|
|
1091
1220
|
baseDeclarations,
|
|
1092
1221
|
modelNames,
|
|
@@ -1105,6 +1234,28 @@ export function interpretPslDocumentToMongoContract(
|
|
|
1105
1234
|
const resolvedModels = polyResult.models;
|
|
1106
1235
|
const resolvedCollections = polyResult.collections;
|
|
1107
1236
|
|
|
1237
|
+
// The storage value set is the source of truth for both the emit typing and the validator's
|
|
1238
|
+
// `enum` keyword. Built once, ahead of validator derivation, from each enum's codec-encoded member
|
|
1239
|
+
// values (mirroring SQL's build-contract). Encoding needs the codec lookup; production always
|
|
1240
|
+
// threads it (the CLI control stack supplies it), so its absence when enums exist is a wiring bug,
|
|
1241
|
+
// not a runtime input to tolerate.
|
|
1242
|
+
const storageValueSets: Record<string, MongoValueSetInput> = {};
|
|
1243
|
+
const enumEntries = Object.entries(builtEnums);
|
|
1244
|
+
if (enumEntries.length > 0) {
|
|
1245
|
+
assertDefined(
|
|
1246
|
+
codecLookup,
|
|
1247
|
+
'Mongo PSL interpretation requires a codec lookup to encode enum values',
|
|
1248
|
+
);
|
|
1249
|
+
for (const [enumName, builtEnum] of enumEntries) {
|
|
1250
|
+
storageValueSets[enumName] = {
|
|
1251
|
+
kind: 'valueSet',
|
|
1252
|
+
values: builtEnum.members.map((m) =>
|
|
1253
|
+
encodeEnumValue(m.value, builtEnum.codecId, codecLookup),
|
|
1254
|
+
),
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1108
1259
|
for (const [, modelEntry] of Object.entries(resolvedModels)) {
|
|
1109
1260
|
if (modelEntry.base) continue;
|
|
1110
1261
|
|
|
@@ -1125,9 +1276,15 @@ export function interpretPslDocumentToMongoContract(
|
|
|
1125
1276
|
variantEntries,
|
|
1126
1277
|
valueObjects,
|
|
1127
1278
|
codecLookup,
|
|
1279
|
+
storageValueSets,
|
|
1128
1280
|
);
|
|
1129
1281
|
} else {
|
|
1130
|
-
coll['validator'] = deriveJsonSchema(
|
|
1282
|
+
coll['validator'] = deriveJsonSchema(
|
|
1283
|
+
modelEntry.fields,
|
|
1284
|
+
valueObjects,
|
|
1285
|
+
codecLookup,
|
|
1286
|
+
storageValueSets,
|
|
1287
|
+
);
|
|
1131
1288
|
}
|
|
1132
1289
|
}
|
|
1133
1290
|
|
|
@@ -1144,18 +1301,27 @@ export function interpretPslDocumentToMongoContract(
|
|
|
1144
1301
|
'arktype-validated JSON shapes satisfy MongoCollectionInput by construction'
|
|
1145
1302
|
>(raw);
|
|
1146
1303
|
}
|
|
1304
|
+
const hasValueSets = Object.keys(storageValueSets).length > 0;
|
|
1305
|
+
|
|
1147
1306
|
const unboundNamespace = buildMongoNamespace({
|
|
1148
1307
|
id: UNBOUND_NAMESPACE_ID,
|
|
1149
|
-
entries: {
|
|
1308
|
+
entries: {
|
|
1309
|
+
collection: collectionInputs,
|
|
1310
|
+
...(hasValueSets ? { valueSet: storageValueSets } : {}),
|
|
1311
|
+
},
|
|
1150
1312
|
});
|
|
1151
|
-
// Hash the constructed (normalized)
|
|
1152
|
-
//
|
|
1153
|
-
// constructed form.
|
|
1313
|
+
// Hash the constructed (normalized) entries, not the raw input literals —
|
|
1314
|
+
// persisted storageHash values were computed over the constructed form.
|
|
1154
1315
|
const storageWithoutHash = {
|
|
1155
1316
|
namespaces: {
|
|
1156
1317
|
[UNBOUND_NAMESPACE_ID]: {
|
|
1157
1318
|
id: UNBOUND_NAMESPACE_ID,
|
|
1158
|
-
entries: {
|
|
1319
|
+
entries: {
|
|
1320
|
+
collection: unboundNamespace.entries.collection,
|
|
1321
|
+
...(unboundNamespace.entries.valueSet !== undefined
|
|
1322
|
+
? { valueSet: unboundNamespace.entries.valueSet }
|
|
1323
|
+
: {}),
|
|
1324
|
+
},
|
|
1159
1325
|
},
|
|
1160
1326
|
},
|
|
1161
1327
|
};
|
|
@@ -1173,6 +1339,8 @@ export function interpretPslDocumentToMongoContract(
|
|
|
1173
1339
|
}) as Contract['storage'];
|
|
1174
1340
|
const capabilities: Record<string, Record<string, boolean>> = {};
|
|
1175
1341
|
|
|
1342
|
+
const hasEnums = Object.keys(builtEnums).length > 0;
|
|
1343
|
+
|
|
1176
1344
|
return ok({
|
|
1177
1345
|
targetFamily,
|
|
1178
1346
|
target,
|
|
@@ -1182,6 +1350,7 @@ export function interpretPslDocumentToMongoContract(
|
|
|
1182
1350
|
[UNBOUND_NAMESPACE_ID]: {
|
|
1183
1351
|
models: polyResult.models,
|
|
1184
1352
|
...(Object.keys(valueObjects).length > 0 ? { valueObjects } : {}),
|
|
1353
|
+
...(hasEnums ? { enum: builtEnums } : {}),
|
|
1185
1354
|
},
|
|
1186
1355
|
},
|
|
1187
1356
|
},
|