/** * Resource Contract Generator * Generate Zod contract from resource definition */ import type { ResourceDefinition, ResourceField } from "../schema"; import { getPluralName, getEnabledEndpoints } from "../schema"; /** * Generate contract file for resource * * @returns Contract file content */ export function generateResourceContract(definition: ResourceDefinition): string { const resourceName = definition.name; const pascalName = toPascalCase(resourceName); const _pluralName = getPluralName(definition); const endpoints = getEnabledEndpoints(definition); // Generate schema definitions const schemaDefinitions = generateSchemaDefinitions(definition); // Generate request schemas const requestSchemas = generateRequestSchemas(definition, endpoints); // Generate response schemas const responseSchemas = generateResponseSchemas(definition, pascalName); return `// 📜 Mandu Resource Contract - ${resourceName} // Auto-generated from resource definition // DO NOT EDIT - Regenerated on every \`mandu generate\` import { z } from "zod"; import { Mandu } from "@mandujs/core"; // ============================================ // 🥟 Schema Definitions // ============================================ ${schemaDefinitions} // ============================================ // 📜 Contract Definition // ============================================ export default Mandu.contract({ description: "${definition.options?.description || `${pascalName} API`}", tags: ${JSON.stringify(definition.options?.tags || [resourceName])}, request: { ${requestSchemas} }, response: { ${responseSchemas} }, }); `; } /** * Generate schema definitions for fields */ function generateSchemaDefinitions(definition: ResourceDefinition): string { const pascalName = toPascalCase(definition.name); const fields = Object.entries(definition.fields); // Generate individual field schemas const fieldSchemas = fields.map(([name, field]) => { const zodSchema = generateZodSchema(name, field); return ` ${name}: ${zodSchema},`; }); return `/** * ${pascalName} Schema */ const ${pascalName}Schema = z.object({ ${fieldSchemas.join("\n")} }); /** * ${pascalName} Create Schema (exclude id, createdAt, updatedAt) */ const ${pascalName}CreateSchema = ${pascalName}Schema.omit({ id: true, createdAt: true, updatedAt: true, }); /** * ${pascalName} Update Schema (all fields optional except id) */ const ${pascalName}UpdateSchema = ${pascalName}Schema.partial().required({ id: true });`; } /** * Generate Zod schema for a field */ function generateZodSchema(fieldName: string, field: ResourceField): string { // Use custom schema if provided if (field.schema) { return "z.unknown() /* Custom schema */"; } let schema: string; switch (field.type) { case "string": schema = "z.string()"; break; case "number": schema = "z.number()"; break; case "boolean": schema = "z.boolean()"; break; case "date": schema = "z.string().datetime()"; break; case "uuid": schema = "z.string().uuid()"; break; case "email": schema = "z.string().email()"; break; case "url": schema = "z.string().url()"; break; case "json": schema = "z.record(z.unknown())"; break; case "array": const itemType = field.items || "unknown"; schema = `z.array(${generateZodTypeFromFieldType(itemType)})`; break; case "object": schema = "z.record(z.unknown())"; break; default: schema = "z.unknown()"; } // Add optional/required if (!field.required) { schema += ".optional()"; } // Add default if (field.default !== undefined) { const defaultValue = JSON.stringify(field.default); schema += `.default(${defaultValue})`; } // Add description if (field.description) { schema += `.describe("${field.description}")`; } return schema; } /** * Generate Zod type from FieldType */ function generateZodTypeFromFieldType(type: string): string { switch (type) { case "string": return "z.string()"; case "number": return "z.number()"; case "boolean": return "z.boolean()"; case "date": return "z.string().datetime()"; case "uuid": return "z.string().uuid()"; case "email": return "z.string().email()"; case "url": return "z.string().url()"; default: return "z.unknown()"; } } /** * Generate request schemas based on enabled endpoints */ function generateRequestSchemas(definition: ResourceDefinition, endpoints: string[]): string { const pascalName = toPascalCase(definition.name); const schemas: string[] = []; if (endpoints.includes("list")) { const defaultLimit = definition.options?.pagination?.defaultLimit || 10; const maxLimit = definition.options?.pagination?.maxLimit || 100; schemas.push(` GET: { query: z.object({ page: z.coerce.number().int().min(1).default(1), limit: z.coerce.number().int().min(1).max(${maxLimit}).default(${defaultLimit}), }), }`); } if (endpoints.includes("create")) { schemas.push(` POST: { body: ${pascalName}CreateSchema, }`); } if (endpoints.includes("update")) { schemas.push(` PUT: { body: ${pascalName}UpdateSchema, }`); } if (endpoints.includes("delete")) { schemas.push(` DELETE: { // No body for DELETE }`); } return schemas.join(",\n\n"); } /** * Generate response schemas */ function generateResponseSchemas(definition: ResourceDefinition, pascalName: string): string { return ` 200: z.object({ data: z.union([${pascalName}Schema, z.array(${pascalName}Schema)]), pagination: z.object({ page: z.number(), limit: z.number(), total: z.number(), }).optional(), }), 201: z.object({ data: ${pascalName}Schema, }), 400: z.object({ error: z.string(), details: z.array(z.object({ type: z.string(), issues: z.array(z.object({ path: z.string(), message: z.string(), })), })).optional(), }), 404: z.object({ error: z.string(), })`; } /** * Convert string to PascalCase */ function toPascalCase(str: string): string { return str .split(/[-_]/) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(""); }