flex-rules-engine
v1.0.23
Published
A flexible rules engine for TypeScript/JavaScript applications
Downloads
223
Maintainers
Readme
Flex Rules Engine
A flexible, powerful rules engine for TypeScript/JavaScript applications with built-in NestJS integration and MongoDB support.
Features
- Flexible Rule Definition: Define complex business rules with nested conditions and multiple actions
- Priority-Based Execution: Control rule execution order with priority values
- Multiple Storage Options: MongoDB, in-memory, or custom storage adapters
- NestJS Integration: First-class support for NestJS with decorators and dependency injection
- REST API: Built-in CRUD endpoints with Swagger documentation
- Rule Versioning: Track and revert rule changes
- Bulk Operations: Create and update multiple rules at once
- Type Safety: Full TypeScript support with comprehensive type definitions
- Direct Service Integration: Inject the rules engine directly into your services for high-performance evaluation
Installation
npm install flex-rules-enginePeer Dependencies
For NestJS integration with MongoDB:
npm install @nestjs/common @nestjs/core @nestjs/mongoose mongooseQuick Start
NestJS Integration with MongoDB
// app.module.ts
import { Module } from '@nestjs/common';
import { RulesModule } from 'flex-rules-engine';
@Module({
imports: [
// Option 1: Use your existing MongoDB connection
MongooseModule.forRoot('mongodb://localhost:27017/myapp'),
RulesModule.forMongoDB({
enableController: true, // Enables REST API endpoints
global: true, // Makes the engine available globally
}),
// Option 2: Create a dedicated connection for rules
RulesModule.forRootMongoDB({
mongoUri: 'mongodb://localhost:27017',
dbName: 'rulesdb',
enableController: true,
global: true,
}),
],
})
export class AppModule {}Auto-Loading Rules on Startup
// app.module.ts
import { Module, OnModuleInit, Inject, Logger } from '@nestjs/common';
import { FlexRulesEngine, RulesModule } from 'flex-rules-engine';
@Module({
imports: [
RulesModule.forRootMongoDB({
mongoUri: 'mongodb://localhost:27017',
dbName: 'rulesdb',
enableController: true,
global: true,
}),
],
})
export class AppModule implements OnModuleInit {
private readonly logger = new Logger(AppModule.name);
constructor(
@Inject('FLEX_RULES_ENGINE')
private readonly rulesEngine: FlexRulesEngine,
) {}
async onModuleInit() {
try {
this.logger.log('Loading rules from database...');
await this.rulesEngine.loadRules();
const loadedRules = this.rulesEngine.getRules();
this.logger.log(`Rules Engine initialized with ${loadedRules.length} active rules`);
if (loadedRules.length > 0) {
loadedRules.forEach(rule => {
this.logger.debug(`Loaded rule: ${rule.name} (priority: ${rule.priority})`);
});
} else {
this.logger.warn('No active rules found in database');
}
} catch (error) {
this.logger.error('Failed to load rules', error);
}
}
}Usage
Direct Service Integration (Recommended)
Inject the rules engine directly into your services for maximum performance:
// your.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { FlexRulesEngine } from 'flex-rules-engine';
@Injectable()
export class YourService {
constructor(
@Inject('FLEX_RULES_ENGINE')
private readonly rulesEngine: FlexRulesEngine,
) {}
async processOrder(orderData: any) {
// Evaluate rules against your data
const result = this.rulesEngine.evaluate({
data: {
orderType: 'B2B',
orderAmount: 5000,
customerTier: 'Gold',
...orderData,
}
});
if (result.success) {
console.log('Applied rules:', result.appliedRules);
console.log('Computed values:', result.results);
// Use the computed values
const discount = result.results.discount || 0;
const priority = result.results.priority || 'normal';
return {
...orderData,
discount,
priority,
totalAmount: orderData.amount - discount,
};
} else {
console.error('Rule evaluation errors:', result.errors);
throw new Error('Failed to process order rules');
}
}
async evaluateSpecificRules(data: any, ruleIds: string[]) {
// Evaluate only specific rules by ID
const result = this.rulesEngine.evaluate(
{ data },
ruleIds
);
return result;
}
async getAvailableRules() {
// Get all loaded rules
const rules = this.rulesEngine.getRules();
return rules.map(r => ({
id: r._id,
name: r.name,
priority: r.priority,
active: r.active,
}));
}
}Creating Rules
Via REST API
POST /api/rules
Content-Type: application/json
{
"name": "B2B Gold Customer Discount",
"description": "Apply 10% discount for B2B Gold customers on orders over $1000",
"priority": 100,
"active": true,
"conditions": {
"operator": "AND",
"rules": [
{
"field": "orderType",
"operator": "equals",
"value": "B2B"
},
{
"field": "customerTier",
"operator": "equals",
"value": "Gold"
},
{
"field": "orderAmount",
"operator": "greaterThan",
"value": 1000
}
]
},
"actions": [
{
"type": "setValue",
"target": "discount",
"value": 0.10
},
{
"type": "setValue",
"target": "priority",
"value": "high"
},
{
"type": "calculate",
"target": "finalAmount",
"formula": "orderAmount * (1 - discount)"
}
]
}Programmatically
import { Injectable, Inject } from '@nestjs/common';
import { IRuleManager } from 'flex-rules-engine';
@Injectable()
export class RulesSetupService {
constructor(
@Inject('RULE_MANAGER')
private readonly ruleManager: IRuleManager,
@Inject('FLEX_RULES_ENGINE')
private readonly rulesEngine: FlexRulesEngine,
) {}
async setupInitialRules() {
const rule = await this.ruleManager.createRule({
name: 'B2B Gold Customer Discount',
description: 'Apply 10% discount for B2B Gold customers',
priority: 100,
active: true,
conditions: {
operator: 'AND',
rules: [
{ field: 'orderType', operator: 'equals', value: 'B2B' },
{ field: 'customerTier', operator: 'equals', value: 'Gold' },
{ field: 'orderAmount', operator: 'greaterThan', value: 1000 }
]
},
actions: [
{ type: 'setValue', target: 'discount', value: 0.10 },
{ type: 'setValue', target: 'priority', value: 'high' }
]
});
// Reload the engine to include the new rule
await this.rulesEngine.reloadRules();
return rule;
}
}Rule Definition
Rule Structure
interface RuleDefinition {
_id?: string; // Auto-generated MongoDB ID
name: string; // Human-readable name
description?: string; // Optional description
priority: number; // Execution order (higher = earlier)
active: boolean; // Enable/disable rule
conditions: ConditionGroup; // When to apply this rule
actions: Action[]; // What to do when conditions match
metadata?: Record<string, any>; // Optional custom metadata
version?: number; // Auto-incremented version
createdAt?: Date;
updatedAt?: Date;
}Condition Operators
Logical Operators
AND- All conditions must be trueOR- At least one condition must be true
Comparison Operators
Note: Both camelCase and snake_case naming conventions are supported.
Equality Operators:
equals- Exact matchnotEquals/not_equals- Not equal
Numeric Comparison:
greaterThan/greater_than- Greater thanlessThan/less_than- Less thangreaterThanOrEqual/greater_than_or_equal- Greater than or equallessThanOrEqual/less_than_or_equal- Less than or equal
String Operators:
contains- String/array contains valuestartsWith/starts_with- String starts with valueendsWith/ends_with- String ends with valueregex- Regex pattern match
Array Operators:
in- Value exists in arraynotIn/not_in- Value does not exist in array
Operator Examples
// Equality
{ field: 'status', operator: 'equals', value: 'active' }
{ field: 'status', operator: 'notEquals', value: 'deleted' }
// Numeric comparison
{ field: 'age', operator: 'greaterThan', value: 18 }
{ field: 'price', operator: 'lessThanOrEqual', value: 100 }
// String operations
{ field: 'email', operator: 'contains', value: '@company.com' }
{ field: 'name', operator: 'startsWith', value: 'John' }
{ field: 'filename', operator: 'endsWith', value: '.pdf' }
{ field: 'email', operator: 'regex', value: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$' }
// Array operations
{ field: 'role', operator: 'in', value: ['admin', 'moderator'] }
{ field: 'status', operator: 'notIn', value: ['deleted', 'archived'] }
// Using snake_case (also works)
{ field: 'age', operator: 'greater_than', value: 18 }
{ field: 'email', operator: 'starts_with', value: 'admin' }Action Types
setValue
Set a value in the results
{
type: 'setValue',
target: 'discount',
value: 0.10
}calculate
Perform mathematical calculations
{
type: 'calculate',
target: 'finalAmount',
formula: 'orderAmount * (1 - discount)'
}Supported operators: +, -, *, /, %, ^ (power)
transform
Transform a value using functions
{
type: 'transform',
target: 'email',
field: 'rawEmail',
function: 'toLowerCase'
}Available functions: toUpperCase, toLowerCase, trim, round, floor, ceil, abs
merge
Merge objects or arrays
{
type: 'merge',
target: 'combinedData',
sources: ['source1', 'source2']
}REST API Endpoints
When enableController: true, the following endpoints are automatically available:
Rules Management
POST /api/rules- Create a ruleGET /api/rules- Get all rules (with filtering)GET /api/rules/:id- Get rule by IDPUT /api/rules/:id- Update a ruleDELETE /api/rules/:id- Delete a rulePOST /api/rules/:id/test- Test a rule with sample dataPOST /api/rules/:id/validate- Validate rule structureGET /api/rules/:id/versions- Get rule version historyPOST /api/rules/:id/revert/:version- Revert to specific version
Bulk Operations
POST /api/rules/bulk/create- Create multiple rulesPUT /api/rules/bulk/update- Update multiple rules
Engine Operations
GET /api/rules/engine/status- Get engine statusPOST /api/rules/engine/reload- Reload rules from databasePOST /api/rules/engine/evaluate- Evaluate rules against data
Query Parameters
GET /api/rules?active=true&priority=100&search=discount&sortBy=priority&sortOrder=descAPI Response Format
All endpoints return responses in this format:
{
"result": {
// Single item or object
}
}
// OR for multiple items
{
"results": [
// Array of items
]
}Configuration Options
RulesModule.forMongoDB(options)
Uses an existing MongoDB connection:
{
connectionString?: string; // Optional: Override connection
collectionName?: string; // Optional: Custom collection name (default: 'rules')
enableController?: boolean; // Enable REST API (default: true)
controllerPath?: string; // Custom API path
global?: boolean; // Make module global (default: false)
}RulesModule.forRootMongoDB(options)
Creates its own MongoDB connection:
{
mongoUri: string; // MongoDB connection URI
dbName?: string; // Database name
collectionName?: string; // Collection name (default: 'rules')
enableController?: boolean; // Enable REST API (default: true)
controllerPath?: string; // Custom API path
global?: boolean; // Make module global (default: false)
mongoOptions?: Record<string, any>; // Additional Mongoose options
}RulesModule.forMemory(options)
In-memory storage for development/testing:
{
enableController?: boolean; // Enable REST API (default: true)
controllerPath?: string; // Custom API path
global?: boolean; // Make module global (default: false)
}Advanced Usage
Custom Rule Manager
import { IRuleManager } from 'flex-rules-engine';
class CustomRuleManager implements IRuleManager {
// Implement your custom storage logic
}
@Module({
imports: [
RulesModule.forRoot({
ruleManager: new CustomRuleManager(),
enableController: true,
}),
],
})
export class AppModule {}Rule Evaluation Result
interface RuleExecutionResult {
success: boolean; // True if no errors occurred
appliedRules: string[]; // IDs of rules that matched
results: Record<string, any>; // Computed values from actions
errors?: Array<{ // Any errors that occurred
ruleId: string;
message: string;
action?: string;
}>;
executionTime: number; // Execution time in milliseconds
}Examples
E-commerce Pricing
// Apply tiered discounts based on order value
{
name: "Volume Discount",
priority: 100,
conditions: {
operator: "AND",
rules: [
{ field: "orderAmount", operator: "greaterThan", value: 10000 }
]
},
actions: [
{ type: "setValue", target: "discount", value: 0.15 },
{ type: "calculate", target: "finalAmount", formula: "orderAmount * 0.85" }
]
}User Permissions
// Grant admin access for specific roles
{
name: "Admin Access",
priority: 200,
conditions: {
operator: "OR",
rules: [
{ field: "role", operator: "equals", value: "admin" },
{ field: "role", operator: "equals", value: "superuser" }
]
},
actions: [
{ type: "setValue", target: "canAccessAdmin", value: true },
{ type: "setValue", target: "permissions", value: ["read", "write", "delete"] }
]
}License
MIT
Author
yujuism
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
