Package Exports
- @shoryshift/casecade
- @shoryshift/casecade/testing
Readme
casecade
Declarative, entity-agnostic state-machine + rules engine for TypeScript.
You define dimensions, triggers, conditions, and actions; the engine validates transitions, locks the row, writes, audits, and dispatches actions through handlers you register. No framework lock-in. No DB lock-in. Works with anything that can implement four small adapter interfaces.
import { defineEngine, type CaseDefinition } from "@shoryshift/casecade";
import { createInMemoryAdapters } from "@shoryshift/casecade/testing";
const orderLifecycle: CaseDefinition = {
name: "Order",
entity: "Order",
dimensions: {
payment_status: {
label: "Payment", column: "paymentStatus",
values: ["pending", "paid", "refunded"],
transitions: {
_null: ["pending"],
pending: ["paid", "refunded"],
paid: ["refunded"],
refunded: [],
},
permission: "orders:payment",
},
},
rules: [
{
id: "payment-paid-notify",
name: "Notify warehouse on payment",
trigger: { type: "dimension_transition", dimension: "payment_status", to: "paid" },
actions: [{ type: "create_task", title: "Ship order",
category: "fulfillment", priority: "high",
assignTo: { by: "role", role: "warehouse" } }],
},
],
};
const adapters = createInMemoryAdapters({ definition: orderLifecycle });
const engine = defineEngine({ ...adapters, handlers: { create_task: myHandler } });
await engine.transition("order-1", "payment_status", "paid", "user-1");
// → payment_status persisted, audit logged, rule fired, task created via your handlerWhy?
Most real apps have 3+ status fields that interact, multiple roles triggering downstream work, and a need for a paper trail of why state changed. Without a rules engine, that logic lives in branchy procedural code at every mutation site. With casecade, it lives in one declarative config, validated, locked, and audited from a single path.
What you get
- Pluggable adapters — bring your own persistence (Postgres, SQLite, in-memory), audit destination, RBAC, and config source.
- Receipt-not-throw — failures surface as structured receipts (
{success, error}), never thrown exceptions. Safe for cron jobs and event loops. - Depth-guarded recursion —
transition_dimensionactions cascade safely; circular rules become a clean error, not a stack overflow. - Atomic stage write-back — if you model lifecycle stages, the stage column is updated inside the same transaction as the dimension write.
- Lazy condition evaluation — content/aggregate/milestone queries only fire when a condition of that type is evaluated.
- Extensible mutation registry — register custom data-mutation actions via TypeScript declaration merging.
- First-class testing helper —
createInMemoryAdapters()gives you a full in-memory wiring (audit log, permission modes, stage tracking) so unit tests need zero infrastructure.
Install
pnpm add @shoryshift/casecade
# or
npm install @shoryshift/casecadeDocumentation
The full developer guide is at docs/DEVELOPER-GUIDE.md. It covers:
- Quickstart (10 minutes, runnable)
- Mental model
- The four adapter contracts
- Authoring action handlers
- Six common recipes (cascades, conditional rules, named conditions, item events, scheduled reminders, DB rule overrides)
- Error catalog
- Testing patterns
- Performance notes
- Ten gotchas
Example
examples/order-lifecycle/ is a runnable end-to-end demo: ~250 lines of TypeScript implementing an Order workflow with all four adapters hand-written in memory. It's the shortest realistic answer to "what does a working integration actually look like?" — clone, pnpm install, pnpm --filter casecade-example-order-lifecycle start.
Bring your own Prisma
If you're on Prisma, @shoryshift/casecade-prisma ships a ready-made PersistenceAdapter that handles the row-lock + dimension UPDATE + stage write-back + milestone INSERT. Drops your adapter wiring from ~80 lines to one factory call:
import { defineEngine } from "@shoryshift/casecade";
import { createPrismaPersistenceAdapter } from "@shoryshift/casecade-prisma";
const persistence = createPrismaPersistenceAdapter({
prisma,
caseTable: "orders",
stageColumn: "stage", // optional
milestoneTable: "milestone_achievements", // optional
});
const engine = defineEngine({ config, persistence, audit, context, handlers });You still write AuditAdapter, ContextResolver, and ConfigAdapter yourself — those are domain-specific. See the adapter package README for full options.
Status
Pre-1.0. The API is stable in practice (in production use), but breaking changes may land before a 1.0 tag. Pin a minor version (^0.1.0) in your package.json if you want stability without surprises.