@ruleflow-ts/dsl-core
v0.3.0
Published
Ruleflow DSL interpreter in TypeScript (isomorphic, SSR+Edge compatible).
Downloads
37
Maintainers
Readme
@ruleflow-ts/dsl-core
TypeScript interpreter for the Ruleflow DSL. It runs in isomorphic environments (Node, SSR, and potentially Edge) and exposes a simple API to parse and evaluate rule workflows defined in a DSL.
- Node >= 18
- Based on ANTLR4TS for the grammar
- CommonJS output + type definitions (
.d.ts)
Features
- Arithmetic and comparison expressions:
+ - * / %,< <= > >= == <> =(case-insensitive) - Logical operators:
and,or,not - Lists:
contains,in,starts_withwith literal lists, stored listslist('key'), or properties - Aggregations:
count(),average(),any{...},all{...},none{...},distinct() - Dates:
dateDiff,day_of_week,now(),date_add,date_subtract,date(...),datetime(...) - Regex utility:
regex_strip(prop, 're') - Unary:
abs(expr) - Geo:
geohash_encode,geohash_decode,distance,within_radius - Actions:
action('name', { 'k': 'v' })with params using properties ({'id': user.id}) - Evaluation mode:
evaluation_mode multi_match(or defaultsingle_match)
Install & Build
This package is part of the ts-ruleflow workspace.
# from ts-ruleflow/
npm install
npm run typecheck
npm run buildAvailable scripts in packages/dsl-core/package.json:
generate: generates the parser with antlr4ts fromsrc/grammar/RuleFlowLanguage.g4typecheck: TypeScript type-check without emitting filesbuild: compiles intodist/clean: removesdist/andsrc/generated/
Note: the ANTLR generated code is included in the repo for easier builds. If you change the grammar, run
npm run generatebefore building.
Basic Usage
import { Workflow } from '@ruleflow-ts/dsl-core';
const dsl = `workflow 'w'
ruleset 'eligibility'
'adult_us' user.age > 17 and country = 'US' return 'allow' action('notify', {'id': user.id})
default 'deny'
end`;
const data = { user: { id: '42', age: 20 }, country: 'US' };
const lists = {}; // optional
const wf = new Workflow(dsl);
const result = wf.evaluate(data, lists);
console.log(result);
// {
// workflow: 'w',
// ruleSet: 'eligibility',
// rule: 'adult_us',
// result: 'allow',
// actions: [{ name: 'notify', params: { id: '42' } }],
// warnings: [],
// error: false
// }Main API
new Workflow(dsl: string)evaluate(request: InputMap, lists?: ListsMap): WorkflowResultvalidateAndGetWorkflowName(): string
Relevant types (see src/types.ts):
InputMap = Record<string, unknown>ListsMap = Record<string, unknown[]>WorkflowResultincludesworkflow,ruleSet,rule,result,actions,warnings,matchedRules?,error?
DSL Examples
- Aggregations:
items.count() = 3
items.any{ type = 'a' } = true
items.none{ 'blocked' } = true
items.distinct()- Dates:
dateDiff('2025-01-01T00:00:00Z', '2025-01-02T00:00:00Z', day) = 1
day_of_week('2025-09-14T05:00:00Z') = 'SUNDAY'
date_add('2025-01-01T00:00:00Z', 1, day) > '2025-01-01T00:00:00Z'
date_subtract('2025-01-01T12:00:00Z', 1, hour) < '2025-01-01T12:00:00Z'
datetime(now()) > '2000-01-01T00:00:00Z'- Geo:
distance(37.7749, -122.4194, 34.0522, -118.2437) > 500
within_radius(37.7749, -122.4194, 37.7750, -122.4195, 1) = true
# with geohash
distance(geohash_encode(37.7749, -122.4194, 7), geohash_encode(37.7750, -122.4195, 7)) < 0.5- Multi-match mode:
workflow 'w'
evaluation_mode multi_match
ruleset 'rs'
'r1' 1 = 1 return 'A'
'r2' 2 = 2 return 'B'
default 'ko'
endIn this case evaluate() returns matchedRules with all matching rules, and result is the value of the first match.
Next.js Integration (App Router) and Serverless
Recommended patterns for using @ruleflow-ts/dsl-core directly from Next.js pages, without an API Route, plus considerations for Serverless and Edge environments.
Requirements: Next.js 14+ (App Router) and Node.js 18+. If your deployment requires Edge Runtime, review the "Edge Runtime" section below.
1) Server Actions (recommended)
Use a Server Action to evaluate the DSL on the server, invoked from a client component. It avoids exposing the DSL over the network and keeps the client bundle light.
app/actions/evaluate.ts:
'use server';
import { Workflow } from '@ruleflow-ts/dsl-core';
export async function evaluateDsl(input: { dsl: string; data?: any; lists?: any }) {
const { dsl, data = {}, lists = {} } = input || {};
const wf = new Workflow(dsl);
return wf.evaluate(data, lists);
}app/page.tsx (client):
'use client';
import { useState } from 'react';
import { evaluateDsl } from './actions/evaluate';
export const runtime = 'nodejs'; // ensure Node.js runtime if your package is CJS
export default function Page() {
const [dsl, setDsl] = useState("workflow 'w'\n ruleset 'rs'\n 'r1' 1 = 1 return 'ok'\n default 'ko'\nend");
const [dataStr, setDataStr] = useState('{}');
const [listsStr, setListsStr] = useState('{}');
const [out, setOut] = useState('');
async function onSend() {
const data = dataStr ? JSON.parse(dataStr) : {};
const lists = listsStr ? JSON.parse(listsStr) : {};
const res = await evaluateDsl({ dsl, data, lists });
setOut(JSON.stringify(res, null, 2));
}
return (
<div>
{/* textareas for dsl/data/lists */}
<button onClick={onSend}>Send</button>
<pre>{out}</pre>
</div>
);
}Notes:
- If you use CommonJS in the core, set
export const runtime = 'nodejs'in the current segment to avoid Edge runtime. - For user-provided inputs, avoid caching with
export const dynamic = 'force-dynamic'orunstable_noStore().
2) Server Component (SSR during render)
Evaluate during server render without a Server Action. Useful if input comes from searchParams or server data.
import { Workflow } from '@ruleflow-ts/dsl-core';
import { unstable_noStore as noStore } from 'next/cache';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }: { searchParams: { dsl?: string } }) {
noStore();
const dsl = searchParams.dsl ?? "workflow 'w'\n ruleset 'rs'\n 'r1' 1 = 1 return 'ok'\n default 'ko'\nend";
const wf = new Workflow(dsl);
const result = wf.evaluate({}, {});
return <pre>{JSON.stringify(result, null, 2)}</pre>;
}3) Client Component (evaluate in the browser)
Import the core in the client and evaluate locally. Good for offline demos; consider bundle size.
'use client';
import { useState } from 'react';
export default function Page() {
const [dsl, setDsl] = useState("workflow 'w'\n ruleset 'rs'\n 'r1' 1 = 1 return 'ok'\n default 'ko'\nend");
const [out, setOut] = useState('');
async function onSend() {
// Deferred loading to reduce TTI
const { Workflow } = await import('@ruleflow-ts/dsl-core');
const wf = new Workflow(dsl);
const res = wf.evaluate({}, {});
setOut(JSON.stringify(res, null, 2));
}
return <button onClick={onSend}>Send</button>;
}Considerations:
- The bundle will include the parser; if size matters, prefer Server Actions.
4) Alternative with API Route (if you prefer an endpoint)
Expose a POST /api/evaluate endpoint and consume it from the UI:
// app/api/evaluate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Workflow } from '@ruleflow-ts/dsl-core';
export const runtime = 'nodejs';
export async function POST(req: NextRequest) {
const { dsl, data = {}, lists = {} } = await req.json();
if (typeof dsl !== 'string' || !dsl.trim()) {
return NextResponse.json({ message: 'Field dsl is required' }, { status: 400 });
}
const wf = new Workflow(dsl);
const result = wf.evaluate(data, lists);
return NextResponse.json(result, { status: 200 });
}Serverless and Edge Runtime
- Node.js Serverless (Vercel/Netlify): works out-of-the-box with
runtime = 'nodejs'. - Edge Runtime: requires ESM-only dependencies. If your build is CommonJS, use
runtime = 'nodejs'or publish a dual package (ESM + CJS). Edge example (if the core is ESM-compatible):
export const runtime = 'edge';
import { Workflow } from '@ruleflow-ts/dsl-core';Recommendations:
- Security: validate DSL and limit input size/time if accepting user-provided DSL.
- Caching: for dynamic inputs use
noStore()/dynamic = 'force-dynamic'. - Sizes: in the client use dynamic
import()to reduce initial cost.
Types and Result
new Workflow(dsl)evaluate(data?: InputMap, lists?: ListsMap): WorkflowResultWorkflowResultmay includeresult,warnings,actions, andmatchedRules(inevaluation_mode multi_match).
Development
- Grammar changed:
npm run generate - Typecheck:
npm run typecheck - Build:
npm run build - Quick smoke test:
node scripts/smoke.jsLicense
MIT
