Skip to content

Core data model: tree nodes, entries, and schema variants

At a glance

packages/types has no runtime logic of its own to speak of — its job is to define the shapes that every other package (node-logic, node-inputs, plugins, schema-serialization, file-viewer) agrees on. This page walks through the central pieces: the tree that represents a mapping, and the schema metadata that wraps it.

Tree nodes: what a mapping is made of

A template's mapping is a tree. Every node in it is one of the EntryType variants: Boolean, Simple, Graph, Internal, Cell, Code, or Map. Map is the odd one out — it's a container (it has children), while the other six are leaf values. That split is captured directly in the types: EntryItemType is EntryType minus Map, and is used anywhere a field should only accept a leaf, not a container (see allowedTypes below).

Every leaf node shares a common shape, BaseTreeNode — an id/key/name, a Zod validation schema, a computed value, a parentId, and a dependencies record (dependsOn / requiredBy, both Set<string>) used to track which other nodes this one relies on. The six leaf variants ( TreeCodeItem, TreeGraphItem, TreeBooleanItem, TreeSimpleItem, TreeCellItem, TreeInternalItem) each intersect BaseTreeNode with a type tag and whatever extra field that variant needs — TreeCellItem adds a spreadsheet column, TreeGraphItem adds a graphId, and so on. Their union is TreeItem.

TreeMap, the container variant, is different: instead of a single value it has children, a forwardMap/reverseMap pair of Record<string, string> for looking up children by key in either direction, and an outputType (one of 'unwrap', 'group', 'array', 'forEach', 'data', 'code') that determines how its children get combined into output. formerOutputType sits alongside it, presumably to detect a change from the previous value — the source doesn't show anything reading it.

Open question

Is formerOutputType on TreeMap read anywhere, or is it write-only bookkeeping for now? Nothing found in packages/ reads it — if you know the answer, this is the line to fix.

TreeNode is the full union, TreeItem | TreeMap — this is the type most other packages import when they mean "a node in the tree" (plugins' framework, for instance, operates on TreeNode).

Building tree nodes

You don't construct a TreeItem or TreeMap by hand — the id and most of BaseTreeNode get filled in at registration time. Before that, a node is an UnregisteredTreeItem<T>: the same shape with id and the rest of BaseTreeNode's keys stripped off.

Two factories build these unregistered nodes:

  • createUnregisteredMap takes a CreateMapItem and returns a bare TreeMap (minus id) with empty forwardMap/reverseMap and validation: z.any().
  • createUnregisteredItem takes an EntryItemType plus a CreateEntryItem, builds the common BaseTreeNode fields, then uses ts-pattern's exhaustive match on the type to fill in the type-specific fields (a Boolean item gets its value cast to boolean, a Cell item gets column: undefined, and so on). The match(...).exhaustive() means adding a new EntryType without updating this function is a compile error, not a silent gap.

For the "is this node a...?" side, the type guards (isTreeItem, isTreeMap, isTreeGraphItem, isTreeSimpleItem, isCodeOutputMap) all check the type (or, for isCodeOutputMap, type plus outputType === 'code') rather than using instanceof, since these are plain data objects, not classes.

Schema variants and schema metadata

SchemaVariantPdf, Table (value 'xls/csv'), Xml, Text, Email, Json — is the one enum almost every package touches. It picks which node-logic rule set applies to a node, and which file-viewer organism renders the source file.

A schema itself (the persisted, top-level record a template is built from) is described by SchemaListItem — id, version, domain/type/variant, a free-form definition, and a SchemaStatus ('draft' | 'active' | 'archived'; only 'active' schemas are usable). CreateSchemaInput and UpdateSchemaInput are the create/patch variants of the same shape.

SchemaSource (File/Email/Http) and EdiType (POST/GET/TokenBased) describe where an inbound file comes from when it's not a manual upload. ediOptionInputs maps each EdiType to the credential fields the UI should render for it (user/password, plus an internal-flagged OAuth field for GET, plus an endpoint field for TokenBased) — each field's schemaVariable is the runtime variable name (e.g. $ediUser) the value gets injected under.

Reserved names

Templates compile down to expressions evaluated against a runtime scope that has certain identifiers pre-injected (data, db, RegExp, emailMessage, ...). reservedNames is the full set of those identifiers.

Silent failure mode

A node's key must not collide with a name in reservedNames. Nothing enforces this at the type level — a colliding key doesn't error, it silently shadows the runtime value instead of referencing it, producing a template that looks fine until it runs.

reservedWrapVariables is a smaller, separate map for identifiers that need to be rewritten to a function call rather than simply reserved (currently just $xlsColumn). ReservedNodeType is a related but distinct enum — node types ($data, $declareVariable, #iterator, #repeatItems, #parseInt) reserved for the tree structure itself, not the runtime variable scope.

The graph-facing wrapper

Everything above describes a node's data. packages/types/src/graph describes how a node is presented and connected in the graph view:

  • Port / Ports — a node's input and output connection points, each with a PortDirection and an optional PortValueKind (string/number/boolean/object/array/any).
  • GraphOperation — what a graph-editing gesture does: Replace, Insert, Add, or Dependency.
  • OptionType / OptionEntry<T> — how one configuration option's value is sourced: user-entered, a reference to a sibling/parent entry, a graph, code, or a boolean; wrapped with an OptionVisibility flag for whether it shows in the node's Quick View.
  • NodeData<T> — the container actually stored on a graph node: ui (Quick View open/closed), graph (dependencies + ports), and logic.options (a NodeOptionsWrapper<T>, i.e. one OptionEntry per key of the node's logic-block options — see Logic & structure blocks).

The Input enum

One more enum lives in packages/types: Input. It's the registry of form-input kinds a node's configuration panel can use. types only defines the enum; the components it maps to live in node-inputs — see Input widget registry.