Prism Documentation
Complete reference for every operation in @niscorp/prism.
Concepts
Configs and Nodes
A Prism config is a JSON structure that describes a transformation. It can be:
- A primitive (
"hello",42,true,null) — returned as-is - An op node (
{ $ref: "$.path" }) — evaluated by the engine - A plain object (
{ name: { $ref: "$.user.name" } }) — each value evaluated recursively - An array (
[{ $const: 1 }, { $ref: "$.x" }]) — each element evaluated
A key that starts with $ is an op's name. A plain object has none: { "$fetch": … } is not a config, because Prism has no $fetch — validate refuses it with the key's path (card.$fetch), and evaluate and compile fail with E_SCHEMA before anything runs. Data that has such keys goes in $const, which returns its value untouched; $fromEntries builds one from computed values.
Source Data
Every evaluation takes a source — the data the transformation reads from via $ref. evaluate accepts any JSON value (object, array, or scalar); "$" is the whole of it.
Bindings and Variables
$ref reads from source. $var reads from scoped variables created by $with, $map, $filter, $reduce, $sortBy, $keyBy, $groupBy, $update, and $walk.
Optional Fields
Plain objects support an __optional metadata key that lists field names to silently omit when they evaluate to null or their path is missing:
{
"name": { "$ref": "$.user.name" },
"nickname": { "$ref": "$.user.nickname" },
"__optional": ["nickname"]
}
If $.user.nickname doesn't exist, the result is { "name": "Alice" } — no error.
Core Operations
$ref — Read from source
Resolves a path against the source data. A path is keys and indexes: .key and [index]. The rest of JSONPath — a wildcard [*], a filter [?(…)], a slice, a negative index, a quoted key, .. — is refused when the config is checked (E_SCHEMA, "Not a path Prism reads"); for every item of a list use $map or $pluck. The JSON Schema from getConfigJsonSchema, getNodeJsonSchema and getProfileJsonSchema does not carry this rule: its pattern for $ref is ^\$(\..*)?$, so a path can pass the JSON Schema and still be refused by validate.
{ "$ref": "$.user.name" }
{ "$ref": "$.items[0].sku" }
{ "$ref": "$.deeply.nested.value" }
{ "$ref": "$" }
"$" is the whole source. Throws E_MISSING_PATH if the path doesn't exist.
$ref and $var read the context, so the compiler never constant-folds them.
$const — Literal value
Returns a JSON value unchanged. Use this when you need a fixed value inside an expression.
{ "$const": 42 }
{ "$const": "hello" }
{ "$const": { "key": "value" } }
{ "$const": [1, 2, 3] }
{ "$const": null }
$var — Read a variable
Reads a variable from the current scope (created by $with, $map, $filter, etc.).
{ "$var": "item" }
{ "$var": "accumulator" }
Throws E_VAR_NOT_FOUND if the variable doesn't exist in scope.
$get — Dynamic path access
Navigates into a value using an array of path segments. Segments can be strings (object keys), numbers (array indices), or node expressions (evaluated dynamically).
{ "$get": { "from": { "$ref": "$.user" }, "path": ["address", "city"] } }
{ "$get": { "from": { "$ref": "$.items" }, "path": [0, "name"] } }
{ "$get": { "from": { "$ref": "$.data" }, "path": ["missing"], "fallback": { "$const": "N/A" } } }
Without fallback, throws E_MISSING_PATH when the path is missing, and E_TYPE when a key is asked of a non-object or an index of a non-array. With fallback, returns the fallback value in either case.
Dynamic segments:
{
"$get": {
"from": { "$ref": "$.catalog" },
"path": [{ "$ref": "$.selectedSku" }, "title"]
}
}
$with — Scoped variables
Binds variables in a scoped block. Variables are available only inside value.
{
"$with": {
"let": {
"user": { "$ref": "$.user" },
"tax": { "$mul": [{ "$ref": "$.subtotal" }, { "$const": 0.2 }] }
},
"value": {
"name": { "$get": { "from": { "$var": "user" }, "path": ["name"] } },
"totalWithTax": { "$add": [{ "$ref": "$.subtotal" }, { "$var": "tax" }] }
}
}
}
Array Operations
$map — Transform each element
Iterates over an array, binding each element to a variable, and evaluates the body for each.
{
"$map": {
"over": { "$ref": "$.items" },
"as": "item",
"body": { "$get": { "from": { "$var": "item" }, "path": ["name"] } }
}
}
Source: { "items": [{ "name": "A" }, { "name": "B" }] } → ["A", "B"]
$filter — Keep matching elements
Keeps elements where the when condition evaluates to truthy.
{
"$filter": {
"over": { "$ref": "$.numbers" },
"as": "n",
"when": { "$gt": [{ "$var": "n" }, { "$const": 10 }] }
}
}
Source: { "numbers": [5, 15, 3, 20] } → [15, 20]
$reduce — Fold/accumulate
Reduces an array to a single value. Binds each element as as and the running accumulator as acc (customizable).
{
"$reduce": {
"over": { "$ref": "$.numbers" },
"as": "n",
"acc": "total",
"init": { "$const": 0 },
"body": { "$add": [{ "$var": "total" }, { "$var": "n" }] }
}
}
Source: { "numbers": [1, 2, 3] } → 6
The acc name defaults to "acc" if omitted.
$slice — Slice array or string
{ "$slice": { "from": { "$ref": "$.items" }, "start": 1, "end": 3 } }
{ "$slice": { "from": { "$ref": "$.text" }, "start": 0, "end": 5 } }
start defaults to 0, end defaults to length.
$flatten — Flatten one level
{ "$flatten": { "$ref": "$.nested" } }
Source: { "nested": [[1, 2], [3], [4, 5]] } → [1, 2, 3, 4, 5]
Non-array elements are kept as-is: [[1], 2, [3]] → [1, 2, 3]
$unique — Deduplicate
Removes duplicate values, compared by JSON.stringify.
{ "$unique": { "$ref": "$.tags" } }
Source: { "tags": ["a", "b", "a", "c", "b"] } → ["a", "b", "c"]
$sortBy — Sort by computed key
{
"$sortBy": {
"over": { "$ref": "$.items" },
"as": "item",
"by": { "$get": { "from": { "$var": "item" }, "path": ["price"] } },
"dir": "desc"
}
}
dir defaults to "asc". A sort key is a number or a string. Where the keys are of more than one kind, numbers come first, then strings, then false and true, and "desc" turns that around; an item whose key is null is last in both directions. Items with equal keys keep the order they came in. A key that is a list or an object is refused (E_TYPE): to sort by two keys, sort by the second and then by the first — the sort keeps the order of equal items.
Math Operations
All math ops take a [left, right] pair. Both operands are evaluated and must be numbers.
$add
{ "$add": [{ "$ref": "$.price" }, { "$ref": "$.tax" }] }
$sub
{ "$sub": [{ "$ref": "$.total" }, { "$ref": "$.discount" }] }
$mul
{ "$mul": [{ "$ref": "$.quantity" }, { "$ref": "$.unitPrice" }] }
$div
{ "$div": [{ "$ref": "$.total" }, { "$ref": "$.count" }] }
Throws E_DIVISION_BY_ZERO if the divisor is 0.
$mod
{ "$mod": [{ "$ref": "$.row" }, 2] }
The remainder of the first number divided by the second. It takes the sign of the second number, so with a positive divisor n the answer is always from 0 up to n — { "$mod": [-1, 5] } is 4, where JavaScript's % gives -1. Throws E_DIVISION_BY_ZERO if the divisor is 0.
$round
{ "$round": { "value": { "$const": 3.14159 }, "digits": 2 } }
Result: 3.14. digits defaults to 0. mode is "nearest" unless given: "floor" rounds down (toward negative infinity) and "ceil" rounds up, at the same digits.
{ "$round": { "value": { "$div": [{ "$ref": "$.minutes" }, 60] }, "mode": "floor" } }
$toNumber — A number from text
{ "$toNumber": { "value": { "$ref": "$.price" } } }
{ "$toNumber": { "value": { "$ref": "$.discount" }, "fallback": 0 } }
A number is answered as it is. Text is read as a number when it is one written in digits, with spaces around it allowed: "42", "-3.5", ".5", " 1e3 ". Everything else — "", "12 kg", "1,234", "0x10", true, null, a list — throws E_TYPE, or answers fallback where it is given. The other ops do not convert: $add on "4" is still an error.
String Operations
$join — Concatenate with separator
{ "$join": { "parts": [{ "$ref": "$.first" }, { "$ref": "$.last" }], "sep": " " } }
Parts are coerced to strings. sep defaults to "". parts may also be any
node that evaluates to an array — a mapped list, a $var:
{ "$join": { "parts": { "$map": { "over": { "$ref": "$.tags" }, "as": "t", "body": { "$upper": { "$var": "t" } } } }, "sep": ", " } }
$toString — Stringify
{ "$toString": { "$ref": "$.count" } }
Numbers → "42", null → "null", objects → JSON string.
$interpolate — Template string
{
"$interpolate": {
"template": "Hello {{name}}, you have {{count}} items",
"values": { "$ref": "$.user" }
}
}
Replaces {{key}} placeholders with values from the evaluated object. Missing keys become empty strings.
$fill — Fill a counted phrase
{ "$fill": { "$ref": "$.progress" } }
Source: { "progress": { "phrase": "{n} of {total}", "slots": { "n": 1, "total": 12 } } } → "1 of 12"
Takes a node that evaluates to a pattern value { phrase, slots } and replaces each {name} hole with its slot, in the source language. A slot that is itself a pattern is filled recursively; a hole with no slot (or a null one) is left as written. Anything that is not a pattern passes through unchanged.
$trim
{ "$trim": { "$const": " hello " } }
Result: "hello"
$lower
{ "$lower": { "$ref": "$.name" } }
$upper
{ "$upper": { "$ref": "$.code" } }
$split — String to array
{ "$split": { "value": { "$const": "a,b,c" }, "sep": "," } }
Result: ["a", "b", "c"]
$replace — Replace a substring
{ "$replace": { "value": { "$ref": "$.text" }, "search": "world", "replacement": "there" } }
{ "$replace": { "value": { "$ref": "$.phone" }, "search": " ", "replacement": "", "all": true } }
The first occurrence of search, or every occurrence with all: true. search is plain text, not a pattern.
Predicate Operations
All predicates return true or false.
$eq / $neq — Deep equality
{ "$eq": [{ "$ref": "$.status" }, { "$const": "active" }] }
{ "$neq": [{ "$ref": "$.role" }, { "$const": "admin" }] }
Compares by JSON.stringify — works for primitives, arrays, and objects.
$gt / $gte / $lt / $lte — Ordered comparison
{ "$gt": [{ "$ref": "$.age" }, { "$const": 18 }] }
{ "$lte": [{ "$ref": "$.score" }, { "$const": 100 }] }
Works with numbers and strings (lexicographic).
$empty — Emptiness check
{ "$empty": { "$ref": "$.items" } }
Returns true for: null, "", [], {}. Returns false for everything else (including 0 and false).
$startsWith / $endsWith / $contains — String checks
{ "$startsWith": { "value": { "$ref": "$.url" }, "prefix": { "$const": "https://" } } }
{ "$endsWith": { "value": { "$ref": "$.file" }, "suffix": { "$const": ".json" } } }
{ "$contains": { "value": { "$ref": "$.text" }, "search": { "$const": "error" } } }
Logic Operations
$not — Boolean negation
{ "$not": { "$ref": "$.isDisabled" } }
Negates truthy/falsy: true → false, 0 → true, null → true.
$and — Short-circuit AND
{ "$and": [{ "$ref": "$.isActive" }, { "$ref": "$.hasPermission" }] }
Returns the last truthy value, or the first falsy value. Short-circuits.
$or — Short-circuit OR
{ "$or": [{ "$ref": "$.nickname" }, { "$ref": "$.name" }, { "$const": "Anonymous" }] }
Returns the first truthy value, or the last falsy value. Short-circuits.
Structure Operations
$merge — Shallow merge objects
{ "$merge": [{ "$ref": "$.defaults" }, { "$ref": "$.overrides" }] }
Left to right, later values win. All elements must evaluate to objects.
$coalesce — First non-null
{ "$coalesce": [{ "$ref": "$.preferred" }, { "$ref": "$.fallback" }, { "$const": "default" }] }
Returns the first value that is not null or undefined.
$case — Conditional branching
{
"$case": {
"branches": [
{ "when": { "$gt": [{ "$ref": "$.score" }, { "$const": 90 }] }, "then": { "$const": "A" } },
{ "when": { "$gt": [{ "$ref": "$.score" }, { "$const": 80 }] }, "then": { "$const": "B" } },
{ "when": { "$gt": [{ "$ref": "$.score" }, { "$const": 70 }] }, "then": { "$const": "C" } }
],
"else": { "$const": "F" }
}
}
Evaluates branches in order, returns the then of the first truthy when. Falls back to else (or null if omitted).
$entriesOf — Object to entries
{ "$entriesOf": { "$ref": "$.config" } }
{ "a": 1, "b": 2 } → [["a", 1], ["b", 2]]
$keyBy — Array to object by key
{
"$keyBy": {
"over": { "$ref": "$.users" },
"as": "user",
"key": { "$get": { "from": { "$var": "user" }, "path": ["id"] } }
}
}
[{ "id": "u1", "name": "Alice" }] → { "u1": { "id": "u1", "name": "Alice" } }
Last element wins on key collision.
$groupBy — Array to grouped object
{
"$groupBy": {
"over": { "$ref": "$.items" },
"as": "item",
"key": { "$get": { "from": { "$var": "item" }, "path": ["category"] } }
}
}
Groups elements into arrays by computed key: { "fruit": [...], "vegetable": [...] }
Object Operations
$keys
{ "$keys": { "$ref": "$.config" } }
{ "a": 1, "b": 2 } → ["a", "b"]
$values
{ "$values": { "$ref": "$.config" } }
{ "a": 1, "b": 2 } → [1, 2]
$fromEntries
{ "$fromEntries": { "$const": [["x", 1], ["y", 2]] } }
Result: { "x": 1, "y": 2 }
$pick — Keep specific keys
{ "$pick": { "from": { "$ref": "$.user" }, "keys": ["name", "email"] } }
$omit — Remove specific keys
{ "$omit": { "from": { "$ref": "$.user" }, "keys": ["password", "secret"] } }
$type — Get value type
{ "$type": { "$ref": "$.value" } }
Returns one of: "string", "number", "boolean", "null", "array", "object".
$length — Array or string length
{ "$length": { "$ref": "$.items" } }
{ "$length": { "$ref": "$.name" } }
Transform Operations
Rewriting a document rather than deriving a value — built for migrations (strata's document steps), useful anywhere a config returns its input with a precise edit.
$has — Is the path there at all?
{ "$has": { "from": { "$ref": "$.document" }, "path": ["props", "label"] } }
True when the whole path exists — a key present with null counts; a missing
one does not (which $get with a fallback cannot tell apart).
$renameKeys — Rename in place
{ "$renameKeys": { "from": { "$ref": "$.endpoint" }, "map": { "body": "request", "transform": "response" } } }
Each renamed key keeps its position. Absent keys are ignored; a rename onto an existing key replaces that entry.
$update — One path changed, everything else identical
{ "$update": { "from": { "$ref": "$.document" }, "path": ["props", "count"], "value": { "$add": [{ "$var": "current" }, 1] } } }
The value currently at path (null when absent) is bound as current (rename
it with "as"). Missing objects along a string path are created; an array index
that does not exist is E_MISSING_PATH.
$assert — Refuse loudly
{ "$assert": { "when": { "$has": { "from": { "$ref": "$.document" }, "path": ["id"] } }, "message": "An action without an id cannot be migrated.", "value": { "$ref": "$.document" } } }
Throws E_ASSERT with message unless when is truthy; otherwise evaluates to
value.
$walk — Rewrite every node of a tree
{
"$walk": {
"over": { "$ref": "$.document" },
"as": "n",
"rules": [
{ "when": { "$eq": [{ "$var": "n" }, "$.q"] }, "then": "$.search" },
{ "when": { "$eq": [{ "$get": { "from": { "$var": "n" }, "path": ["ref"], "fallback": null } }, "q"] },
"then": { "$update": { "from": { "$var": "n" }, "path": ["ref"], "value": "search" } } }
]
}
}
Visits every node — objects, arrays and leaves, at any depth — and replaces it
with the first rule whose when holds (bound to as); no rule, the node stays.
"order": "post" (default) rewrites children first, so a node's rules see them
rewritten; "pre" applies the rules first and descends into the result. A
replacement is never walked again at its own level, so it always terminates. A
walk visits objects too: guard a string op with a type check
({ "$and": [{ "$eq": [{ "$type": { "$var": "n" } }, "string"] }, …] }).
The op set only ever grows
Configs are stored — endpoint requests, vex mappings, strata migrations — and a
migration cannot be migrated by the language it is written in. So an op is
never removed or reshaped: an old form stays and desugars to the new one.
test/transform.test.ts keeps the record (EVERY_OP_EVER) and fails on a
removal. Changes to the grammar are strata migrations on nisc.prism
(@niscorp/prism/migrations), gated by pnpm check:grammars.
Time Operations
Powered by dayjs. Date values can be ISO 8601 strings or Unix timestamps (milliseconds). A date-only string (YYYY-MM-DD) is parsed and computed in UTC, so the calendar day survives the round trip.
$date — Format a date
{ "$date": { "value": { "$ref": "$.createdAt" } } }
{ "$date": { "value": { "$ref": "$.createdAt" }, "format": "YYYY-MM-DD" } }
{ "$date": { "value": { "$ref": "$.createdAt" }, "format": "HH:mm", "utc": true } }
Without format, returns ISO 8601. With format, uses dayjs format tokens.
$date is locale-blind — MMM is Mar in every language it will ever run
in. That is right for a machine-readable stamp and wrong for anything a person
reads: for that, use $localeDate below.
$dateAdd — Date arithmetic
{ "$dateAdd": { "date": { "$ref": "$.startDate" }, "amount": 30, "unit": "day" } }
{ "$dateAdd": { "date": { "$ref": "$.now" }, "amount": -1, "unit": "hour" } }
Returns ISO 8601 string — or, when the input is a date-only string ("2026-06-01"), a date-only string ("2026-07-01"). Units: year, month, day, hour, minute, second.
$dateDiff — Date difference
{ "$dateDiff": { "from": { "$ref": "$.start" }, "to": { "$ref": "$.end" }, "unit": "day" } }
Returns a number. Same units as $dateAdd.
Locale-Aware Formatting
Three ops that turn a value into text for a person reading in a particular
language. They delegate to the platform's own Intl, so the rules come from
CLDR rather than from a table in this package.
locale is required on all three. There is no default, deliberately: a default
renders something plausible for everybody it is wrong for, and the only way to
discover it is a reader in Vienna being shown American dates.
All three take an optional fallback (default ""), returned when value is
null or an empty string — before locale is even looked at.
$localeMoney — an amount as money
{ "$localeMoney": { "value": { "$ref": "$.price_cents" }, "currency": { "$ref": "$.currency" }, "locale": "de-AT" } }
{ "$localeMoney": { "value": { "$ref": "$.total" }, "currency": "EUR", "locale": "de-DE", "minorUnits": false, "digits": 0 } }
Why this is not a $join of a symbol and a number — one currency, one language,
three countries:
| locale | output |
|---|---|
de-AT |
€ 45,00 |
de-DE |
45,00 € |
de-CH |
EUR 45.00 |
en-IE |
€45.00 |
valueis in minor units (cents) by default. The divisor comes from the currency, not a hardcoded100— JPY has no minor unit, and dividing it would be a hundredfold error. PassminorUnits: falsefor major units.digitsoverrides the currency's own fraction digits.fallback(default"") is rendered for a null/absent amount, so an empty SUM shows a dash rather than a confident€0.00.- An ISO code
Intldoes not know prints beside the number instead of throwing.
$localeDate — a date for a human
{ "$localeDate": { "value": { "$ref": "$.starts_on" }, "locale": "de-AT" } }
{ "$localeDate": { "value": { "$ref": "$.starts_on" }, "locale": "de-AT", "options": { "weekday": "short", "day": "numeric", "month": "short" } } }
options is a checked subset of Intl.DateTimeFormatOptions (dateStyle,
timeStyle, weekday, year, month, day, hour, minute, second,
hour12, timeZone). Defaults to { "dateStyle": "medium" }.
DATE-only values are read as UTC. "2026-03-14" is UTC midnight, and
formatting UTC midnight in any zone west of Greenwich prints the 13th. Every
date column in a schema has this shape, so the trap is handled once here rather
than rediscovered at each call site. An explicit timeZone still wins.
$localeNumber — a number
{ "$localeNumber": { "value": { "$ref": "$.attendance" }, "locale": "de-DE", "minDigits": 1 } }
{ "$localeNumber": { "value": { "$ref": "$.rate" }, "locale": "en-GB", "style": "percent" } }
style is decimal (default) or percent — percent multiplies by 100, so pass
0.42, not 42. digits/minDigits cap and floor the fraction digits;
compact gives 1.2k. Grouping and the decimal mark follow the locale
(1.234,5 in Germany, 1 234,5 in Austria, 1,234.5 in Britain).
Sugar Operations
Convenience shorthands that desugar to core operations before evaluation. You can use these anywhere — they're expanded in a single pass before the config is evaluated or compiled.
$sum — Sum an array
{ "$sum": { "over": { "$ref": "$.prices" } } }
Desugars to $reduce + $add. Empty array → 0.
$avg — Average an array
{ "$avg": { "over": { "$ref": "$.scores" } } }
Desugars to $div($sum, $count). Empty array → E_DIVISION_BY_ZERO.
$count — Count elements
{ "$count": { "over": { "$ref": "$.items" } } }
Empty array → 0.
$min / $max — Find extremes
{ "$min": { "over": { "$ref": "$.prices" } } }
{ "$max": { "over": { "$ref": "$.scores" } } }
Empty array → null.
$pluck — Extract a field from each element
{ "$pluck": { "over": { "$ref": "$.users" }, "key": "name" } }
[{ "name": "A" }, { "name": "B" }] → ["A", "B"]
$take / $drop — Slice from start
{ "$take": { "from": { "$ref": "$.items" }, "count": 3 } }
{ "$drop": { "from": { "$ref": "$.items" }, "count": 2 } }
$match — Filter by string containment
{ "$match": { "over": { "$ref": "$.words" }, "as": "w", "search": { "$const": "hel" } } }
Keeps elements where the element contains the search string.
$flatMap — Map then flatten
{
"$flatMap": {
"over": { "$ref": "$.users" },
"as": "user",
"body": { "$get": { "from": { "$var": "user" }, "path": ["tags"] } }
}
}
Each body should return an array. Results are flattened one level.
Compilation
For configs that will be executed many times against different source data, compile once and execute many:
const ir = await compile(config, { name: 'user-transform', version: '1.0.0' });
// ir contains:
// - Desugared config (sugar ops already resolved)
// - SHA256 fingerprint (for cache invalidation)
// - Stats (node count, op frequency, max depth, optimizations applied)
// - Tables (every $ref string and string literal in the tree)
const result1 = execute(ir, source1); // No validation, no desugaring
const result2 = execute(ir, source2); // Each op's handler already attached
The IR is JSON-serializable — store it in a database, cache in Redis, send over a wire.
A constant in an answer is frozen. Every execute of an IR hands out the IR's own constants: a $const that is a list or an object, and anything the compiler worked out ahead of time because it did not depend on the source ({ "$split": { "value": "a,b", "sep": "," } } is a constant list). They are frozen, all the way down, so one caller cannot change what the next is given: answer.tags.push(x) on a list that came from { "$const": [] } throws a TypeError. To change an answer, copy the part first ([...answer.tags, x]). Everything built for the call — a template's object, a $map's list — is the caller's own. evaluate does not freeze: what it answers may be changed.
evaluate
evaluate(config: unknown, source: unknown, options?: { limits?: Partial<Limits>; check?: 'always' }): JsonValue
evaluateSafe(config, source, options?): { ok: true, data } | { ok: false, error }
A config and a source in, the answer out.
- The config is checked against the schema the first time its object is
handed over, and refused with a
PrismError(E_SCHEMA) that names the part that is wrong. Its tree is kept: the second call with the same object prepares it ascompilewould (handlers attached, paths parsed, constants folded), and every call after that only evaluates. It is kept by the object, so a config changed in place after its first call is not read again — hand over a new object. A config that is a new object on every call is checked on every call, and is not prepared: that would cost more than it saves. - The source is taken as it is. Nothing checks it.
- The answer is the caller's to change: a constant is handed out as a copy.
It is the function a host's transform seam takes. nova's shell (transform),
tide's engine (transform) and strata's upgrader ({ transform }) each run a
config through a function they are handed and know nothing of Prism:
import { evaluate } from '@niscorp/prism';
const tide = createTide({ store, transform: evaluate, effects });
check: 'always'
evaluate(config, source, { check: 'always' })
For writing a config, for tests and for tools. Nothing is kept and nothing is
taken on trust: the config is checked on this call whatever was checked before,
and the source must be plain JSON — null, strings, booleans, finite numbers,
and arrays and plain objects of those. One that holds undefined, a function,
a Date or a non-finite number is refused: E_TYPE, "The source must be plain
JSON."
Which call
| You have | Call |
|---|---|
| a config your program holds, run again and again | evaluate(config, source) |
| a config being written, a test, a tool | evaluate(config, source, { check: 'always' }) |
| a config that is stored, or that must run without its checks | compile once, execute the IR |
Limits
evaluate and evaluateSafe take them as options.limits, execute as its
third argument: a Partial<Limits>, merged over DEFAULT_LIMITS:
| Limit | Default | Counts |
|---|---|---|
maxSteps |
1_000_000 |
Nodes evaluated in one evaluation |
maxStringLength |
1_000_000 |
Characters in any one string a node produces |
maxValues |
1_000_000 |
Values (scalars, array items, object fields) in a $reduce accumulator, a $with binding, or the result |
Past any of them the evaluation throws E_BUDGET. maxValues counts a value
each time it appears, as it would be serialized: a $reduce whose body is
[{ "$var": "acc" }, { "$var": "acc" }] doubles its count with every item.
Error Codes
| Code | Meaning |
|---|---|
E_SCHEMA |
Config failed Zod validation — a malformed op, or a $ key that is not an op |
E_MISSING_PATH |
$ref or $get path doesn't exist (and no fallback) |
E_TYPE |
Wrong type for operation (e.g. $map.over is not an array) |
E_DIVISION_BY_ZERO |
$div with divisor 0 |
E_DATE_INVALID |
Invalid date value |
E_VAR_NOT_FOUND |
$var references undefined variable |
E_NODE_SHAPE |
A node no op answers to, in a tree that never went through the schema |
E_ASSERT |
A config's own $assert refused its input |
E_BUDGET |
Evaluation went past a limit (see Limits) |
All errors are instances of PrismError with .code and optional .context.
Valid, and what it does not promise. validate, evaluate and compile hold a config to the same schema, so a config that validates has no node the evaluator cannot dispatch: every object in it is an op Prism has or a plain object with no $ key. It can still fail on the data it is given — a missing path, a wrong type, a budget. E_NODE_SHAPE is what is left for a tree the schema never saw: an IR handed to execute, which does not validate.