@edgerules/node
EdgeRules decision service for Node.js. Ships two builds:
| Export | Description |
|---|---|
@edgerules/node | Production build — execution only (DecisionService) |
@edgerules/node/mutable | Development build — execution + CRUD (MutableDecisionService) |
Installation
npm install @edgerules/node @edgerules/portable
Requires Node.js 18 or later (ESM only).
Production Usage
Load from an EdgeRules DSL string
import { DecisionService } from '@edgerules/node';
const service = new DecisionService(`{
applicant: {
age: <number, required: true>
income: <number, default: 0>
}
isEligible: applicant.age >= 18 and applicant.income >= 1000
}`);
// Execute the whole model — inputs are bound via a model-shaped request object
const result = service.execute('*', { applicant: { age: 25, income: 2500 } });
// { applicant: { age: 25, income: 2500 }, isEligible: true }
Load from a Portable JSON model
import { DecisionService } from '@edgerules/node';
import type { PortableContext } from '@edgerules/portable';
const model: PortableContext = {
'@kind': 'context',
fields: {
taxRate: 0.21,
calculateTax: {
'@kind': 'method',
parameters: { amount: 'number', rate: 'number' },
body: { '@kind': 'context', fields: { result: 'amount * rate' } },
},
},
};
const service = new DecisionService(model);
// Execute a specific function — pass its arguments as request bindings
const tax = service.execute('calculateTax', { amount: 100, rate: 0.21 });
// { result: 21 }
Executing a ruleset
const service = new DecisionService(`{
ruleset risk(age: number, income: number): {
hitPolicy: "first-match"
rules: [
{ when: { age: 18..25, income: < 30000 }, then: { level: "high", limit: 1000 } }
{ when: { age: 18..25, income: >= 30000 }, then: { level: "medium", limit: 5000 } }
{ when: { age: > 25 }, then: { level: "low", limit: 20000 } }
]
default: { level: "none", limit: 0 }
}
decision: risk(age: applicant.age, income: applicant.income)
}`);
const result = service.execute('decision', { applicant: { age: 23, income: 25000 } });
// { level: "high", limit: 1000 }
Error handling
Every execution or model error is a structured PortableError — never a plain string.
import { DecisionService } from '@edgerules/node';
import type { PortableError } from '@edgerules/portable';
try {
service.execute('unknownField');
} catch (err) {
const error = err as PortableError;
console.error(error.type); // "EntryNotFound"
console.error(error.message); // human-readable, safe to surface
console.error(error.path); // "unknownField"
}
error.type | Meaning |
|---|---|
EntryNotFound | Path does not exist in the model |
WrongFieldPath | Invalid path, out-of-bounds array index |
Parse | Expression string failed to parse |
Linking | Type mismatch or broken reference |
Execution | Runtime error during evaluation |
Mutable / Development Usage
Use the ./mutable subpath import to access MutableDecisionService, which adds CRUD operations on top of execution.
This build is intended for IDEs, rule editors, and test tooling — not for production traffic.
import { MutableDecisionService } from '@edgerules/node/mutable';
const service = new MutableDecisionService(`{
taxRate: 0.21
tax: amount * taxRate
}`);
// Read a node enriched with linked type information
const node = service.get('taxRate');
// { "@kind": "expression", "expression": "0.21", "type": "number", "linked": true }
// Update a field at runtime — the engine relinks lazily on the next execute
service.set('taxRate', 0.25);
// Read the entire model snapshot
const model = service.get('*');
// Add a new typed input
service.set('amount', { '@kind': 'placeholder', type: 'number', required: true });
// Remove a field
service.remove('taxRate');
// Rename a field
service.rename('tax', 'vat');
// Read a node as DSL source text instead of Portable JSON
service.getCode('tax');
// 'amount * taxRate'
// Write DSL source text back — returns a PortableNode, not code text
service.setCode('tax', 'amount * taxRate * 2');
get kinds
import { Kinds } from '@edgerules/node';
// Absent kinds: every declaration, with bodies
service.get('isEligible');
// The method signature only
service.get('isEligible', ['method-interface']);
// The type only, without its facts
service.get('Customer', ['type']);
// A project-explorer view without bodies and expression text
service.get('*', [...Kinds.EXPRESSION_INTERFACES, ...Kinds.TYPES, ...Kinds.INVOCABLE_INTERFACES]);
API Reference
DecisionService
| Method | Description |
|---|---|
new DecisionService(model) | Create from DSL string or a root PortableContext |
execute(method, args?) | Evaluate method (field path, function, or "*") against optional request bindings |
MutableDecisionService (dev build)
Inherits execute and adds:
| Method | Description |
|---|---|
get(path, kinds?) | Read a node, filtered by @kind; linked tells if the model links |
set(path, value) | Write an expression, placeholder, method, type, or context |
getCode(path) | Read a node as EdgeRules DSL source text |
setCode(path, code) | Write a node from EdgeRules DSL source text; returns a PortableNode, not code text |
remove(path) | Delete an entry |
rename(oldPath, newPath) | Rename an entry within its context |
Related packages
@edgerules/web— same API for the browser@edgerules/portable— Portable JSON type definitions