@f-o-t/rules-engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 FOT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,513 @@
1
+ # @f-o-t/rules-engine
2
+
3
+ A fully type-safe, functional, enterprise-grade rules orchestration engine for TypeScript. Built on top of `@f-o-t/condition-evaluator` for condition evaluation.
4
+
5
+ ## Features
6
+
7
+ - **Type-Safe**: Full TypeScript support with autocomplete for consequences
8
+ - **Functional**: Pure functions, immutable data structures, composable APIs
9
+ - **Fluent Builders**: Chainable rule and condition builders
10
+ - **Caching**: Built-in TTL-based caching with configurable eviction
11
+ - **Validation**: Rule schema validation, conflict detection, integrity checks
12
+ - **Versioning**: Track rule changes with rollback support
13
+ - **Simulation**: Test rules without side effects, compare rule sets
14
+ - **Indexing**: Fast rule lookups by field, tag, category, priority
15
+ - **Analysis**: Rule complexity analysis, usage statistics
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ bun add @f-o-t/rules-engine
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```typescript
26
+ import { createEngine, rule, all, num, str } from "@f-o-t/rules-engine";
27
+
28
+ // Create an engine
29
+ const engine = createEngine();
30
+
31
+ // Add rules using the fluent builder
32
+ engine.addRule(
33
+ rule()
34
+ .named("Premium Discount")
35
+ .when(
36
+ all(
37
+ num("orderTotal", "gt", 100),
38
+ str("customerType", "eq", "premium")
39
+ )
40
+ )
41
+ .then("apply_discount", { percentage: 15 })
42
+ .withPriority(100)
43
+ .tagged("pricing", "discount")
44
+ .build()
45
+ );
46
+
47
+ // Evaluate against context
48
+ const result = await engine.evaluate({
49
+ orderTotal: 150,
50
+ customerType: "premium",
51
+ });
52
+
53
+ console.log(result.matchedRules); // Rules that matched
54
+ console.log(result.consequences); // Actions to execute
55
+ ```
56
+
57
+ ## Building Conditions
58
+
59
+ ### Shorthand Helpers
60
+
61
+ ```typescript
62
+ import { num, str, bool, date, arr, all, any } from "@f-o-t/rules-engine";
63
+
64
+ // Number conditions
65
+ num("amount", "gt", 100) // amount > 100
66
+ num("count", "lte", 10) // count <= 10
67
+ num("price", "eq", 50) // price === 50
68
+
69
+ // String conditions
70
+ str("status", "eq", "active")
71
+ str("name", "contains", "test")
72
+ str("email", "ends_with", "@example.com")
73
+ str("role", "in", ["admin", "moderator"])
74
+
75
+ // Boolean conditions
76
+ bool("isActive", "eq", true)
77
+
78
+ // Date conditions
79
+ date("createdAt", "gt", "2024-01-01")
80
+ date("expiresAt", "between", ["2024-01-01", "2024-12-31"])
81
+
82
+ // Array conditions
83
+ arr("tags", "contains", "urgent")
84
+ arr("items", "is_not_empty", undefined)
85
+
86
+ // Combine with AND/OR
87
+ all(num("amount", "gt", 100), str("status", "eq", "approved"))
88
+ any(bool("isVip", "eq", true), num("orders", "gt", 10))
89
+ ```
90
+
91
+ ### Fluent Condition Builder
92
+
93
+ ```typescript
94
+ import { and, or, conditions } from "@f-o-t/rules-engine";
95
+
96
+ // Using and/or with builder function
97
+ const complexCondition = and((c) =>
98
+ c.number("amount", "gt", 100)
99
+ .string("status", "eq", "active")
100
+ .or((nested) =>
101
+ nested.boolean("isVip", "eq", true)
102
+ .number("loyaltyPoints", "gt", 1000)
103
+ )
104
+ );
105
+ ```
106
+
107
+ ## Building Rules
108
+
109
+ ```typescript
110
+ import { rule } from "@f-o-t/rules-engine";
111
+
112
+ const myRule = rule()
113
+ .id("rule-001") // Optional custom ID
114
+ .named("My Rule") // Required name
115
+ .describedAs("Rule description") // Optional description
116
+ .when(conditions) // Required conditions
117
+ .then("action_type", { payload: "data" }) // Required consequence(s)
118
+ .then("another_action", {}) // Multiple consequences
119
+ .withPriority(100) // Higher = evaluated first
120
+ .enabled() // Enabled by default
121
+ .stopOnMatch() // Stop evaluation on match
122
+ .tagged("tag1", "tag2") // Categorization tags
123
+ .inCategory("pricing") // Single category
124
+ .withMetadata({ custom: "data" }) // Custom metadata
125
+ .build();
126
+ ```
127
+
128
+ ## Engine API
129
+
130
+ ### Rule Management
131
+
132
+ ```typescript
133
+ const engine = createEngine();
134
+
135
+ // Add rules
136
+ const addedRule = engine.addRule(ruleInput);
137
+ const addedRules = engine.addRules([rule1, rule2]);
138
+
139
+ // Get rules
140
+ const singleRule = engine.getRule("rule-id");
141
+ const allRules = engine.getRules();
142
+ const filteredRules = engine.getRules({
143
+ enabled: true,
144
+ tags: ["pricing"],
145
+ category: "discounts",
146
+ });
147
+
148
+ // Update rules
149
+ engine.updateRule("rule-id", { priority: 200 });
150
+ engine.enableRule("rule-id");
151
+ engine.disableRule("rule-id");
152
+
153
+ // Remove rules
154
+ engine.removeRule("rule-id");
155
+ engine.clearRules();
156
+ ```
157
+
158
+ ### Evaluation
159
+
160
+ ```typescript
161
+ const result = await engine.evaluate(context, {
162
+ skipDisabled: true, // Skip disabled rules (default: true)
163
+ bypassCache: false, // Bypass cache (default: false)
164
+ maxRules: 100, // Limit rules evaluated
165
+ tags: ["pricing"], // Filter by tags
166
+ category: "discounts", // Filter by category
167
+ ruleSetId: "set-001", // Use specific rule set
168
+ conflictResolution: "all", // "all" | "first-match" | "highest-priority"
169
+ });
170
+
171
+ // Result structure
172
+ result.matchedRules // Rules that matched
173
+ result.consequences // Aggregated consequences
174
+ result.totalRulesEvaluated // Count of rules evaluated
175
+ result.totalRulesMatched // Count of matches
176
+ result.executionTimeMs // Execution time
177
+ result.cacheHit // Whether result was cached
178
+ ```
179
+
180
+ ### Rule Sets
181
+
182
+ ```typescript
183
+ // Group rules into sets
184
+ engine.addRuleSet({
185
+ name: "Holiday Promotions",
186
+ ruleIds: ["rule-1", "rule-2", "rule-3"],
187
+ });
188
+
189
+ // Evaluate only rules in a set
190
+ await engine.evaluate(context, { ruleSetId: "set-id" });
191
+ ```
192
+
193
+ ## Engine Configuration
194
+
195
+ ```typescript
196
+ import { createEngine } from "@f-o-t/rules-engine";
197
+ import { z } from "zod";
198
+
199
+ const engine = createEngine({
200
+ // Type-safe consequence definitions
201
+ consequences: {
202
+ apply_discount: z.object({ percentage: z.number() }),
203
+ send_email: z.object({ template: z.string(), to: z.string() }),
204
+ },
205
+
206
+ // Cache configuration
207
+ cache: {
208
+ enabled: true,
209
+ ttl: 60000, // 1 minute
210
+ maxSize: 1000,
211
+ },
212
+
213
+ // Conflict resolution
214
+ conflictResolution: "all", // "all" | "first-match" | "highest-priority"
215
+
216
+ // Error handling
217
+ continueOnError: true,
218
+
219
+ // Performance monitoring
220
+ slowRuleThresholdMs: 100,
221
+
222
+ // Lifecycle hooks
223
+ hooks: {
224
+ onBeforeEvaluation: async (context, rules) => {},
225
+ onAfterEvaluation: async (result) => {},
226
+ onRuleMatch: async (rule, context) => {},
227
+ onRuleError: async (rule, error) => {},
228
+ onCacheHit: async (key, result) => {},
229
+ },
230
+ });
231
+ ```
232
+
233
+ ## Validation
234
+
235
+ ```typescript
236
+ import {
237
+ validateRule,
238
+ detectConflicts,
239
+ checkIntegrity,
240
+ } from "@f-o-t/rules-engine";
241
+
242
+ // Validate single rule
243
+ const validation = validateRule(rule);
244
+ if (!validation.valid) {
245
+ console.log(validation.errors);
246
+ }
247
+
248
+ // Detect conflicts between rules
249
+ const conflicts = detectConflicts(rules);
250
+ // Types: DUPLICATE_ID, DUPLICATE_CONDITIONS, OVERLAPPING_CONDITIONS,
251
+ // PRIORITY_COLLISION, UNREACHABLE_RULE
252
+
253
+ // Check rule set integrity
254
+ const integrity = checkIntegrity(rules);
255
+ // Checks: negative priority, missing fields, invalid operators
256
+ ```
257
+
258
+ ## Simulation
259
+
260
+ ```typescript
261
+ import { simulate, whatIf, batchSimulate } from "@f-o-t/rules-engine";
262
+
263
+ // Simulate without side effects
264
+ const result = simulate(rules, { data: context });
265
+
266
+ // Compare two rule sets
267
+ const comparison = whatIf(originalRules, modifiedRules, { data: context });
268
+ console.log(comparison.differences.newMatches);
269
+ console.log(comparison.differences.lostMatches);
270
+ console.log(comparison.differences.consequenceChanges);
271
+
272
+ // Test multiple contexts
273
+ const batchResults = batchSimulate(rules, [
274
+ { data: { amount: 50 } },
275
+ { data: { amount: 150 } },
276
+ { data: { amount: 500 } },
277
+ ]);
278
+ ```
279
+
280
+ ## Versioning
281
+
282
+ ```typescript
283
+ import {
284
+ createVersionStore,
285
+ addVersion,
286
+ getHistory,
287
+ rollbackToVersion,
288
+ } from "@f-o-t/rules-engine";
289
+
290
+ let store = createVersionStore();
291
+
292
+ // Track changes
293
+ store = addVersion(store, rule, "create", { comment: "Initial version" });
294
+ store = addVersion(store, updatedRule, "update", { comment: "Increased priority" });
295
+
296
+ // Get history
297
+ const history = getHistory(store, rule.id);
298
+
299
+ // Rollback
300
+ const { store: newStore, rule: restoredRule } = rollbackToVersion(
301
+ store,
302
+ rule.id,
303
+ 1 // version number
304
+ );
305
+ ```
306
+
307
+ ## Indexing & Optimization
308
+
309
+ ```typescript
310
+ import {
311
+ buildIndex,
312
+ getRulesByField,
313
+ getRulesByTag,
314
+ analyzeOptimizations,
315
+ } from "@f-o-t/rules-engine";
316
+
317
+ // Build index for fast lookups
318
+ const index = buildIndex(rules);
319
+
320
+ // Query by field
321
+ const amountRules = getRulesByField(index, "amount");
322
+
323
+ // Query by tag
324
+ const pricingRules = getRulesByTag(index, "pricing");
325
+
326
+ // Get optimization suggestions
327
+ const suggestions = analyzeOptimizations(rules);
328
+ ```
329
+
330
+ ## Analysis
331
+
332
+ ```typescript
333
+ import {
334
+ analyzeRuleSet,
335
+ analyzeRuleComplexity,
336
+ findMostComplexRules,
337
+ } from "@f-o-t/rules-engine";
338
+
339
+ // Analyze entire rule set
340
+ const analysis = analyzeRuleSet(rules);
341
+ console.log(analysis.ruleCount);
342
+ console.log(analysis.uniqueFields);
343
+ console.log(analysis.uniqueCategories);
344
+ console.log(analysis.fieldUsage);
345
+ console.log(analysis.operatorUsage);
346
+
347
+ // Find complex rules
348
+ const complexRules = findMostComplexRules(rules, 5);
349
+ ```
350
+
351
+ ## Serialization
352
+
353
+ ```typescript
354
+ import {
355
+ exportToJson,
356
+ importFromJson,
357
+ cloneRule,
358
+ mergeRuleSets,
359
+ diffRuleSets,
360
+ } from "@f-o-t/rules-engine";
361
+
362
+ // Export/Import
363
+ const json = exportToJson(rules);
364
+ const { success, rules: imported } = importFromJson(json, {
365
+ generateNewIds: true, // Generate new IDs on import
366
+ });
367
+
368
+ // Clone rule
369
+ const cloned = cloneRule(rule, { generateNewId: true });
370
+
371
+ // Merge rule sets
372
+ const merged = mergeRuleSets(rulesA, rulesB, {
373
+ onConflict: "keep-first", // "keep-first" | "keep-second" | "keep-both"
374
+ });
375
+
376
+ // Diff rule sets
377
+ const diff = diffRuleSets(rulesA, rulesB);
378
+ console.log(diff.added);
379
+ console.log(diff.removed);
380
+ console.log(diff.modified);
381
+ ```
382
+
383
+ ## Filtering, Sorting & Grouping
384
+
385
+ ```typescript
386
+ import {
387
+ filterRules,
388
+ filterByTags,
389
+ filterByCategory,
390
+ filterByEnabled,
391
+ sortRules,
392
+ sortByPriority,
393
+ sortByName,
394
+ sortByCreatedAt,
395
+ groupRules,
396
+ groupByCategory,
397
+ groupByPriority,
398
+ groupByEnabled,
399
+ } from "@f-o-t/rules-engine";
400
+
401
+ // Filter rules
402
+ const activeRules = filterByEnabled(rules, true);
403
+ const pricingRules = filterByTags(rules, ["pricing"]);
404
+ const discountRules = filterByCategory(rules, "discounts");
405
+
406
+ // Combined filters
407
+ const filtered = filterRules(rules, {
408
+ enabled: true,
409
+ tags: ["pricing"],
410
+ category: "discounts",
411
+ });
412
+
413
+ // Sort rules
414
+ const byPriority = sortByPriority(rules, "desc"); // Highest first
415
+ const byName = sortByName(rules, "asc");
416
+ const byDate = sortByCreatedAt(rules, "desc");
417
+
418
+ // Custom sort
419
+ const sorted = sortRules(rules, { field: "priority", direction: "desc" });
420
+
421
+ // Group rules
422
+ const byCategory = groupByCategory(rules);
423
+ const byPriorityLevel = groupByPriority(rules);
424
+ const byStatus = groupByEnabled(rules);
425
+ ```
426
+
427
+ ## Utilities
428
+
429
+ ```typescript
430
+ import {
431
+ generateId,
432
+ hashContext,
433
+ hashRules,
434
+ pipe,
435
+ compose,
436
+ identity,
437
+ always,
438
+ tap,
439
+ measureTime,
440
+ measureTimeAsync,
441
+ withTimeout,
442
+ delay,
443
+ } from "@f-o-t/rules-engine";
444
+
445
+ // Generate unique IDs
446
+ const id = generateId();
447
+
448
+ // Hash context for caching
449
+ const hash = hashContext({ amount: 100 });
450
+
451
+ // Functional composition
452
+ const process = pipe(
453
+ (rules) => filterRules(rules, { enabled: true }),
454
+ (rules) => sortByPriority(rules, "desc"),
455
+ (rules) => rules.slice(0, 10)
456
+ );
457
+
458
+ // Measure execution time
459
+ const { result, durationMs } = measureTime(() => expensiveOperation());
460
+
461
+ // Async timing
462
+ const { result: asyncResult, durationMs: asyncTime } = await measureTimeAsync(
463
+ () => fetchData()
464
+ );
465
+
466
+ // Timeout wrapper
467
+ const resultWithTimeout = await withTimeout(
468
+ () => slowOperation(),
469
+ 5000 // 5 second timeout
470
+ );
471
+ ```
472
+
473
+ ## State Management (Functional API)
474
+
475
+ For functional programming without the engine wrapper:
476
+
477
+ ```typescript
478
+ import {
479
+ createInitialState,
480
+ addRule,
481
+ addRules,
482
+ updateRule,
483
+ removeRule,
484
+ getRule,
485
+ getRules,
486
+ enableRule,
487
+ disableRule,
488
+ cloneState,
489
+ } from "@f-o-t/rules-engine";
490
+
491
+ // Create initial state
492
+ let state = createInitialState();
493
+
494
+ // Add rules (returns new state)
495
+ state = addRule(state, ruleInput);
496
+ state = addRules(state, [rule1, rule2]);
497
+
498
+ // Query rules
499
+ const rule = getRule(state, "rule-id");
500
+ const allRules = getRules(state);
501
+
502
+ // Update rules
503
+ state = updateRule(state, "rule-id", { priority: 200 });
504
+ state = enableRule(state, "rule-id");
505
+ state = disableRule(state, "rule-id");
506
+
507
+ // Clone state for comparison
508
+ const clonedState = cloneState(state);
509
+ ```
510
+
511
+ ## License
512
+
513
+ MIT