UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

145 lines (143 loc) 3.98 kB
// src/schema/define.ts import { z } from "zod"; function rel(target, cardinality = "one") { return { target, cardinality }; } function rollup(cfg) { return { spec: { valueType: "rollup", rollup: cfg, computed: true, editable: false } }; } function formula(expr) { return { spec: { valueType: "formula", formula: expr, computed: true, editable: false } }; } function defineType(type, shape, opts = {}) { const relations = opts.relations ?? {}; const computed = opts.computed ?? {}; const fields = [ ...Object.entries(shape).map(([name, zt]) => { const spec = zodToSpec(name, zt, name === opts.title); const sync = opts.fieldSync?.[name]; return sync ? { ...spec, sync } : spec; }), ...Object.entries(relations).map(([name, r]) => relationToSpec(name, r)), ...Object.entries(computed).map( ([name, c]) => ({ name, required: false, ...c.spec }) ) ]; const definition = { "@id": `trellis:${type}`, "@type": "trellis:Schema", version: opts.version ?? "1.0.0", tier: opts.tier ?? "user", label: opts.label ?? type, fields, ...opts.extends ? { subClassOf: opts.extends } : {} }; return { type, zod: z.object(shape), relations, computed, definition, toOntologySchema: () => lowerToLegacy(type, definition, relations) }; } function unwrap(zt) { let t = zt; for (let i = 0; i < 8; i++) { if (t instanceof z.ZodOptional || t instanceof z.ZodNullable || t instanceof z.ZodDefault) { const inner = t._def.innerType; if (!inner) break; t = inner; } else { break; } } return t; } function zodToSpec(name, zt, isTitle) { const required = !zt.isOptional(); const base = unwrap(zt); let valueType = "rich_text"; let selectOptions; if (isTitle) { valueType = "title"; } else if (base instanceof z.ZodString) { const checks = base._def.checks ?? []; if (checks.some((c) => c.kind === "email")) valueType = "email"; else if (checks.some((c) => c.kind === "url")) valueType = "url"; else if (checks.some((c) => c.kind === "datetime")) valueType = "date"; else valueType = "rich_text"; } else if (base instanceof z.ZodNumber) { valueType = "number"; } else if (base instanceof z.ZodBoolean) { valueType = "checkbox"; } else if (base instanceof z.ZodDate) { valueType = "date"; } else if (base instanceof z.ZodEnum) { valueType = "select"; selectOptions = base.options.slice(); } else if (base instanceof z.ZodArray) { const el = unwrap(base._def.type); valueType = "multi_select"; if (el instanceof z.ZodEnum) { selectOptions = el.options.slice(); } } else { valueType = "json"; } const spec = { name, valueType, required }; if (selectOptions) spec.selectOptions = selectOptions; return spec; } function targetName(r) { return typeof r.target === "string" ? r.target : r.target().type; } function relationToSpec(name, r) { return { name, valueType: "relation", required: false, relation: { targetSchema: targetName(r), cardinality: r.cardinality } }; } function attrType(v) { switch (v) { case "number": case "rollup": return "number"; case "checkbox": return "boolean"; case "date": return "date"; case "relation": return "ref"; default: return "string"; } } function lowerToLegacy(type, def, relations) { const attributes = def.fields.filter((f) => f.valueType !== "relation").map((f) => ({ name: f.name, type: attrType(f.valueType), required: f.required ?? false })); const rels = Object.entries(relations).map(([name, r]) => ({ name, sourceTypes: [type], targetTypes: [targetName(r)], cardinality: r.cardinality })); return { id: def["@id"], name: type, version: def.version, entities: [{ name: type, attributes }], relations: rels }; } export { rel, rollup, formula, defineType };