/** * Declarative module YAML schema definition and validation. * * Defines the structure of .module.yaml files that produce * runtime-generated Fastify CRUD routes with no TypeScript needed. */ import { z } from 'zod'; // ── Field types supported in YAML definitions ───────────────────────────── const FieldTypeEnum = z.enum(['string', 'number', 'boolean', 'date', 'array', 'enum']); // ── Field definition ────────────────────────────────────────────────────── export const FieldSchema = z.object({ type: FieldTypeEnum, required: z.boolean().default(false), default: z.any().optional(), min: z.number().optional(), max: z.number().optional(), minLength: z.number().optional(), maxLength: z.number().optional(), values: z.array(z.string()).optional(), // for enum type items: z.string().optional(), // for array type (item type) description: z.string().optional(), }); // ── Endpoint definition ─────────────────────────────────────────────────── export const EndpointSchema = z.object({ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']), path: z.string().min(1), auth: z.enum(['none', 'user', 'admin']).default('user'), custom: z.boolean().default(false), description: z.string().optional(), }); // ── Module definition (top-level YAML structure) ────────────────────────── export const ModuleSchema = z.object({ name: z.string().min(1).max(100), container: z.string().min(1).max(100), partitionKey: z.string().min(1).max(100).default('/productId'), idPrefix: z.string().max(20).default(''), fields: z.record(z.string(), FieldSchema), endpoints: z.array(EndpointSchema).optional(), description: z.string().optional(), }); // ── Inferred types ──────────────────────────────────────────────────────── export type FieldDef = z.infer; export type EndpointDef = z.infer; export type ModuleDef = z.infer; // ── Helpers ─────────────────────────────────────────────────────────────── /** * Convert a FieldDef to a Zod schema for runtime validation. */ export function fieldToZodSchema(name: string, field: FieldDef): z.ZodTypeAny { let schema: z.ZodTypeAny; switch (field.type) { case 'string': { let s = z.string(); if (field.minLength !== undefined) s = s.min(field.minLength); if (field.maxLength !== undefined) s = s.max(field.maxLength); schema = s; break; } case 'number': { let n = z.number(); if (field.min !== undefined) n = n.min(field.min); if (field.max !== undefined) n = n.max(field.max); schema = n; break; } case 'boolean': schema = z.boolean(); break; case 'date': schema = z.string().datetime({ offset: true }).or(z.string().datetime()); break; case 'enum': if (!field.values || field.values.length === 0) { throw new Error(`Field "${name}" is enum but has no values`); } schema = z.enum(field.values as [string, ...string[]]); break; case 'array': schema = z.array(z.any()); break; default: schema = z.any(); } if (field.default !== undefined) { // .default() already handles undefined → value, so no need for .optional() schema = schema.default(field.default); } else if (!field.required) { schema = schema.optional(); } return schema; } /** * Build a Zod object schema for create operations from a ModuleDef. */ export function buildCreateSchema(mod: ModuleDef): z.ZodObject> { const shape: Record = {}; for (const [name, field] of Object.entries(mod.fields)) { shape[name] = fieldToZodSchema(name, field); } return z.object(shape); } /** * Build a Zod object schema for update operations (all fields optional). */ export function buildUpdateSchema(mod: ModuleDef): z.ZodObject> { const shape: Record = {}; for (const [name, field] of Object.entries(mod.fields)) { // For updates, make everything optional let schema = fieldToZodSchema(name, { ...field, required: false }); // Remove default on updates if (field.default !== undefined) { schema = fieldToZodSchema(name, { ...field, required: false, default: undefined }); } shape[name] = schema; } return z.object(shape); } /** * Generate default CRUD endpoints for a module if none are specified. */ export function defaultEndpoints(mod: ModuleDef): EndpointDef[] { const base = `/${mod.name}`; return [ { method: 'GET', path: base, auth: 'user', custom: false, description: `List ${mod.name}` }, { method: 'GET', path: `${base}/:id`, auth: 'user', custom: false, description: `Get ${mod.name} by ID`, }, { method: 'POST', path: base, auth: 'user', custom: false, description: `Create ${mod.name}` }, { method: 'PATCH', path: `${base}/:id`, auth: 'user', custom: false, description: `Update ${mod.name}`, }, { method: 'DELETE', path: `${base}/:id`, auth: 'admin', custom: false, description: `Delete ${mod.name}`, }, ]; }