UNPKG

trellis

Version:

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

234 lines (230 loc) 7.29 kB
// src/schema/entity-projection.ts function entityRecordToPlain(entity) { const obj = { id: entity.id, type: entity.type }; for (const f of entity.facts) { if (f.a !== "type") obj[f.a] = f.v; } return obj; } function bindingEntityId(row) { const id = row.id ?? row.e ?? row["?e"]; return typeof id === "string" && id.length > 0 ? id : null; } function isSparseBinding(row) { const id = bindingEntityId(row); if (!id) return false; return typeof row.type !== "string"; } function bindingToEntity(row) { const id = bindingEntityId(row); if (id && typeof row.type === "string") { return { ...row, id, type: row.type }; } if (id) return { id, type: String(row.type ?? ""), ...row }; return row; } function hydrateBindings(kernel, bindings) { return bindings.map((row) => { if (!isSparseBinding(row)) return bindingToEntity(row); const id = bindingEntityId(row); const entity = kernel.getEntity(id); return entity ? entityRecordToPlain(entity) : bindingToEntity(row); }); } // src/schema/eql.ts function escapeValue(v) { return v.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); } var WHERE_OP_TO_EQL = { eq: "=", ne: "!=", lt: "<", lte: "<=", gt: ">", gte: ">=", contains: "contains", startsWith: "startsWith", endsWith: "endsWith" }; function isWhereFilter(value) { if (typeof value !== "object" || value === null || Array.isArray(value)) { return false; } return Object.keys(WHERE_OP_TO_EQL).some( (op) => value[op] !== void 0 ); } function formatEqlLiteral(value) { if (typeof value === "string") return `"${escapeValue(value)}"`; if (typeof value === "boolean") return value ? "true" : "false"; if (typeof value === "number" && Number.isFinite(value)) return String(value); if (value === null || value === void 0) { throw new Error("Cannot format null/undefined as an EQL literal"); } return `"${escapeValue(String(value))}"`; } function whereCondition(attr, value) { if (isWhereFilter(value)) { const ops = Object.keys(WHERE_OP_TO_EQL).filter( (op2) => value[op2] !== void 0 ); if (ops.length === 0) { throw new Error(`Empty where filter for attribute "${attr}"`); } if (ops.length > 1) { throw new Error( `Where filter for "${attr}" must specify one operator, got: ${ops.join(", ")}` ); } const op = ops[0]; return `${attr} ${WHERE_OP_TO_EQL[op]} ${formatEqlLiteral(value[op])}`; } return `${attr} = ${formatEqlLiteral(value)}`; } function entitiesQuery(type, where) { const conds = [`type = "${escapeValue(type)}"`]; for (const [k, v] of Object.entries(where ?? {})) { if (v === void 0 || v === null) continue; conds.push(whereCondition(k, v)); } return `find ?e where ${conds.join(" and ")}`; } function entityQuery(type, entityId) { const t = formatEqlLiteral(type); const id = formatEqlLiteral(entityId); return `SELECT ?e WHERE { [?e "type" ${t}] } FILTER ?e = ${id}`; } // src/schema/resolve.ts function isNestedResolveSpec(v) { return typeof v === "object" && v !== null; } function relationTargetName(r) { return typeof r.target === "string" ? r.target : r.target().type; } function relationTargetSchema(r, schemaLookup) { if (typeof r.target !== "string") return r.target(); return schemaLookup?.(r.target) ?? null; } function inverseForeignKey(parent, relationName, child) { for (const [key, r] of Object.entries(child.relations)) { if (relationTargetName(r) === parent.type) return key; } return null; } async function loadByIds(client, ids, cache) { const missing = ids.filter((id) => !cache.has(id)); if (missing.length === 0) return; await Promise.all( missing.map(async (id) => { cache.set(id, await client.read(id)); }) ); } async function resolveReverseMany(client, parent, relationName, parents, cache, schemaLookup) { const rel = parent.relations[relationName]; if (!rel || rel.cardinality !== "many") return; const childSchema = relationTargetSchema(rel, schemaLookup); if (!childSchema) return; const foreignKey = inverseForeignKey(parent, relationName, childSchema); if (!foreignKey) return; const parentIds = new Set(parents.map((p) => p.id)); const qr = await client.query(entitiesQuery(childSchema.type)); const childIds = qr.bindings.map((b) => bindingEntityId(b)).filter((id) => Boolean(id)); await loadByIds(client, childIds, cache); const grouped = /* @__PURE__ */ new Map(); for (const id of childIds) { const child = cache.get(id); if (!child) continue; const fk = child[foreignKey]; if (typeof fk !== "string" || !parentIds.has(fk)) continue; if (!grouped.has(fk)) grouped.set(fk, []); grouped.get(fk).push(child); } for (const row of parents) { row[relationName] = grouped.get(row.id) ?? []; } } async function resolveForwardOne(client, parents, relationName, cache) { const ids = parents.map((p) => p[relationName]).filter((v) => typeof v === "string" && v.length > 0); await loadByIds(client, ids, cache); for (const row of parents) { const ref = row[relationName]; if (typeof ref !== "string") continue; const loaded = cache.get(ref); if (loaded) row[relationName] = loaded; } } async function resolveRelations(client, schema, entities, spec, opts) { if (entities.length === 0 || Object.keys(spec).length === 0) return entities; const cache = /* @__PURE__ */ new Map(); const rows = opts?.copy === false ? entities : entities.map((e) => ({ ...e })); for (const [name, enabled] of Object.entries(spec)) { if (!enabled) continue; const rel = schema.relations[name]; if (!rel) continue; if (rel.cardinality === "many") { await resolveReverseMany( client, schema, name, rows, cache, opts?.schemaLookup ); if (isNestedResolveSpec(enabled)) { const childSchema = relationTargetSchema(rel, opts?.schemaLookup); if (childSchema) { for (const row of rows) { const kids = row[name]; if (kids?.length) { await resolveRelations(client, childSchema, kids, enabled, { copy: false, schemaLookup: opts?.schemaLookup }); } } } } } else if (isNestedResolveSpec(enabled)) { await resolveForwardOne(client, rows, name, cache); const childSchema = relationTargetSchema(rel, opts?.schemaLookup); if (childSchema) { const nested = []; for (const row of rows) { const loaded = row[name]; if (loaded && typeof loaded === "object" && "id" in loaded) { nested.push(loaded); } } if (nested.length) { await resolveRelations(client, childSchema, nested, enabled, { copy: false, schemaLookup: opts?.schemaLookup }); } } } else { await resolveForwardOne(client, rows, name, cache); } } return rows; } export { entityRecordToPlain, bindingEntityId, isSparseBinding, bindingToEntity, hydrateBindings, escapeValue, isWhereFilter, formatEqlLiteral, whereCondition, entitiesQuery, entityQuery, inverseForeignKey, resolveRelations };