nx-rules
v1.0.2
Published
A semi-strict, production-grade rules runtime for Node.js/TypeScript with internal registries, TTL state, and optional local LLM conditions
Maintainers
Readme
nx-rules
A semi-strict, production-grade rules engine for Node.js and TypeScript. Build complex decision logic with composable conditions, actions, and enrichments.
Features
- 🎯 Declarative Rule Definitions - Define rules as JSON/TypeScript objects
- 🔧 Function & API Registries - Register custom functions and API integrations
- 🎨 Rich Expression Language - Boolean logic, comparisons, path references, and more
- 📊 Flexible Actions - Enrich, aggregate, detect patterns, and write to output
- 🧠 Optional LLM Support - Built-in local LLM conditions for classification and extraction
- 💾 TTL State Management - In-memory state store with automatic cleanup
- 🔍 Comprehensive Tracing - Track rule execution with detailed trace events
- 📦 TypeScript First - Full type safety with excellent IDE support
Installation
npm install nx-rulesPeer Dependencies
nx-rules uses external registries for functions and APIs:
npm install nx-functions x-api-registererThese are automatically installed as dependencies.
Quick Start
import {
createEngine,
createFunctionRegistry,
createApiRegistry
} from 'nx-rules';
// 1. Create registries
const fn = createFunctionRegistry();
const api = createApiRegistry();
// 2. Register custom functions
fn.register('is_premium_user', async (ctx) => {
return ctx.input.tier === 'premium';
});
// 3. Register API integrations
api.register('fetch_user_profile', async (ctx) => {
const userId = ctx.input.userId;
// Your API logic here
return {
ok: true,
data: { name: 'John Doe', credits: 100 }
};
});
// 4. Create the engine
const engine = createEngine({
functionRegistry: fn,
apiRegistry: api,
config: {
matchPolicy: 'ALL_MATCH',
conflictPolicy: 'MERGE',
trace: true
}
});
// 5. Define rules
const rule = {
ruleId: 'premium-user-bonus',
ruleName: 'Award bonus to premium users',
route: {
eq: [{ path: 'input.action' }, { const: 'login' }]
},
conditions: [
{ expr: { callFn: ['is_premium_user'] } }
],
actions: {
onMatch: [
{
type: 'enrich',
impl: { kind: 'api', id: 'fetch_user_profile' },
write: { targetPath: 'output.profile', mode: 'replace' }
}
]
}
};
// 6. Add rule to engine
const ruleSet = engine.getRuleSet();
ruleSet.addRule(rule);
// 7. Evaluate
const result = await engine.evaluate({
input: { userId: 'u123', action: 'login', tier: 'premium' }
});
console.log(result.output); // { profile: { name: 'John Doe', credits: 100 } }Core Concepts
1. Rules
A rule consists of three main parts:
- Route: Determines if the rule should be evaluated (fast pre-filter)
- Conditions: Boolean expressions that must pass for the rule to match
- Actions: Operations to perform when the rule matches (or doesn't match)
interface XRule {
ruleId: string;
ruleName?: string;
priority?: number;
enabled?: boolean;
route?: XExpr;
conditions?: XCondition[];
actions?: {
onMatch?: XAction[];
onNoMatch?: XAction[];
};
params?: Record<string, unknown>;
}2. Expression Language
The expression language supports:
Boolean Operators
{ and: [expr1, expr2, ...] }
{ or: [expr1, expr2, ...] }
{ not: expr }Comparisons
{ eq: [left, right] } // Equal
{ neq: [left, right] } // Not equal
{ gt: [left, right] } // Greater than
{ gte: [left, right] } // Greater than or equal
{ lt: [left, right] } // Less than
{ lte: [left, right] } // Less than or equal
{ in: [value, array] } // Value in array
{ exists: path } // Path exists
{ regex: [value, pattern] } // Regex matchValue References
{ path: 'input.userId' } // Access input data
{ param: 'threshold' } // Access rule parameters
{ const: 'some-value' } // Constant value
{ baggage: 'previousResult' } // Access baggage from previous rulesFunction Calls
{ callFn: ['functionName', arg1, arg2, ...] }3. Actions
Actions are executed when a rule matches (or doesn't match):
Enrich Action
Call an API and write the result to output:
{
type: 'enrich',
impl: { kind: 'api', id: 'get_user_data' },
write: { targetPath: 'output.userData', mode: 'replace' }
}Detect Action
Pattern detection with windowing:
{
type: 'detect',
impl: {
kind: 'window',
id: 'login_attempts',
window: { size: 5, unit: 'minutes' },
threshold: 3
},
write: { targetPath: 'output.suspicious', mode: 'replace' }
}Aggregate Action
Aggregate values over time:
{
type: 'aggregate',
impl: {
kind: 'sum',
id: 'total_spent',
ttl: '1d'
},
write: { targetPath: 'output.totalSpent', mode: 'replace' }
}4. Function Registry
Register custom functions that can be called from conditions:
import { createFunctionRegistry } from 'nx-rules';
const fn = createFunctionRegistry();
// Simple boolean function
fn.register('is_valid_email', async (ctx) => {
const email = ctx.input.email;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
});
// Function with baggage (additional data)
fn.register('calculate_risk_score', async (ctx) => {
const score = ctx.input.amount * ctx.params.multiplier;
return {
passed: score > 100,
baggage: { riskScore: score }
};
});Function Context:
interface XFnContext {
input: Record<string, unknown>; // Input data
content?: string; // Optional content string
facts?: Record<string, unknown>; // Facts about the evaluation
params?: Record<string, unknown>; // Rule parameters
baggage?: Record<string, unknown>; // Accumulated baggage
args: unknown[]; // Function arguments
now: () => number; // Current timestamp
state?: StateStore; // State store
llm?: LLMProvider; // LLM provider
trace?: (event: any) => void; // Trace function
}5. API Registry
Register API integrations for enrichment:
import { createApiRegistry } from 'nx-rules';
const api = createApiRegistry();
// Successful API call
api.register('get_user_profile', async (ctx) => {
const userId = ctx.input.userId;
const profile = await fetchUserProfile(userId);
return {
ok: true,
data: profile
};
});
// API call with error handling
api.register('charge_payment', async (ctx) => {
try {
const result = await processPayment(ctx.input.amount);
return { ok: true, data: result };
} catch (error) {
return {
ok: false,
error: {
code: 'PAYMENT_FAILED',
message: error.message
}
};
}
});API Result:
type XApiResult =
| { ok: true; data: any; meta?: Record<string, unknown> }
| { ok: false; error: { code: string; message: string; details?: any } };Advanced Features
State Management
Use the built-in state store for TTL-based memory:
import { createEngine, createStateStore } from 'nx-rules';
const state = createStateStore({
maxEntries: 100000,
cleanupIntervalMs: 60000
});
const engine = createEngine({
functionRegistry: fn,
apiRegistry: api,
stateStore: state
});
// In your function
fn.register('track_login', async (ctx) => {
const userId = ctx.input.userId;
const key = `login:${userId}`;
// Remember this login for 1 hour
await ctx.state.remember(key, { timestamp: Date.now() }, '1h');
// Recall previous logins
const history = await ctx.state.recall({ key });
return history.length > 5; // Flag if more than 5 logins in 1 hour
});LLM Integration
Use local LLM models for classification and extraction:
import {
createEngine,
createLLMProvider,
createOllamaAdapter,
registerLLMFunctions
} from 'nx-rules';
// Create LLM provider with Ollama backend
const adapter = createOllamaAdapter('http://localhost:11434');
const llm = createLLMProvider({
models: {
llama2: 'llama2:7b',
tiny: 'phi3:mini'
}
}, adapter);
// Register LLM functions
const fn = createFunctionRegistry();
registerLLMFunctions(fn, llm);
const engine = createEngine({
functionRegistry: fn,
apiRegistry: api,
llmProvider: llm
});
// Use in rules
const rule = {
ruleId: 'classify-content',
conditions: [
{
expr: {
callFn: [
'llm.tiny.classify',
{
scope: 'content',
labels: ['spam', 'legitimate', 'promotional'],
minConfidence: 0.7
}
]
}
}
]
};Templates
Use built-in templates for common patterns:
import { createDefaultTemplateRegistry } from 'nx-rules';
const templates = createDefaultTemplateRegistry();
// Create a regex detection rule from template
const rule = templates.apply('detect.regex.pipeline.v1', {
ruleId: 'detect-email',
route: { entityClass: 'Content', eventClass: 'Scan' },
regex: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}',
findingType: 'EMAIL_ADDRESS',
writeTargetPath: 'output.findings'
});Tracing
Enable detailed execution tracing:
const result = await engine.evaluate({
input: { userId: 'u123' },
config: { trace: true }
});
// Examine trace events
result.trace.forEach(event => {
console.log(event.type, event.ruleId, event.timestamp);
});Trace Event Types:
route_start/route_endcondition_start/condition_endaction_start/action_endwriteerror
Configuration
Engine Configuration
interface XEngineConfig {
matchPolicy?: 'FIRST_MATCH' | 'ALL_MATCH'; // Stop after first match or evaluate all
conflictPolicy?: 'FIRST_WRITE_WINS' | 'LAST_WRITE_WINS' | 'MERGE' | 'ERROR';
defaultPriority?: number;
trace?: boolean;
maxRulesPerEvaluation?: number;
maxActionsPerRule?: number;
}Rule Priority
Rules are evaluated in priority order (higher priority first):
const highPriorityRule = {
ruleId: 'critical-check',
priority: 100,
// ...
};
const normalRule = {
ruleId: 'standard-check',
priority: 0, // default
// ...
};System Functions
Built-in functions available out of the box:
Conditions
always()- Always returns truenever()- Always returns false
Windowing
window.count(id, window, threshold)- Count events in time windowwindow.sum(id, window, threshold, valuePath)- Sum values in windowwindow.avg(id, window, threshold, valuePath)- Average values in window
Memory
memory.exists(key)- Check if key exists in statememory.count(selector)- Count items matching selectormemory.recall(selector)- Recall items from state
Detection Helpers
detect.threshold(value, threshold)- Simple threshold checkdetect.range(value, min, max)- Range checkdetect.pattern(value, pattern)- Regex pattern match
Error Handling
The engine provides detailed error information:
const result = await engine.evaluate({ input: data });
if (result.hasErrors) {
result.errors.forEach(error => {
console.error(`Rule ${error.ruleId}, Step ${error.step}:`, error.error);
});
}Testing
Run the integration test:
npm run test:integrationCreate your own tests:
import { describe, test, expect } from 'vitest';
import { createEngine, createFunctionRegistry, createApiRegistry } from 'nx-rules';
describe('My Rules', () => {
test('should match premium users', async () => {
const fn = createFunctionRegistry();
const api = createApiRegistry();
fn.register('is_premium', async (ctx) => ctx.input.tier === 'premium');
const engine = createEngine({ functionRegistry: fn, apiRegistry: api });
const ruleSet = engine.getRuleSet();
ruleSet.addRule({
ruleId: 'premium-check',
conditions: [{ expr: { callFn: ['is_premium'] } }]
});
const result = await engine.evaluate({
input: { tier: 'premium' }
});
expect(result.matchedRules).toHaveLength(1);
});
});Performance Tips
- Use Routes Effectively: Routes are evaluated before conditions, so use them to quickly filter out irrelevant rules
- Optimize Function Calls: Cache expensive computations in baggage
- Limit State Store Size: Configure
maxEntriesbased on your memory constraints - Use FIRST_MATCH: If you only need one rule to match, use
FIRST_MATCHpolicy - Batch Evaluations: Process multiple inputs in parallel when possible
TypeScript Support
Full TypeScript definitions are included:
import type {
XRule,
XExpr,
XCondition,
XAction,
XEvaluationResult,
XEngineConfig,
FunctionRegistry,
ApiRegistry
} from 'nx-rules';API Reference
Core Exports
createEngine(options)- Create a rules enginecreateFunctionRegistry()- Create a function registrycreateApiRegistry()- Create an API registrycreateStateStore(options?)- Create a state storecreateLLMProvider(config?, adapter?)- Create an LLM providercreateOllamaAdapter(baseUrl?)- Create an Ollama adaptercreateTemplateRegistry()- Create a template registryregisterSystemFunctions(registry)- Register built-in functionsregisterLLMFunctions(registry, provider)- Register LLM functions
Utilities
getPath(obj, path)- Get value at pathsetPath(obj, path, value)- Set value at pathdeepMerge(obj1, obj2)- Deep merge objectsdeepClone(obj)- Deep clone objectparseTtl(ttl)- Parse TTL string to millisecondshashObject(obj)- Hash object to stringgenerateId()- Generate unique ID
Examples
Check out the .tests/ directory for complete examples:
- Basic integration test
- Function and API registration
- Rule evaluation
- State management
- LLM integration
Contributing
Contributions are welcome! Please see the repository for guidelines.
License
MIT
Links
Support
For questions and support, please open an issue on GitHub.
