Skip to content

revisium/schema-toolkit

Repository files navigation

@revisium/schema-toolkit

Quality Gate Status Coverage GitHub License GitHub Release

Framework-agnostic TypeScript types, system schemas, runtime stores, and utilities for working with JSON Schema in Revisium projects.

Installation

npm install @revisium/schema-toolkit

Quick Start

Schema helpers

import { obj, str, num, bool, arr } from '@revisium/schema-toolkit';

const schema = obj({
  name: str(),
  age: num(),
  active: bool(),
  tags: arr(str()),
});

RowModel

import { obj, str, num, createRowModel } from '@revisium/schema-toolkit';

const schema = obj({ name: str(), price: num() });

const row = createRowModel({
  rowId: 'row-1',
  schema,
  data: { name: 'Widget', price: 9.99 },
});

row.getValue('name');           // string (typed!)
row.setValue('price', 19.99);   // OK
row.setValue('price', 'wrong'); // TS Error!
row.getPlainValue();            // { name: string, price: number }
row.patches;                    // JSON Patch operations
row.root;                       // typed root node (InferNode<S>)
row.reset({ name: 'New', price: 0 }); // reset to new data, commit
row.reset();                    // reset to schema defaults

Array Search

Arrays expose find and findIndex for searching elements by node properties:

import { obj, str, arr, createRowModel } from '@revisium/schema-toolkit';

const schema = obj({
  sorts: arr(obj({ field: str(), direction: str() })),
});

const row = createRowModel({
  rowId: 'row-1',
  schema,
  data: { sorts: [
    { field: 'name', direction: 'asc' },
    { field: 'age', direction: 'desc' },
  ]},
});

const sortsNode = row.get('sorts');
// find returns the typed node
const ageSort = sortsNode.find(
  (node) => node.child('field').getPlainValue() === 'age',
);
// findIndex returns the index
const idx = sortsNode.findIndex(
  (node) => node.child('field').getPlainValue() === 'age',
); // 1

TableModel

import { obj, str, num, bool, createTableModel } from '@revisium/schema-toolkit';

const schema = obj({ title: str(), price: num(), inStock: bool() });

const table = createTableModel({
  tableId: 'products',
  schema,
  rows: [
    { rowId: 'p1', data: { title: 'Laptop', price: 999, inStock: true } },
  ],
});

const row = table.getRow('p1');
row?.getValue('title');         // string (typed!)
row?.getPlainValue();           // { title: string, price: number, inStock: boolean }

const newRow = table.addRow('p2', { title: 'Mouse', price: 29, inStock: true });
newRow.setValue('price', 39);   // OK
newRow.setValue('price', 'x');  // TS Error!

Typed API without helpers

When the schema is typed (via helpers or as const), createRowModel / createTableModel return typed models automatically. With plain JsonSchema they return the untyped API as before:

import { createRowModel } from '@revisium/schema-toolkit';
import type { JsonObjectSchema } from '@revisium/schema-toolkit';

// Untyped — returns plain RowModel with unknown types
const schema: JsonObjectSchema = getSchemaFromApi();
const row = createRowModel({ rowId: 'row-1', schema, data });
row.getValue('name'); // unknown

See Typed API documentation for all approaches: as const, explicit type declarations, SchemaFromValue<T>, and more.

Reactivity (MobX)

By default all models use a noop reactivity provider, which works for backend and plain scripts. To enable MobX reactivity (e.g. in a React app), configure the provider once at startup:

import * as mobx from 'mobx';
import { setReactivityProvider, createMobxProvider } from '@revisium/schema-toolkit/core';

setReactivityProvider(createMobxProvider(mobx));

After this call every model created via createRowModel, createTableModel, or createDataModel becomes fully observable.

See Reactivity Module docs for the full API, noop behaviour table, and test-setup examples.

Formulas (Computed Fields)

Fields with x-formula are automatically computed from other fields. Use readOnly: true and the formula option in helpers:

import { obj, num, numFormula, createRowModel } from '@revisium/schema-toolkit';

const schema = obj({
  price: num(),
  quantity: num(),
  subtotal: numFormula('price * quantity'),
  tax: numFormula('subtotal * 0.1'),
  total: numFormula('subtotal + tax'),
});

const row = createRowModel({
  rowId: 'order-1',
  schema,
  data: { price: 100, quantity: 5, subtotal: 0, tax: 0, total: 0 },
});

row.getPlainValue();
// { price: 100, quantity: 5, subtotal: 500, tax: 50, total: 550 }

Formulas are evaluated in dependency order. With the MobX reactivity provider configured, changing a dependency triggers automatic re-evaluation of all affected formulas.

Expression Syntax

