import type { ContractSourceDiagnostic, ContractSourceDiagnostics, } from '@prisma-next/config/config-types'; import { computeProfileHash, computeStorageHash } from '@prisma-next/contract/hashing'; import { type Contract, type ContractEnum, type ContractField, type ContractReferenceRelation, type ContractValueObject, type CrossReference, crossRef, type JsonValue, type ValueSetRef, } from '@prisma-next/contract/types'; import type { EnumTypeHandle } from '@prisma-next/contract-authoring'; import { errorEnumCodecNotInPackStack } from '@prisma-next/errors/control'; import type { AuthoringContributions, AuthoringEntityContext, } from '@prisma-next/framework-components/authoring'; import { instantiateAuthoringEntityType, isAuthoringEntityTypeDescriptor, } from '@prisma-next/framework-components/authoring'; import type { CodecLookup } from '@prisma-next/framework-components/codec'; import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir'; import { applyPolymorphicScopeToMongoIndex, buildMongoNamespace, type MongoCollectionInput, MongoIndex, type MongoIndexKeyDirection, MongoStorage, type MongoValueSetInput, } from '@prisma-next/mongo-contract'; import { mongoContractCanonicalizationHooks } from '@prisma-next/mongo-contract/canonicalization-hooks'; import type { CollationOptions } from '@prisma-next/mongo-value/mongodb-types'; import type { CompositeTypeSymbol, FieldSymbol, ModelSymbol, NamespaceSymbol, PslExtensionBlock, PslSpan, ResolvedAttribute, SymbolTable, } from '@prisma-next/psl-parser'; import { nodePslSpan } from '@prisma-next/psl-parser'; import type { SourceFile } from '@prisma-next/psl-parser/syntax'; import { assertDefined } from '@prisma-next/utils/assertions'; import { blindCast } from '@prisma-next/utils/casts'; import { ifDefined } from '@prisma-next/utils/defined'; import { notOk, ok, type Result } from '@prisma-next/utils/result'; import { deriveJsonSchema, derivePolymorphicJsonSchema } from './derive-json-schema'; import { getAttribute, getMapName, getNamedArgument, getPositionalArgument, lowerFirst, parseIndexFieldList, parseQuotedStringLiteral, parseRelationAttribute, } from './psl-helpers'; /** * Encode an authored enum value to its codec-encoded JSON form via the codec resolved by id from the * contract's codec lookup, so a non-identity `encodeJson` (permitted by the `mongoCodec` factory) is * respected. Matches the TS builder's `encodeEnumValue`: the lookup is always threaded in production, * and a codecId the lookup cannot resolve is a hard error — the enum uses a codec that is not part of * the contract's pack stack. */ function encodeEnumValue(value: unknown, codecId: string, codecLookup: CodecLookup): JsonValue { const codec = codecLookup.get(codecId); if (!codec) { throw errorEnumCodecNotInPackStack({ codecId }); } return codec.encodeJson(value); } export interface InterpretPslDocumentToMongoContractInput { readonly symbolTable: SymbolTable; readonly sourceFile: SourceFile; readonly sourceId: string; readonly scalarTypeDescriptors: ReadonlyMap; readonly codecLookup?: CodecLookup; readonly seedDiagnostics?: readonly ContractSourceDiagnostic[]; readonly authoringContributions?: AuthoringContributions; /** The target's default codec ids for an `enum` block that omits `@@type`. */ readonly enumInferenceCodecs?: { readonly text: string; readonly int: string }; } /** * Mongo's PSL surface binds the database from the connection string, so every * explicit namespace block is invalid, including `namespace unbound { … }`. */ function validateNamespaceBlocksForMongoTarget(input: { readonly namespaces: readonly NamespaceSymbol[]; readonly sourceId: string; readonly sourceFile: SourceFile; readonly diagnostics: ContractSourceDiagnostic[]; }): void { for (const namespace of input.namespaces) { input.diagnostics.push({ code: 'PSL_UNSUPPORTED_NAMESPACE_BLOCK', 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).`, sourceId: input.sourceId, span: nodePslSpan(namespace.node.syntax, input.sourceFile), }); } } interface FieldMappings { readonly pslNameToMapped: Map; } interface FkRelation { readonly declaringModel: string; readonly fieldName: string; readonly targetModel: string; readonly relationName?: string; readonly localFields: readonly string[]; readonly targetFields: readonly string[]; } function fkRelationPairKey(declaringModel: string, targetModel: string): string { return `${declaringModel}::${targetModel}`; } function resolveFieldMappings(model: ModelSymbol): FieldMappings { const pslNameToMapped = new Map(); for (const field of Object.values(model.fields)) { const mapped = getMapName(field.attributes) ?? field.name; pslNameToMapped.set(field.name, mapped); } return { pslNameToMapped }; } function resolveCollectionName(model: ModelSymbol): string { return getMapName(model.attributes) ?? lowerFirst(model.name); } interface MongoModelEntry { readonly fields: Record; readonly relations: Record; readonly storage: { readonly collection: string }; readonly discriminator?: { readonly field: string }; readonly variants?: Record; readonly base?: CrossReference; } type DiscriminatorDeclaration = { readonly fieldName: string; readonly span: PslSpan }; type BaseDeclaration = { readonly baseName: string; readonly value: string; readonly collectionName: string; readonly span: PslSpan; }; function mongoCrossRef(modelName: string): CrossReference { return crossRef(modelName, UNBOUND_NAMESPACE_ID); } function collectPolymorphismDeclarations( models: readonly ModelSymbol[], sourceId: string, diagnostics: ContractSourceDiagnostic[], ): { discriminatorDeclarations: Map; baseDeclarations: Map; } { const discriminatorDeclarations = new Map(); const baseDeclarations = new Map(); for (const model of models) { for (const attr of model.attributes) { if (attr.name === 'discriminator') { const fieldName = getPositionalArgument(attr); if (!fieldName) { diagnostics.push({ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT', message: `Model "${model.name}" @@discriminator requires a field name argument`, sourceId, span: attr.span, }); continue; } const discField = model.fields[fieldName]; if (discField && discField.typeName !== 'String') { diagnostics.push({ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT', message: `Discriminator field "${fieldName}" on model "${model.name}" must be of type String, but is "${discField.typeName}"`, sourceId, span: attr.span, }); continue; } discriminatorDeclarations.set(model.name, { fieldName, span: attr.span }); } if (attr.name === 'base') { const baseName = getPositionalArgument(attr, 0); const rawValue = getPositionalArgument(attr, 1); if (!baseName || !rawValue) { diagnostics.push({ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT', message: `Model "${model.name}" @@base requires two arguments: base model name and discriminator value`, sourceId, span: attr.span, }); continue; } const value = parseQuotedStringLiteral(rawValue); if (value === undefined) { diagnostics.push({ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT', message: `Model "${model.name}" @@base discriminator value must be a quoted string literal`, sourceId, span: attr.span, }); continue; } const collectionName = resolveCollectionName(model); baseDeclarations.set(model.name, { baseName, value, collectionName, span: attr.span }); } } } return { discriminatorDeclarations, baseDeclarations }; } function resolvePolymorphism(input: { models: Record; roots: Record; collections: Record>; allModels: readonly ModelSymbol[]; discriminatorDeclarations: Map; baseDeclarations: Map; modelNames: ReadonlySet; indexSpans: Map; modelIndexesByName: Map; sourceId: string; }): { models: Record; roots: Record; collections: Record>; diagnostics: ContractSourceDiagnostic[]; } { const { discriminatorDeclarations, baseDeclarations, modelNames, sourceId, allModels: allModelViews, indexSpans, modelIndexesByName, } = input; let patched = input.models; let roots = input.roots; let collections = input.collections; const diagnostics: ContractSourceDiagnostic[] = []; for (const [modelName, decl] of discriminatorDeclarations) { if (baseDeclarations.has(modelName)) { diagnostics.push({ code: 'PSL_DISCRIMINATOR_AND_BASE', message: `Model "${modelName}" cannot have both @@discriminator and @@base`, sourceId, span: decl.span, }); continue; } const model = patched[modelName]; if (!model) continue; const modelView = allModelViews.find((m) => m.name === modelName); const mappedDiscriminatorField = modelView ? (resolveFieldMappings(modelView).pslNameToMapped.get(decl.fieldName) ?? decl.fieldName) : decl.fieldName; if (!Object.hasOwn(model.fields, mappedDiscriminatorField)) { diagnostics.push({ code: 'PSL_DISCRIMINATOR_FIELD_NOT_FOUND', message: `Discriminator field "${decl.fieldName}" is not a field on model "${modelName}"`, sourceId, span: decl.span, }); continue; } const variants: Record = {}; for (const [variantName, baseDecl] of baseDeclarations) { if (baseDecl.baseName !== modelName) continue; variants[variantName] = { value: baseDecl.value }; } if (Object.keys(variants).length === 0) { diagnostics.push({ code: 'PSL_ORPHANED_DISCRIMINATOR', message: `Model "${modelName}" has @@discriminator but no variant models declare @@base(${modelName}, ...)`, sourceId, span: decl.span, }); continue; } patched = { ...patched, [modelName]: { ...model, discriminator: { field: mappedDiscriminatorField }, variants }, }; } for (const [variantName, baseDecl] of baseDeclarations) { if (!modelNames.has(baseDecl.baseName)) { diagnostics.push({ code: 'PSL_BASE_TARGET_NOT_FOUND', message: `Model "${variantName}" @@base references non-existent model "${baseDecl.baseName}"`, sourceId, span: baseDecl.span, }); continue; } if (!discriminatorDeclarations.has(baseDecl.baseName)) { diagnostics.push({ code: 'PSL_ORPHANED_BASE', message: `Model "${variantName}" declares @@base(${baseDecl.baseName}, ...) but "${baseDecl.baseName}" has no @@discriminator`, sourceId, span: baseDecl.span, }); continue; } if (discriminatorDeclarations.has(variantName)) { continue; } const baseModel = patched[baseDecl.baseName]; const variantModelView = allModelViews.find((m) => m.name === variantName); if (!variantModelView) continue; const hasExplicitMap = getMapName(variantModelView.attributes) !== undefined; if (hasExplicitMap && baseModel && baseDecl.collectionName !== baseModel.storage.collection) { diagnostics.push({ code: 'PSL_MONGO_VARIANT_SEPARATE_COLLECTION', message: `Mongo variant "${variantName}" cannot use a different collection than its base "${baseDecl.baseName}". Mongo only supports single-collection polymorphism.`, sourceId, span: baseDecl.span, }); continue; } const baseCollection = baseModel?.storage.collection ?? baseDecl.collectionName; const variantModel = patched[variantName]; if (variantModel) { patched = { ...patched, [variantName]: { ...variantModel, base: mongoCrossRef(baseDecl.baseName), storage: { collection: baseCollection }, }, }; } const variantCollectionName = resolveCollectionName(variantModelView); if (roots[variantCollectionName]?.model === variantName) { if (variantCollectionName === baseCollection && baseModel) { roots = { ...roots, [variantCollectionName]: mongoCrossRef(baseDecl.baseName) }; } else { roots = Object.fromEntries( Object.entries(roots).filter(([key]) => key !== variantCollectionName), ); } } const variantOwnIndexes = modelIndexesByName.get(variantName) ?? []; const baseColl = collections[baseCollection]; const baseModelEntry = patched[baseDecl.baseName]; const discriminatorField = baseModelEntry?.discriminator?.field; const scopedVariantIndexes: MongoIndex[] = []; if (discriminatorField) { for (const idx of variantOwnIndexes) { const result = applyPolymorphicScopeToMongoIndex(idx, { discriminatorField, discriminatorValue: baseDecl.value, }); if (result.kind === 'conflict') { const span = indexSpans.get(idx) ?? baseDecl.span; diagnostics.push({ code: 'PSL_INVALID_INDEX', message: `Variant "${variantName}" index conflicts with discriminator scope: ${result.reason}`, sourceId, span, }); continue; } if (result.index !== idx) { indexSpans.set(result.index, indexSpans.get(idx) ?? baseDecl.span); } scopedVariantIndexes.push(result.index); } } else { scopedVariantIndexes.push(...variantOwnIndexes); } if (variantCollectionName !== baseCollection) { const filtered = Object.fromEntries( Object.entries(collections).filter(([key]) => key !== variantCollectionName), ); if (scopedVariantIndexes.length > 0 && baseColl) { const baseIndexes = (baseColl['indexes'] ?? []) as MongoIndex[]; collections = { ...filtered, [baseCollection]: { ...baseColl, indexes: [...baseIndexes, ...scopedVariantIndexes], }, }; } else { collections = filtered; } } else if (baseColl) { const existingIndexes = (baseColl['indexes'] ?? []) as MongoIndex[]; const variantIndexSet = new Set(variantOwnIndexes); const withoutUnscopedVariants = existingIndexes.filter((idx) => !variantIndexSet.has(idx)); const mergedIndexes = [...withoutUnscopedVariants]; for (const idx of scopedVariantIndexes) { const idxKey = canonicalJson(idx); const isDuplicate = withoutUnscopedVariants.some( (existing) => canonicalJson(existing) === idxKey, ); if (!isDuplicate) { mergedIndexes.push(idx); } } if ( mergedIndexes.length !== existingIndexes.length || mergedIndexes.some((idx, i) => idx !== existingIndexes[i]) ) { const next: Record = { ...baseColl }; if (mergedIndexes.length > 0) { next['indexes'] = mergedIndexes; } else { delete next['indexes']; } collections = { ...collections, [baseCollection]: next }; } } } return { models: patched, roots, collections, diagnostics }; } // Property-order-stable serialization for structural equality of plain // JSON-compatible values. Used for comparing MongoIndex shapes in // the variant-merge dedup path where a future change to the spread order // would otherwise produce JSON-stringify mismatches even though the // indexes are structurally identical. function canonicalJson(value: unknown): string { if (Array.isArray(value)) { return `[${value.map(canonicalJson).join(',')}]`; } if (value && typeof value === 'object') { return `{${Object.entries(value as Record) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`) .join(',')}}`; } return JSON.stringify(value); } function parseIndexDirection(raw: string | undefined): MongoIndexKeyDirection { if (!raw) return 1; const stripped = raw.replace(/^["']/, '').replace(/["']$/, ''); const num = Number(stripped); if (num === 1 || num === -1) return num; if (['text', '2dsphere', '2d', 'hashed'].includes(stripped)) return stripped as MongoIndexKeyDirection; return 1; } function parseNumericArg(raw: string | undefined): number | undefined { if (!raw) return undefined; const n = Number(raw); return Number.isFinite(n) ? n : undefined; } function parseBooleanArg(raw: string | undefined): boolean | undefined { if (raw === 'true') return true; if (raw === 'false') return false; return undefined; } function parseJsonArg(raw: string | undefined): Record | undefined { if (!raw) return undefined; const stripped = raw.replace(/^["']/, '').replace(/["']$/, '').replace(/\\"/g, '"'); try { const parsed = JSON.parse(stripped); if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { return parsed as Record; } } catch { // not valid JSON } return undefined; } function parseCollation(attr: ResolvedAttribute): CollationOptions | null | undefined { const locale = stripQuotesHelper(getNamedArgument(attr, 'collationLocale')); if (!locale) { const hasAnyCollationArg = getNamedArgument(attr, 'collationStrength') != null || getNamedArgument(attr, 'collationCaseLevel') != null || getNamedArgument(attr, 'collationCaseFirst') != null || getNamedArgument(attr, 'collationNumericOrdering') != null || getNamedArgument(attr, 'collationAlternate') != null || getNamedArgument(attr, 'collationMaxVariable') != null || getNamedArgument(attr, 'collationBackwards') != null || getNamedArgument(attr, 'collationNormalization') != null; return hasAnyCollationArg ? null : undefined; } const collation: CollationOptions = { locale }; const strength = parseNumericArg(getNamedArgument(attr, 'collationStrength')); if (strength != null) collation.strength = strength; const caseLevel = parseBooleanArg(getNamedArgument(attr, 'collationCaseLevel')); if (caseLevel != null) collation.caseLevel = caseLevel; const caseFirst = stripQuotesHelper(getNamedArgument(attr, 'collationCaseFirst')); if (caseFirst != null) collation.caseFirst = caseFirst; const numericOrdering = parseBooleanArg(getNamedArgument(attr, 'collationNumericOrdering')); if (numericOrdering != null) collation.numericOrdering = numericOrdering; const alternate = stripQuotesHelper(getNamedArgument(attr, 'collationAlternate')); if (alternate != null) collation.alternate = alternate; const maxVariable = stripQuotesHelper(getNamedArgument(attr, 'collationMaxVariable')); if (maxVariable != null) collation.maxVariable = maxVariable; const backwards = parseBooleanArg(getNamedArgument(attr, 'collationBackwards')); if (backwards != null) collation.backwards = backwards; const normalization = parseBooleanArg(getNamedArgument(attr, 'collationNormalization')); if (normalization != null) collation.normalization = normalization; return collation; } function stripQuotesHelper(raw: string | undefined): string | undefined { if (!raw) return undefined; return raw.replace(/^["']/, '').replace(/["']$/, ''); } function parseProjectionList( raw: string | undefined, value: 0 | 1, ): Record | undefined { if (!raw) return undefined; const stripped = raw.replace(/^["']/, '').replace(/["']$/, ''); const inner = stripped.replace(/^\[/, '').replace(/\]$/, '').trim(); if (inner.length === 0) return undefined; const fields = inner .split(',') .map((s) => s.trim()) .filter((s) => s.length > 0); const result: Record = {}; for (const f of fields) { result[f] = value; } return result; } function collectIndexes( pslModel: ModelSymbol, fieldMappings: FieldMappings, modelNames: ReadonlySet, sourceId: string, diagnostics: ContractSourceDiagnostic[], indexSpans: Map, ): MongoIndex[] { const indexes: MongoIndex[] = []; let textIndexCount = 0; // Storage-indexable PSL field names — i.e. all declared fields except // relation fields (which don't materialize a column on this model). The // index field-existence check (PSL_INDEX_FIELD_NOT_FOUND) consults this // rather than fieldMappings.pslNameToMapped because the latter contains // every PSL field including relation fields. const indexableFieldNames = new Set(); for (const f of Object.values(pslModel.fields)) { if (modelNames.has(f.typeName)) continue; indexableFieldNames.add(f.name); } for (const field of Object.values(pslModel.fields)) { if (modelNames.has(field.typeName)) continue; const uniqueAttr = getAttribute(field.attributes, 'unique'); if (!uniqueAttr) continue; const mappedName = fieldMappings.pslNameToMapped.get(field.name) ?? field.name; const fieldUniqueIndex = new MongoIndex({ keys: [{ field: mappedName, direction: 1 }], unique: true, }); indexes.push(fieldUniqueIndex); indexSpans.set(fieldUniqueIndex, uniqueAttr.span); } for (const attr of pslModel.attributes) { const isIndex = attr.name === 'index'; const isUnique = attr.name === 'unique'; const isTextIndex = attr.name === 'textIndex'; if (!isIndex && !isUnique && !isTextIndex) continue; const fieldsArg = getPositionalArgument(attr, 0); if (!fieldsArg) continue; const parsedFields = parseIndexFieldList(fieldsArg); if (parsedFields.length === 0) continue; const hasWildcard = parsedFields.some((f) => f.isWildcard); const wildcardCount = parsedFields.filter((f) => f.isWildcard).length; if (wildcardCount > 1) { diagnostics.push({ code: 'PSL_INVALID_INDEX', message: 'An index can contain at most one wildcard() field', sourceId, span: attr.span, }); continue; } if (isUnique && hasWildcard) { diagnostics.push({ code: 'PSL_INVALID_INDEX', message: 'Unique indexes cannot use wildcard() fields', sourceId, span: attr.span, }); continue; } if (isTextIndex) { textIndexCount++; if (textIndexCount > 1) { diagnostics.push({ code: 'PSL_INVALID_INDEX', message: `Only one @@textIndex is allowed per collection (model "${pslModel.name}")`, sourceId, span: attr.span, }); continue; } if (hasWildcard) { diagnostics.push({ code: 'PSL_INVALID_INDEX', message: 'wildcard() fields cannot be combined with type: hashed/2dsphere/2d or @@textIndex', sourceId, span: attr.span, }); continue; } } const typeArg = getNamedArgument(attr, 'type'); const defaultDirection: MongoIndexKeyDirection = isTextIndex ? 'text' : parseIndexDirection(typeArg); if ( hasWildcard && typeof defaultDirection === 'string' && ['hashed', '2dsphere', '2d'].includes(defaultDirection) ) { diagnostics.push({ code: 'PSL_INVALID_INDEX', message: `wildcard() fields cannot be combined with type: ${defaultDirection}`, sourceId, span: attr.span, }); continue; } if (defaultDirection === 'hashed' && parsedFields.length > 1) { diagnostics.push({ code: 'PSL_INVALID_INDEX', message: 'Hashed indexes must have exactly one field', sourceId, span: attr.span, }); continue; } let missingField: string | undefined; for (const pf of parsedFields) { let fieldNameForLookup: string | undefined; if (pf.isWildcard) { const wildcardMatch = pf.name.match(/^(.+)\.\$\*\*$/); fieldNameForLookup = wildcardMatch ? wildcardMatch[1] : undefined; } else { fieldNameForLookup = pf.name; } if (fieldNameForLookup === undefined || fieldNameForLookup.length === 0) continue; if (!indexableFieldNames.has(fieldNameForLookup)) { missingField = fieldNameForLookup; break; } } if (missingField !== undefined) { diagnostics.push({ code: 'PSL_INDEX_FIELD_NOT_FOUND', message: `Index on model "${pslModel.name}" references unknown field "${missingField}"`, sourceId, span: attr.span, }); continue; } const keys = parsedFields.map((pf) => { const mappedName = pf.isWildcard ? pf.name.replace(/^(.+)\.\$\*\*$/, (_, prefix: string) => { const mapped = fieldMappings.pslNameToMapped.get(prefix); return mapped ? `${mapped}.$**` : `${prefix}.$**`; }) : (fieldMappings.pslNameToMapped.get(pf.name) ?? pf.name); const direction: MongoIndexKeyDirection = pf.direction != null ? (pf.direction as MongoIndexKeyDirection) : defaultDirection; return { field: mappedName, direction }; }); const unique = isUnique ? true : undefined; const sparse = isTextIndex ? undefined : parseBooleanArg(getNamedArgument(attr, 'sparse')); const expireAfterSeconds = isTextIndex ? undefined : parseNumericArg(getNamedArgument(attr, 'expireAfterSeconds')); if (hasWildcard && expireAfterSeconds != null) { diagnostics.push({ code: 'PSL_INVALID_INDEX', message: 'expireAfterSeconds cannot be combined with wildcard() fields', sourceId, span: attr.span, }); continue; } const partialFilterExpression = parseJsonArg(getNamedArgument(attr, 'filter')); const includeArg = getNamedArgument(attr, 'include'); const excludeArg = getNamedArgument(attr, 'exclude'); if (includeArg != null && excludeArg != null) { diagnostics.push({ code: 'PSL_INVALID_INDEX', message: 'Cannot specify both include and exclude on the same index', sourceId, span: attr.span, }); continue; } if ((includeArg != null || excludeArg != null) && !hasWildcard) { diagnostics.push({ code: 'PSL_INVALID_INDEX', message: 'include/exclude options are only valid when the index contains a wildcard() field', sourceId, span: attr.span, }); continue; } const wildcardProjection = includeArg != null ? parseProjectionList(includeArg, 1) : excludeArg != null ? parseProjectionList(excludeArg, 0) : undefined; const collation = parseCollation(attr); if (collation === null) { diagnostics.push({ code: 'PSL_INVALID_INDEX', message: 'collationLocale is required when using collation options', sourceId, span: attr.span, }); continue; } const rawWeights = parseJsonArg(getNamedArgument(attr, 'weights')); let weights: Record | undefined; if (rawWeights) { weights = {}; for (const [k, v] of Object.entries(rawWeights)) { if (typeof v === 'number') weights[k] = v; } } const rawDefaultLang = isTextIndex ? getNamedArgument(attr, 'language') : getNamedArgument(attr, 'default_language'); const default_language = stripQuotesHelper(rawDefaultLang); const rawLangOverride = getNamedArgument(attr, 'languageOverride'); const language_override = stripQuotesHelper(rawLangOverride); const index = new MongoIndex({ keys, ...(unique != null && { unique }), ...(sparse != null && { sparse }), ...(expireAfterSeconds != null && { expireAfterSeconds }), ...(partialFilterExpression != null && { partialFilterExpression }), ...(wildcardProjection != null && { wildcardProjection }), ...(collation != null && { collation }), ...(weights != null && { weights }), ...(default_language != null && { default_language }), ...(language_override != null && { language_override }), }); indexes.push(index); indexSpans.set(index, attr.span); } return indexes; } function isRelationField(field: FieldSymbol, modelNames: ReadonlySet): boolean { return modelNames.has(field.typeName); } // PSL scalar type name whose codec is mandated for a Mongo model's `_id`. const MONGO_OBJECT_ID_PSL_TYPE = 'ObjectId'; function resolveFieldCodecId( field: FieldSymbol, scalarTypeDescriptors: ReadonlyMap, ): string | undefined { return scalarTypeDescriptors.get(field.typeName); } function resolveNonRelationField( field: FieldSymbol, ownerName: string, compositeTypeNames: ReadonlySet, scalarTypeDescriptors: ReadonlyMap, codecIdByEnumName: ReadonlyMap, sourceId: string, diagnostics: ContractSourceDiagnostic[], ): ContractField | undefined { if (compositeTypeNames.has(field.typeName)) { const result: ContractField = { type: { kind: 'valueObject', name: field.typeName }, nullable: field.optional, }; return field.list ? { ...result, many: true } : result; } // If this field's declared type is a known enum name, treat the field as a scalar // with that enum's codec and stamp the domain valueSet ref. const enumCodecId = codecIdByEnumName.get(field.typeName); if (enumCodecId !== undefined) { const valueSet: ValueSetRef = { plane: 'domain', entityKind: 'enum', namespaceId: UNBOUND_NAMESPACE_ID, entityName: field.typeName, }; const result: ContractField = { type: { kind: 'scalar', codecId: enumCodecId }, nullable: field.optional, valueSet, }; return field.list ? { ...result, many: true } : result; } // Avoid cascading unsupported-type diagnostics after invalid qualification. if (field.malformedType) { return undefined; } const codecId = resolveFieldCodecId(field, scalarTypeDescriptors); if (!codecId) { diagnostics.push({ code: 'PSL_UNSUPPORTED_FIELD_TYPE', message: `Field "${ownerName}.${field.name}" type "${field.typeName}" is not supported in Mongo PSL interpreter`, sourceId, span: field.span, }); return undefined; } const result: ContractField = { type: { kind: 'scalar', codecId }, nullable: field.optional, }; return field.list ? { ...result, many: true } : result; } function processEnumDeclarations(input: { readonly enumBlocks: readonly PslExtensionBlock[]; readonly sourceId: string; readonly authoringContributions: AuthoringContributions | undefined; readonly entityContext: AuthoringEntityContext; readonly diagnostics: ContractSourceDiagnostic[]; }): Record { const builtEnums: Record = {}; if (input.enumBlocks.length === 0) return builtEnums; const enumDescriptor = input.authoringContributions?.entityTypes?.['enum'] !== undefined && isAuthoringEntityTypeDescriptor(input.authoringContributions.entityTypes['enum']) ? input.authoringContributions.entityTypes['enum'] : undefined; if (!enumDescriptor) { for (const decl of input.enumBlocks) { input.diagnostics.push({ code: 'PSL_ENUM_MISSING_FACTORY', message: `enum "${decl.name}" requires an "enum" entityType factory in the active authoring contributions`, sourceId: input.sourceId, span: decl.span, }); } return builtEnums; } for (const decl of input.enumBlocks) { const handle = instantiateAuthoringEntityType( 'enum', enumDescriptor, [decl], input.entityContext, ); if (handle === undefined || handle === null) continue; builtEnums[decl.name] = { codecId: handle.codecId, members: handle.enumMembers.map((m) => ({ name: m.name, value: blindCast( m.value, ), })), }; } return builtEnums; } export function interpretPslDocumentToMongoContract( input: InterpretPslDocumentToMongoContractInput, ): Result { const { symbolTable, sourceFile, scalarTypeDescriptors, codecLookup } = input; const sourceId = input.sourceId; const diagnostics: ContractSourceDiagnostic[] = [...(input.seedDiagnostics ?? [])]; const topLevel = symbolTable.topLevel; validateNamespaceBlocksForMongoTarget({ namespaces: Object.values(topLevel.namespaces), sourceId, sourceFile, diagnostics, }); const allModels: ModelSymbol[] = Object.values(topLevel.models); const allCompositeTypes: CompositeTypeSymbol[] = Object.values(topLevel.compositeTypes); const modelNames = new Set(allModels.map((m) => m.name)); const compositeTypeNames = new Set(allCompositeTypes.map((ct) => ct.name)); const topLevelEnumBlocks = Object.values(topLevel.blocks) .filter((b) => b.keyword === 'enum') .map((b) => b.block); const builtEnums = processEnumDeclarations({ enumBlocks: topLevelEnumBlocks, sourceId, authoringContributions: input.authoringContributions, entityContext: { family: 'mongo', target: 'mongo', ...ifDefined('enumInferenceCodecs', input.enumInferenceCodecs), ...ifDefined('codecLookup', codecLookup), sourceId, diagnostics: { push: (d) => { diagnostics.push( blindCast(d), ); }, }, }, diagnostics, }); const codecIdByEnumName: Map = new Map( Object.entries(builtEnums).map(([name, e]) => [name, e.codecId]), ); const models: Record = {}; const collections: Record> = {}; const roots: Record = {}; const allFkRelations: FkRelation[] = []; const indexSpans = new Map(); const modelIndexesByName = new Map(); interface BackrelationCandidate { readonly modelName: string; readonly fieldName: string; readonly targetModelName: string; readonly relationName?: string; readonly cardinality: '1:1' | '1:N'; readonly field: FieldSymbol; } const backrelationCandidates: BackrelationCandidate[] = []; for (const pslModel of allModels) { const collectionName = resolveCollectionName(pslModel); const fieldMappings = resolveFieldMappings(pslModel); const fields: Record = {}; const relations: Record = {}; for (const field of Object.values(pslModel.fields)) { if (isRelationField(field, modelNames)) { const relation = parseRelationAttribute(field.attributes); if (field.list || !(relation?.fields && relation?.references)) { backrelationCandidates.push({ modelName: pslModel.name, fieldName: field.name, targetModelName: field.typeName, ...ifDefined('relationName', relation?.relationName), cardinality: field.list ? '1:N' : '1:1', field, }); continue; } if (relation?.fields && relation?.references) { const localMapped = relation.fields.map((f) => fieldMappings.pslNameToMapped.get(f) ?? f); const targetModel = allModels.find((m) => m.name === field.typeName); const targetFieldMappings = targetModel ? resolveFieldMappings(targetModel) : undefined; const targetMapped = relation.references.map( (f) => targetFieldMappings?.pslNameToMapped.get(f) ?? f, ); relations[field.name] = { to: mongoCrossRef(field.typeName), cardinality: 'N:1' as const, on: { localFields: localMapped, targetFields: targetMapped, }, }; allFkRelations.push({ declaringModel: pslModel.name, fieldName: field.name, targetModel: field.typeName, ...ifDefined('relationName', relation.relationName), localFields: localMapped, targetFields: targetMapped, }); } continue; } const resolved = resolveNonRelationField( field, pslModel.name, compositeTypeNames, scalarTypeDescriptors, codecIdByEnumName, sourceId, diagnostics, ); if (!resolved) continue; const mappedName = fieldMappings.pslNameToMapped.get(field.name) ?? field.name; fields[mappedName] = resolved; } const isVariantModel = pslModel.attributes.some((attr) => attr.name === 'base'); const hasIdField = Object.values(pslModel.fields).some( (f) => getAttribute(f.attributes, 'id') !== undefined, ); // Variant models inherit the base's identity and are validated through their base. if (!isVariantModel) { if (!hasIdField) { diagnostics.push({ code: 'PSL_MISSING_ID_FIELD', message: `Model "${pslModel.name}" has no field with @id attribute. Every model must have exactly one @id field.`, sourceId, }); } else { // The resulting document must carry an `_id` of BSON type objectId. We // assert on the emitted shape (the mapped-name-keyed field record), not // on how the user spelled it — `id ObjectId @id @map("_id")` and a field // literally named `_id` both satisfy it; a non-objectId or unmapped id // does not. const objectIdCodecId = scalarTypeDescriptors.get(MONGO_OBJECT_ID_PSL_TYPE); const idField = fields['_id']; const idIsObjectId = idField !== undefined && idField.type.kind === 'scalar' && objectIdCodecId !== undefined && idField.type.codecId === objectIdCodecId; if (!idIsObjectId) { diagnostics.push({ code: 'PSL_MONGO_ID_REQUIRED', message: `Model "${pslModel.name}" must declare an _id field of type ObjectId (e.g. \`id ObjectId @id @map("_id")\`).`, sourceId, }); } } } models[pslModel.name] = { fields, relations, storage: { collection: collectionName } }; const modelIndexes = collectIndexes( pslModel, fieldMappings, modelNames, sourceId, diagnostics, indexSpans, ); modelIndexesByName.set(pslModel.name, modelIndexes); const existingColl = collections[collectionName]; if (existingColl && modelIndexes.length > 0) { const existingIndexes = (existingColl['indexes'] ?? []) as MongoIndex[]; collections[collectionName] = { indexes: [...existingIndexes, ...modelIndexes] }; } else if (!existingColl) { collections[collectionName] = modelIndexes.length > 0 ? { indexes: modelIndexes } : {}; } roots[collectionName] = mongoCrossRef(pslModel.name); } const valueObjects: Record = {}; for (const compositeType of allCompositeTypes) { const fields: Record = {}; for (const field of Object.values(compositeType.fields)) { const resolved = resolveNonRelationField( field, compositeType.name, compositeTypeNames, scalarTypeDescriptors, codecIdByEnumName, sourceId, diagnostics, ); if (!resolved) continue; fields[field.name] = resolved; } valueObjects[compositeType.name] = { fields }; } const fkRelationsByPair = new Map(); for (const fk of allFkRelations) { const key = fkRelationPairKey(fk.declaringModel, fk.targetModel); const existing = fkRelationsByPair.get(key); if (existing) { existing.push(fk); } else { fkRelationsByPair.set(key, [fk]); } } for (const candidate of backrelationCandidates) { const pairKey = fkRelationPairKey(candidate.targetModelName, candidate.modelName); const pairMatches = fkRelationsByPair.get(pairKey) ?? []; const matches = candidate.relationName ? pairMatches.filter((r) => r.relationName === candidate.relationName) : [...pairMatches]; if (matches.length === 0) { diagnostics.push({ code: 'PSL_ORPHANED_BACKRELATION', message: `Backrelation list field "${candidate.modelName}.${candidate.fieldName}" has no matching FK-side relation on model "${candidate.targetModelName}". Add @relation(fields: [...], references: [...]) on the FK-side relation or use an explicit join model for many-to-many.`, sourceId, span: candidate.field.span, }); continue; } if (matches.length > 1) { diagnostics.push({ code: 'PSL_AMBIGUOUS_BACKRELATION', message: `Backrelation list field "${candidate.modelName}.${candidate.fieldName}" matches multiple FK-side relations on model "${candidate.targetModelName}". Add @relation("...") to both sides to disambiguate.`, sourceId, span: candidate.field.span, }); continue; } const fk = matches[0]; if (!fk) continue; const modelEntry = models[candidate.modelName]; if (!modelEntry) continue; modelEntry.relations[candidate.fieldName] = { to: mongoCrossRef(candidate.targetModelName), cardinality: candidate.cardinality, on: { localFields: fk.targetFields, targetFields: fk.localFields, }, }; } const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations( allModels, sourceId, diagnostics, ); const polyResult = resolvePolymorphism({ models, roots, collections, allModels, discriminatorDeclarations, baseDeclarations, modelNames, indexSpans, modelIndexesByName, sourceId, }); if (diagnostics.length > 0 || polyResult.diagnostics.length > 0) { return notOk({ summary: 'PSL to Mongo contract interpretation failed', diagnostics: [...diagnostics, ...polyResult.diagnostics], }); } const resolvedModels = polyResult.models; const resolvedCollections = polyResult.collections; // The storage value set is the source of truth for both the emit typing and the validator's // `enum` keyword. Built once, ahead of validator derivation, from each enum's codec-encoded member // values (mirroring SQL's build-contract). Encoding needs the codec lookup; production always // threads it (the CLI control stack supplies it), so its absence when enums exist is a wiring bug, // not a runtime input to tolerate. const storageValueSets: Record = {}; const enumEntries = Object.entries(builtEnums); if (enumEntries.length > 0) { assertDefined( codecLookup, 'Mongo PSL interpretation requires a codec lookup to encode enum values', ); for (const [enumName, builtEnum] of enumEntries) { storageValueSets[enumName] = { kind: 'valueSet', values: builtEnum.members.map((m) => encodeEnumValue(m.value, builtEnum.codecId, codecLookup), ), }; } } for (const [, modelEntry] of Object.entries(resolvedModels)) { if (modelEntry.base) continue; const collectionName = modelEntry.storage.collection; const coll = resolvedCollections[collectionName]; if (!coll) continue; if (modelEntry.discriminator && modelEntry.variants) { const variantEntries = Object.entries(modelEntry.variants).map( ([variantName, { value }]) => ({ discriminatorValue: value, fields: resolvedModels[variantName]?.fields ?? {}, }), ); coll['validator'] = derivePolymorphicJsonSchema( modelEntry.fields, modelEntry.discriminator.field, variantEntries, valueObjects, codecLookup, storageValueSets, ); } else { coll['validator'] = deriveJsonSchema( modelEntry.fields, valueObjects, codecLookup, storageValueSets, ); } } const target = 'mongo'; const targetFamily = 'mongo'; const collectionInputs: Record = {}; for (const [name, coll] of Object.entries(resolvedCollections)) { const raw: Record = {}; if (coll['indexes'] != null) raw['indexes'] = coll['indexes']; if (coll['validator'] != null) raw['validator'] = coll['validator']; if (coll['options'] != null) raw['options'] = coll['options']; collectionInputs[name] = blindCast< MongoCollectionInput, 'arktype-validated JSON shapes satisfy MongoCollectionInput by construction' >(raw); } const hasValueSets = Object.keys(storageValueSets).length > 0; const unboundNamespace = buildMongoNamespace({ id: UNBOUND_NAMESPACE_ID, entries: { collection: collectionInputs, ...(hasValueSets ? { valueSet: storageValueSets } : {}), }, }); // Hash the constructed (normalized) entries, not the raw input literals — // persisted storageHash values were computed over the constructed form. const storageWithoutHash = { namespaces: { [UNBOUND_NAMESPACE_ID]: { id: UNBOUND_NAMESPACE_ID, entries: { collection: unboundNamespace.entries.collection, ...(unboundNamespace.entries.valueSet !== undefined ? { valueSet: unboundNamespace.entries.valueSet } : {}), }, }, }, }; const storageHash = computeStorageHash({ target, targetFamily, storage: storageWithoutHash, ...mongoContractCanonicalizationHooks, }); const storage = new MongoStorage({ storageHash, namespaces: { [UNBOUND_NAMESPACE_ID]: unboundNamespace, }, }) as Contract['storage']; const capabilities: Record> = {}; const hasEnums = Object.keys(builtEnums).length > 0; return ok({ targetFamily, target, roots: polyResult.roots, domain: { namespaces: { [UNBOUND_NAMESPACE_ID]: { models: polyResult.models, ...(Object.keys(valueObjects).length > 0 ? { valueObjects } : {}), ...(hasEnums ? { enum: builtEnums } : {}), }, }, }, storage, extensionPacks: {}, capabilities, profileHash: computeProfileHash({ target, targetFamily, capabilities }), meta: {}, }); }