JSPM

@shoryshift/casecade

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 14
  • Score
    100M100P100Q37969F
  • License MIT

Declarative, entity-agnostic state-machine + rules engine for TypeScript

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 handler

Why?

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 recursiontransition_dimension actions 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 helpercreateInMemoryAdapters() 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/casecade

Documentation

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.

License

MIT