Syntax Example
Field reference price, item.quantity
Arithmetic price * quantity, a + b - c
Comparison & logic a > b && c < d, x ? y : z
Absolute path /rootField
Relative path ../siblingField
Array access items[0].price, items[*].price
Array context #index, #length, @prev, @next
Functions sum(items[*].price), avg(values), count(array)

Schema-Level Integration

When fields are renamed or moved, formula expressions are automatically updated in the generated patches:

// rename price → cost
// formula 'price * quantity' → 'cost * quantity' (auto-updated)

Warnings

The evaluator tracks problematic results (nan, infinity, runtime-error) on the node's formulaWarning property.

See value-formula docs for the runtime engine API and schema-formula docs for parsing, dependency tracking, and serialization.

Foreign Key Resolution

Schemas with foreignKey fields (string fields referencing another table) can be resolved automatically via ForeignKeyResolver:

import { createForeignKeyResolver, createTableModel, obj, str } from '@revisium/schema-toolkit';

const resolver = createForeignKeyResolver({
  loader: {
    loadSchema: async (tableId) => api.getTableSchema(tableId),
    loadRow: async (tableId, rowId) => api.getRow(tableId, rowId),
  },
  prefetch: true,
});

const table = createTableModel({
  tableId: 'products',
  schema: obj({ name: str(), categoryId: str({ foreignKey: 'categories' }) }),
  rows: [{ rowId: 'p1', data: { name: 'Laptop', categoryId: 'cat-1' } }],
  fkResolver: resolver,
});

// Referenced data is prefetched in the background and available from cache
const category = await resolver.getRowData('categories', 'cat-1');

The same fkResolver option is accepted by createRowModel. When using createDataModel, pass the resolver once and all tables will share it.

See ForeignKeyResolver docs for cache-only mode, prefetch control, loading state, and error handling.

API

Schema Helpers

Function Description
str() Create string schema
num() Create number schema
bool() Create boolean schema
strFormula(expr) Create computed string field (readOnly: true + x-formula)
numFormula(expr) Create computed number field (readOnly: true + x-formula)
boolFormula(expr) Create computed boolean field (readOnly: true + x-formula)
obj(properties) Create object schema (generic — preserves property types)
arr(items) Create array schema (generic — preserves items type)
ref(tableName) Create $ref schema

Table & Row

Function Description
createRowModel(options) Create a row model (typed overload when schema is typed)
createTableModel(options) Create a table model (typed overload when schema is typed)

RowModel

Property / Method Description
root Typed root node (InferNode<S> for typed, ValueNode for untyped)
get(path) Get node at path
getValue(path) Get plain value at path
setValue(path, value) Set value at path
getPlainValue() Get full plain value
patches JSON Patch operations (JsonValuePatch[]) for current changes
reset(data?) Reset to given data (or schema defaults) and commit
commit() Commit current state as base
revert() Revert to last committed state

ArrayValueNode

Method Description
at(index) Get element at index (supports negative)
find(predicate) Find first element matching predicate, or undefined
findIndex(predicate) Find index of first matching element, or -1
push(node) Append element
pushValue(value?) Create and append element from value
removeAt(index) Remove element at index
move(from, to) Move element between positions
clear() Remove all elements

Value Tree

Function Description
createTypedTree(schema, data) Create a typed value tree with path-based access
typedNode(node) Cast an untyped ValueNode to a typed node

Schema

Function Description
createJsonSchemaStore Create runtime schema store
getJsonSchemaStoreByPath Navigate schema by path
applyPatches Apply JSON Patch operations to schema
resolveRefs Resolve $ref to inline schemas
validateJsonFieldName Validate field name format
getInvalidFieldNamesInSchema Find invalid field names in schema

Value

Function Description
createJsonValueStore Create runtime value store
getJsonValueByPath Navigate value by path
computeValueDiff Compute field-level diff between two values
traverseValue Traverse value tree

Foreign Keys

Function Description
getForeignKeysFromSchema Extract foreign keys from schema
getForeignKeysFromValue Extract foreign key values from data
getForeignKeyPatchesFromSchema Get patches for foreign key changes
replaceForeignKeyValue Replace foreign key references

Path Utils

Function Description
parsePath Parse dot-notation path to segments
getParentForPath Get parent path
getPathByStore Get path from store
deepEqual Deep equality comparison

Type Utilities

Type Description
InferValue<S> Schema → plain TypeScript value type
InferNode<S> Schema → typed ValueNode interface
SchemaFromValue<T> Plain TS type → virtual schema shape
SchemaPaths<S> Union of all valid dot-separated paths
TypedRowModel<S> RowModel with typed root, getValue, setValue, getPlainValue, reset
TypedTableModel<S> TableModel with typed rows, addRow, getRow

See Typed API documentation for the full reference.

License

MIT

About

JSON Schema engine — validation, diff, patch, migrations

Topics

Resources

License

Stars

Watchers

Forks

Contributors