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
JavaScript
// 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
};