@factory/database-engine
The pure database engine for the Notion-like product: value codecs, the
filter/sort/group/query pipeline, relations, rollups, the safe formula engine,
and button/view helpers. No Prisma, no I/O, no React (AGENTS.md rules 23/24).
apps/api owns persistence + permissions; apps/web owns rendering; both
share this code path (TASK-0067 R3 — "same code path, no drift").
Public API (subpath exports)
| Export | Contents |
|---|---|
. |
the full public surface (barrel) |
./values |
the 24 property-type ValueCodecs (fromWire/toWire/display/compare), date math (UTC-day buckets, calendar-unit add with month-end clamp), the conversion matrix |
./filter |
evaluateCondition / evaluateTree / applyFilter — per-type operator matrices, me/today/now tokens, relative windows (this/last/next week|month, this year), and/or/not trees |
./query |
runRecordQuery (base set → filter → sort → group|paginate), stable multi-key sort (≤5, nullOrder), ≤3-level grouping (empty group always last), opaque keyset cursors, EAV pivot |
./relations |
link diffing, cardinality policy (1:1 replacement, 1:N one-side cap), planSetLinks (the transaction plan incl. displaced partners), cascade + relation counts |
./rollups |
aggregateRollup (COUNT/SUM/AVERAGE/MIN/MAX/PERCENT/UNIQUE, TASK-0083 empty-set rules), inferredType, aggregateAllowed |
./formula |
the safe formula engine (below) |
./views |
per-type defaultViewConfig for the 9 view types, pure viewData render resolvers, button config validation + {token} templates |
Formula engine (TASK-0084)
parseFormula(src) → AstNode (the FormulaAstNode union lives in
@factory/shared/dto — the editor's autocomplete and the Phase-12 AI
generator consume the same shape) · validateFormula(src, props, types?)
(static return type, sorted dependencies, usesNow) · evaluateFormula(ast, ctx).
- 15 functions:
if, and, or, not, sum, average, min, max, dateAdd, dateBetween, now, concat, replace, contains, length+ theprop(name)accessor. - Zero arbitrary JS — a closed lexer/parser/AST table (rule 18).
evalis not even a registered function name. - Safety rails: 10 000-step budget (RUNTIME), 16-level nesting (PARSE), 1 MB output cap, JSON-serializable AST.
- Semantics (goldens in
formula.test.ts+ deterministic 100-case fuzz):sum([])=0butaverage([])=min([])=max([])=null;1/0=null; null propagates through arithmetic andand/or/not(which short-circuit lazily);==is total (different types →false); ordering and+are strict;date + n→ TYPE with adateAddhint; unknown property → TYPE with a Levenshtein "Did you mean …?" suggestion.
Recorded decisions / spec conflicts
- 24 property types (spec text says "22") — the
propertyTypeSchemain@factory/shared/dtois the source of truth; the engine covers all 24. - AST +
NullLiteral/ArrayLiterallive in@factory/shared/dto(the spec's union had neither; list arguments and the null matrix need them). - Filter operators: the engine accepts a superset of the shared
filterOperatorSchema(e.g.isBetween,isOnOrAfter,has_files,isChecked, relative date tokens) — the shared enum is the API boundary, the engine is lenient inward. - Identifier pattern
[A-Za-z][A-Za-z0-9_]*(the spec's lowercase-only pattern cannot express the function set —dateAddhas a capital). - Buttons:
ButtonActionSchemawas specified in@factory/shared, but shared is frozen for this phase — the pure validation/planning helpers live here (./views) and the API validates with them. - Views:
viewDataproduces render-ready data only; the React renderers live inapps/web(rule 23). Form view is Phase 10 —defaultViewConfigreturns its shape but the renderer reportsform_view_phase_10.
Usage
import { runRecordQuery, applyFilter, validateFormula, evaluateFormula, parseFormula } from "@factory/database-engine";
// query pipeline (the API does the Prisma read, the engine computes)
const page = runRecordQuery({ records, properties, filter, sort, pageSize: 50 });
// formula (save-time validation + read-time evaluation, both pure)
const { returnType, dependencies, usesNow } = validateFormula(src, recordProperties, propertyTypes);
const { value } = evaluateFormula(parseFormula(src), { now: Date.now(), properties });
Status
Source-first (Phase 0 completion adds the build step). Tests: Vitest files
colocated in src/ (values, filter, query, relations, rollups,
formula incl. fuzz, views) — run in CI once pnpm install lands. Pure
engines verified headlessly (Node type-stripping) with 181 checks, 0 failures.
ROADMAP: Phase 3 (this), Phase 12 consumes validateFormula.dependencies
(recompute worker) and the formula AST (AI formula generator).