import { z } from 'zod'; /** * MVP-CardType-Set. Cloze in Phase 8 (Anki-Import) ergänzt, * image-occlusion in Phase 9l. Weitere Erweiterung (type-in, audio, * multiple-choice) bleibt im CardTypeFutureSchema vorbereitet. */ export const CardTypeSchema = z.enum([ 'basic', 'basic-reverse', 'cloze', 'image-occlusion', 'audio-front', 'typing', ]); export type CardType = z.infer; /** Future-Set für Schema-Migration-Vorbereitung. */ export const CardTypeFutureSchema = z.enum([ 'basic', 'basic-reverse', 'cloze', 'type-in', 'image-occlusion', 'audio-front', 'multiple-choice', ]); /** * Generischer Field-Slot. Konkrete Field-Sets pro Type werden runtime * via `validateFieldsForType()` geprüft (siehe unten). */ export const CardFieldsSchema = z.record(z.string(), z.string()); export type CardFields = z.infer; /** * Field-Set-Validierung pro CardType. Keine z.discriminatedUnion, weil * der Type-Slot generisch bleibt — neue CardTypes können ohne Schema- * Bruch hinzugefügt werden. */ export function validateFieldsForType( type: CardType | z.infer, fields: CardFields ): { ok: true } | { ok: false; missing: string[] } { const required: Record = { basic: ['front', 'back'], 'basic-reverse': ['front', 'back'], cloze: ['text'], 'type-in': ['question', 'expected'], 'image-occlusion': ['image_ref', 'mask_regions'], 'audio-front': ['audio_ref', 'back'], typing: ['front', 'answer'], 'multiple-choice': ['question', 'options', 'correct_index'], }; const need = required[type] ?? []; const missing = need.filter((k) => !(k in fields)); return missing.length === 0 ? { ok: true } : { ok: false, missing }; } export const CardSchema = z .object({ id: z.string().min(1), deck_id: z.string().min(1), user_id: z.string().min(1), type: CardTypeSchema, fields: CardFieldsSchema, media_refs: z.array(z.string()).default([]), content_hash: z.string().optional().nullable(), created_at: z.string().datetime(), updated_at: z.string().datetime(), }) .strict(); export type Card = z.infer; export const CardCreateSchema = z .object({ deck_id: z.string().min(1), type: CardTypeSchema, fields: CardFieldsSchema, tags: z.array(z.string()).optional(), media_refs: z.array(z.string()).optional(), }) .strict() .superRefine((val, ctx) => { const check = validateFieldsForType(val.type, val.fields); if (!check.ok) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['fields'], message: `missing fields for type=${val.type}: ${check.missing.join(', ')}`, }); } }); export type CardCreate = z.infer; export const CardUpdateSchema = z .object({ fields: CardFieldsSchema.optional(), tags: z.array(z.string()).optional(), media_refs: z.array(z.string()).optional(), }) .strict(); export type CardUpdate = z.infer;