import type { ContractField, ContractValueObject, JsonValue } from '@prisma-next/contract/types'; import type { CodecLookup } from '@prisma-next/framework-components/codec'; import { MongoValidator } from '@prisma-next/mongo-contract'; /** * The permitted values a field's value set restricts it to, keyed by the value set's name — the * storage `entries.valueSet` slot. The validator's `enum` keyword is sourced from these, not from * `domain.enum`. */ export type FieldValueSets = Record; function resolveBsonType( codecId: string, codecLookup: CodecLookup | undefined, ): string | undefined { return codecLookup?.targetTypesFor(codecId)?.[0]; } function fieldToBsonSchema( field: ContractField, valueObjects: Record | undefined, codecLookup: CodecLookup | undefined, valueSets: FieldValueSets | undefined, ): Record | undefined { if (field.type.kind === 'scalar') { const bsonType = resolveBsonType(field.type.codecId, codecLookup); if (!bsonType) return undefined; const enumValues = field.valueSet !== undefined ? (valueSets?.[field.valueSet.entityName]?.values ?? null) : null; if ('many' in field && field.many) { const items: Record = { bsonType }; if (enumValues) items['enum'] = enumValues; return { bsonType: 'array', items }; } if (field.nullable) { const s: Record = { bsonType: ['null', bsonType] }; if (enumValues) s['enum'] = [...enumValues, null]; return s; } const s: Record = { bsonType }; if (enumValues) s['enum'] = enumValues; return s; } if (field.type.kind === 'valueObject') { const vo = valueObjects?.[field.type.name]; if (!vo) return undefined; const voSchema = deriveObjectSchema(vo.fields, valueObjects, codecLookup, valueSets); if ('many' in field && field.many) { return { bsonType: 'array', items: voSchema }; } if (field.nullable) { return { oneOf: [{ bsonType: 'null' }, voSchema] }; } return voSchema; } return undefined; } function deriveObjectSchema( fields: Record, valueObjects: Record | undefined, codecLookup: CodecLookup | undefined, valueSets: FieldValueSets | undefined, ): Record { const properties: Record = {}; const required: string[] = []; for (const [fieldName, field] of Object.entries(fields)) { const schema = fieldToBsonSchema(field, valueObjects, codecLookup, valueSets); if (schema) { properties[fieldName] = schema; if (!field.nullable) { required.push(fieldName); } } } const result: Record = { bsonType: 'object', properties, additionalProperties: false, }; if (required.length > 0) { result['required'] = required.sort(); } return result; } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } export function deriveJsonSchema( fields: Record, valueObjects?: Record, codecLookup?: CodecLookup, valueSets?: FieldValueSets, ): MongoValidator { return new MongoValidator({ jsonSchema: deriveObjectSchema(fields, valueObjects, codecLookup, valueSets), validationLevel: 'strict', validationAction: 'error', }); } export interface PolymorphicVariant { readonly discriminatorValue: string; readonly fields: Record; } export function derivePolymorphicJsonSchema( baseFields: Record, discriminatorField: string, variants: readonly PolymorphicVariant[], valueObjects?: Record, codecLookup?: CodecLookup, valueSets?: FieldValueSets, ): MongoValidator { const baseSchema = deriveObjectSchema(baseFields, valueObjects, codecLookup, valueSets); const baseProperties = isRecord(baseSchema['properties']) ? baseSchema['properties'] : {}; const oneOf: Record[] = []; for (const variant of variants) { const variantOnlyFields: Record = {}; for (const [name, field] of Object.entries(variant.fields)) { if (!(name in baseFields)) { variantOnlyFields[name] = field; } } const variantProperties: Record = {}; const variantRequired: string[] = [discriminatorField]; for (const [name, field] of Object.entries(variantOnlyFields)) { const schema = fieldToBsonSchema(field, valueObjects, codecLookup, valueSets); if (schema) { variantProperties[name] = schema; if (!field.nullable) { variantRequired.push(name); } } } const entry: Record = { properties: { ...baseProperties, [discriminatorField]: { enum: [variant.discriminatorValue] }, ...variantProperties, }, required: variantRequired.sort(), additionalProperties: false, }; oneOf.push(entry); } const jsonSchema = { ...baseSchema }; delete jsonSchema['additionalProperties']; if (oneOf.length > 0) { jsonSchema['oneOf'] = oneOf; } return new MongoValidator({ jsonSchema, validationLevel: 'strict', validationAction: 'error', }); }