learning_ai_common_plat/services/platform-service/src/lib/declarative-schema.ts
saravanakumardb1 0f299231cc feat(platform-service): declarative YAML module loader (4.4) — 34 tests
New files:
- src/lib/declarative-schema.ts — YAML schema + Zod validation
  - ModuleSchema, FieldSchema, EndpointSchema
  - fieldToZodSchema, buildCreateSchema, buildUpdateSchema, defaultEndpoints

- src/lib/declarative-loader.ts — runtime route generator
  - parseModuleYaml, registerModuleRoutes, loadDeclarativeModules
  - MemoryStore with filtering, sorting, pagination
  - Auth enforcement (none/user/admin), custom endpoint support

- src/lib/declarative-loader.test.ts — 34 tests
  - Schema validation, field conversion, endpoint generation
  - MemoryStore CRUD, route integration with full lifecycle

Dependency: yaml (npm)
2026-03-19 21:16:58 -07:00

168 lines
5.7 KiB
TypeScript

/**
* 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<typeof FieldSchema>;
export type EndpointDef = z.infer<typeof EndpointSchema>;
export type ModuleDef = z.infer<typeof ModuleSchema>;
// ── 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<Record<string, z.ZodTypeAny>> {
const shape: Record<string, z.ZodTypeAny> = {};
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<Record<string, z.ZodTypeAny>> {
const shape: Record<string, z.ZodTypeAny> = {};
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}`,
},
];
}