Prism · guide

Refused, safely

A config can come from a model or a tenant. What Prism does with one that is wrong, and how to ask without a throw.

Install it
pnpm add @niscorp/prism @niscorp/strata zod
Step 1 of 4

A booking

A booking as the box office stores it: who, which show, which seats, and what a seat costs in cents. Every guide here reads this row or a few like it.

A booking
import { PrismError, evaluateSafe, validate } from '@niscorp/prism';

const booking = {
  id: 'B-2041',
  member: { name: 'Grace', pass: 'season' },
  show: { title: 'The Tempest', starts_at: '2026-10-24T19:30:00+02:00' },
  seats: ['C4', 'C5'],
  seat_price_cents: 3800,
};

// a refusal's code, as Prism names it
const codeOf = (error: Error): string => (error instanceof PrismError ? error.code : error.name);
Step 2 of 4

An operator that does not exist

There is no $fetch, and no operator like it: none opens a file, calls a network or runs a string. validate reads a config against the schema without running it, and names the key.

An operator that does not exist
const reachesOut = { card: { $fetch: { url: 'https://example.com/cards' } } };
const checked = validate(reachesOut);
console.log(checked.ok, checked.ok ? '' : checked.issues[0]);
Step 3 of 4

A path that is not there

A valid config can still fail on the data it is given. evaluateSafe answers { ok, data } or { ok, error } and never throws, and the error carries a code.

A path that is not there
const missing = evaluateSafe({ email: { $ref: '$.member.email' } }, booking);
console.log(missing.ok, missing.ok ? '' : codeOf(missing.error));
Step 4 of 4

A config that grows without end

Forty passes, each doubling a string. Prism counts what an evaluation makes, and stops at its limit, a million characters, long before the fortieth.

A config that grows without end
const passes = Array.from({ length: 40 }, (_unused, index) => index);
const endless = {
  $reduce: {
    over: { $const: passes },
    as: 'pass',
    acc: 'text',
    init: { $const: 'seat ' },
    body: { $join: { parts: [{ $var: 'text' }, { $var: 'text' }], sep: '' } },
  },
};
const grown = evaluateSafe(endless, booking);
console.log(grown.ok, grown.ok ? '' : codeOf(grown.error));
Then

Run it.

Three refusals: one by the schema before anything ran, one for a path the row does not have, and one at the budget.

Run it
npx tsx refused.mts
It prints
false {
  path: [ 'card', '$fetch' ],
  message: 'Not a Prism op. A key that starts with "$" names an op; data with such a key goes in $const.'
}
false E_MISSING_PATH
false E_BUDGET