/** * Schema export endpoint for CLI sync * * GET /_emdash/api/schema - Export full schema (requires schema:read permission) * * This endpoint is used by: * - CLI `emdash types` to fetch schema and generate types * - Local dev server to sync schema from remote * - CI/CD to detect schema drift */ import type { APIRoute } from "astro"; import { hashString } from "emdash"; import { requirePerm } from "#api/authorize.js"; import { apiSuccess, handleError, requireDb } from "#api/error.js"; import { SchemaRegistry } from "#schema/registry.js"; import { generateTypeScript, uniqueInterfaceNames } from "#schema/zod-generator.js"; export const prerender = false; export const GET: APIRoute = async ({ request, locals }) => { const { emdash, user } = locals; const dbErr = requireDb(emdash?.db); if (dbErr) return dbErr; const denied = requirePerm(user, "schema:read"); if (denied) return denied; try { const registry = new SchemaRegistry(emdash.db); // Get all collections with their fields const collections = await registry.listCollections(); const collectionsWithFields = await Promise.all( collections.map(async (c) => { const fields = await registry.listFields(c.id); return { ...c, fields }; }), ); // Check for format parameter const url = new URL(request.url); const format = url.searchParams.get("format"); if (format === "typescript") { // Generate TypeScript definitions. Singularizing slugs can collapse // distinct collections onto the same interface name, so resolve names // up front to keep every emitted interface unique (`book` + `books` // would otherwise both emit `Book`). const interfaceNames = uniqueInterfaceNames(collectionsWithFields); const types = collectionsWithFields .map((c) => generateTypeScript(c, interfaceNames.get(c.slug))) .join("\n\n"); const header = `// Generated by EmDash CLI // Do not edit manually - run \`emdash types\` to regenerate import type { PortableTextBlock } from "emdash"; `; return new Response(header + types, { status: 200, headers: { "Content-Type": "text/typescript", "Cache-Control": "private, no-store", }, }); } // Default: JSON format const schemaExport = { collections: collectionsWithFields.map((c) => ({ slug: c.slug, label: c.label, labelSingular: c.labelSingular, description: c.description, icon: c.icon, supports: c.supports, fields: c.fields.map((f) => ({ slug: f.slug, label: f.label, type: f.type, required: f.required, unique: f.unique, defaultValue: f.defaultValue, validation: f.validation, widget: f.widget, options: f.options, })), })), }; const version = await hashString(JSON.stringify(schemaExport)); const response = apiSuccess({ ...schemaExport, version, }); response.headers.set("X-Schema-Version", version); return response; } catch (error) { return handleError(error, "Schema export failed", "SCHEMA_EXPORT_ERROR"); } };