@f-o-t/rules-engine 3.0.0 → 3.0.5

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.
Files changed (105) hide show
  1. package/dist/analyzer/analysis.d.ts +72 -0
  2. package/dist/analyzer/analysis.d.ts.map +1 -0
  3. package/dist/builder/conditions.d.ts +29 -0
  4. package/dist/builder/conditions.d.ts.map +1 -0
  5. package/dist/builder/rule.d.ts +40 -0
  6. package/dist/builder/rule.d.ts.map +1 -0
  7. package/dist/cache/cache.d.ts +21 -0
  8. package/dist/cache/cache.d.ts.map +1 -0
  9. package/dist/cache/noop.d.ts +3 -0
  10. package/dist/cache/noop.d.ts.map +1 -0
  11. package/dist/core/evaluate.d.ts +21 -0
  12. package/dist/core/evaluate.d.ts.map +1 -0
  13. package/dist/core/filter.d.ts +8 -0
  14. package/dist/core/filter.d.ts.map +1 -0
  15. package/dist/core/group.d.ts +10 -0
  16. package/dist/core/group.d.ts.map +1 -0
  17. package/dist/core/sort.d.ts +14 -0
  18. package/dist/core/sort.d.ts.map +1 -0
  19. package/dist/engine/engine.d.ts +27 -0
  20. package/dist/engine/engine.d.ts.map +1 -0
  21. package/dist/engine/hooks.d.ts +16 -0
  22. package/dist/engine/hooks.d.ts.map +1 -0
  23. package/dist/engine/state.d.ts +19 -0
  24. package/dist/engine/state.d.ts.map +1 -0
  25. package/dist/index.d.ts +33 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +3083 -0
  28. package/dist/index.js.map +37 -0
  29. package/dist/optimizer/index-builder.d.ts +48 -0
  30. package/dist/optimizer/index-builder.d.ts.map +1 -0
  31. package/dist/serialization/serializer.d.ts +79 -0
  32. package/dist/serialization/serializer.d.ts.map +1 -0
  33. package/dist/simulation/simulator.d.ts +65 -0
  34. package/dist/simulation/simulator.d.ts.map +1 -0
  35. package/dist/types/config.d.ts +87 -0
  36. package/dist/types/config.d.ts.map +1 -0
  37. package/dist/types/consequence.d.ts +21 -0
  38. package/dist/types/consequence.d.ts.map +1 -0
  39. package/dist/types/evaluation.d.ts +70 -0
  40. package/dist/types/evaluation.d.ts.map +1 -0
  41. package/dist/types/rule.d.ts +114 -0
  42. package/dist/types/rule.d.ts.map +1 -0
  43. package/dist/types/state.d.ts +65 -0
  44. package/dist/types/state.d.ts.map +1 -0
  45. package/dist/utils/conditions.d.ts +24 -0
  46. package/dist/utils/conditions.d.ts.map +1 -0
  47. package/dist/utils/hash.d.ts +3 -0
  48. package/dist/utils/hash.d.ts.map +1 -0
  49. package/dist/utils/id.d.ts +2 -0
  50. package/dist/utils/id.d.ts.map +1 -0
  51. package/dist/utils/time.d.ts +8 -0
  52. package/dist/utils/time.d.ts.map +1 -0
  53. package/dist/validation/conflicts.d.ts +25 -0
  54. package/dist/validation/conflicts.d.ts.map +1 -0
  55. package/dist/validation/integrity.d.ts +38 -0
  56. package/dist/validation/integrity.d.ts.map +1 -0
  57. package/dist/validation/schema.d.ts +69 -0
  58. package/dist/validation/schema.d.ts.map +1 -0
  59. package/dist/versioning/version-store.d.ts +50 -0
  60. package/dist/versioning/version-store.d.ts.map +1 -0
  61. package/package.json +35 -31
  62. package/CHANGELOG.md +0 -168
  63. package/__tests__/builder.test.ts +0 -363
  64. package/__tests__/cache.test.ts +0 -130
  65. package/__tests__/config.test.ts +0 -35
  66. package/__tests__/engine.test.ts +0 -1213
  67. package/__tests__/evaluate.test.ts +0 -339
  68. package/__tests__/exports.test.ts +0 -30
  69. package/__tests__/filter-sort.test.ts +0 -303
  70. package/__tests__/integration.test.ts +0 -419
  71. package/__tests__/money-integration.test.ts +0 -149
  72. package/__tests__/validation.test.ts +0 -862
  73. package/biome.json +0 -39
  74. package/docs/MIGRATION-v3.md +0 -118
  75. package/fot.config.ts +0 -5
  76. package/src/analyzer/analysis.ts +0 -401
  77. package/src/builder/conditions.ts +0 -321
  78. package/src/builder/rule.ts +0 -192
  79. package/src/cache/cache.ts +0 -135
  80. package/src/cache/noop.ts +0 -20
  81. package/src/core/evaluate.ts +0 -185
  82. package/src/core/filter.ts +0 -85
  83. package/src/core/group.ts +0 -103
  84. package/src/core/sort.ts +0 -90
  85. package/src/engine/engine.ts +0 -462
  86. package/src/engine/hooks.ts +0 -235
  87. package/src/engine/state.ts +0 -322
  88. package/src/index.ts +0 -303
  89. package/src/optimizer/index-builder.ts +0 -381
  90. package/src/serialization/serializer.ts +0 -408
  91. package/src/simulation/simulator.ts +0 -359
  92. package/src/types/config.ts +0 -184
  93. package/src/types/consequence.ts +0 -38
  94. package/src/types/evaluation.ts +0 -87
  95. package/src/types/rule.ts +0 -112
  96. package/src/types/state.ts +0 -116
  97. package/src/utils/conditions.ts +0 -108
  98. package/src/utils/hash.ts +0 -30
  99. package/src/utils/id.ts +0 -6
  100. package/src/utils/time.ts +0 -42
  101. package/src/validation/conflicts.ts +0 -440
  102. package/src/validation/integrity.ts +0 -473
  103. package/src/validation/schema.ts +0 -386
  104. package/src/versioning/version-store.ts +0 -337
  105. package/tsconfig.json +0 -29
@@ -1,419 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { ConditionGroup } from "@f-o-t/condition-evaluator";
3
- import { createEvaluator } from "@f-o-t/condition-evaluator";
4
- import {
5
- addVersion,
6
- all,
7
- analyzeRuleSet,
8
- and,
9
- buildIndex,
10
- checkIntegrity,
11
- createEngine,
12
- createVersionStore,
13
- detectConflicts,
14
- exportToJson,
15
- getLatestVersion,
16
- getRulesByField,
17
- importFromJson,
18
- num,
19
- type Rule,
20
- rollbackToVersion,
21
- rule,
22
- simulate,
23
- str,
24
- validateRule,
25
- whatIf,
26
- } from "../src/index";
27
-
28
- const toRule = (input: ReturnType<ReturnType<typeof rule>["build"]>): Rule => ({
29
- id: input.id ?? `rule-${Date.now()}-${Math.random().toString(36).slice(2)}`,
30
- name: input.name,
31
- description: input.description,
32
- conditions: input.conditions,
33
- consequences: input.consequences.map((c) => ({
34
- type: String(c.type),
35
- payload: c.payload,
36
- })),
37
- priority: input.priority ?? 0,
38
- enabled: input.enabled ?? true,
39
- stopOnMatch: input.stopOnMatch ?? false,
40
- tags: input.tags ?? [],
41
- category: input.category,
42
- metadata: input.metadata,
43
- createdAt: new Date(),
44
- updatedAt: new Date(),
45
- });
46
-
47
- describe("Integration Tests", () => {
48
- describe("Rule Builder and Conditions", () => {
49
- test("creates rules with fluent builder", () => {
50
- const discountRule = rule()
51
- .named("Discount Rule")
52
- .describedAs("Apply discount for large orders")
53
- .when(
54
- and((c) =>
55
- c.number("amount", "gt", 100).number("itemCount", "gte", 5),
56
- ),
57
- )
58
- .then("apply_discount", { percentage: 10 })
59
- .withPriority(50)
60
- .tagged("pricing", "discount")
61
- .inCategory("pricing")
62
- .build();
63
-
64
- expect(discountRule.name).toBe("Discount Rule");
65
- expect(discountRule.description).toBe(
66
- "Apply discount for large orders",
67
- );
68
- expect(discountRule.priority).toBe(50);
69
- expect(discountRule.tags).toContain("pricing");
70
- expect(discountRule.category).toBe("pricing");
71
- expect(discountRule.consequences.length).toBe(1);
72
- });
73
-
74
- test("creates rules with shorthand condition helpers", () => {
75
- const conditions = all(
76
- num("amount", "gt", 50),
77
- str("customerType", "eq", "premium"),
78
- );
79
-
80
- const myRule = rule()
81
- .named("Simple Rule")
82
- .when(conditions)
83
- .then("notify", { message: "Hello" })
84
- .build();
85
-
86
- expect(myRule.name).toBe("Simple Rule");
87
- expect(myRule.enabled).toBe(true);
88
- });
89
- });
90
-
91
- describe("Engine Operations", () => {
92
- test("creates engine and manages rules", () => {
93
- const engine = createEngine({ evaluator: createEvaluator() });
94
-
95
- const ruleInput = rule()
96
- .named("Rule 1")
97
- .when(all(num("amount", "gt", 100)))
98
- .then("action", { type: "discount" })
99
- .build();
100
-
101
- const rule1 = engine.addRule(ruleInput);
102
- expect(engine.getRules().length).toBe(1);
103
-
104
- engine.disableRule(rule1.id);
105
- expect(engine.getRule(rule1.id)?.enabled).toBe(false);
106
-
107
- engine.enableRule(rule1.id);
108
- expect(engine.getRule(rule1.id)?.enabled).toBe(true);
109
-
110
- engine.removeRule(rule1.id);
111
- expect(engine.getRules().length).toBe(0);
112
- });
113
-
114
- test("evaluates rules against context", async () => {
115
- const engine = createEngine({ evaluator: createEvaluator() });
116
-
117
- engine.addRule(
118
- rule()
119
- .named("High Amount")
120
- .when(all(num("amount", "gt", 100)))
121
- .then("flag", { level: "high" })
122
- .withPriority(100)
123
- .build(),
124
- );
125
-
126
- engine.addRule(
127
- rule()
128
- .named("Medium Amount")
129
- .when(all(num("amount", "gt", 50)))
130
- .then("flag", { level: "medium" })
131
- .withPriority(50)
132
- .build(),
133
- );
134
-
135
- const result = await engine.evaluate({ amount: 150, itemCount: 2 });
136
-
137
- expect(result.matchedRules.length).toBe(2);
138
- expect(result.totalRulesEvaluated).toBe(2);
139
- });
140
- });
141
-
142
- describe("Simulation", () => {
143
- test("simulates rules without side effects", () => {
144
- const rules: Rule[] = [
145
- toRule(
146
- rule()
147
- .named("Simulation Test")
148
- .when(all(num("amount", "gt", 100)))
149
- .then("discount", { percent: 10 })
150
- .build(),
151
- ),
152
- ];
153
-
154
- const result = simulate(rules, { data: { amount: 150 } });
155
-
156
- expect(result.matchedRules.length).toBe(1);
157
- expect(result.consequences.length).toBe(1);
158
- expect(result.executionTimeMs).toBeGreaterThanOrEqual(0);
159
- });
160
-
161
- test("compares rules with whatIf", () => {
162
- const originalRules: Rule[] = [
163
- toRule(
164
- rule()
165
- .named("Original")
166
- .when(all(num("amount", "gt", 100)))
167
- .then("discount", { percent: 10 })
168
- .build(),
169
- ),
170
- ];
171
-
172
- const modifiedRules: Rule[] = [
173
- toRule(
174
- rule()
175
- .named("Modified")
176
- .when(all(num("amount", "gt", 50)))
177
- .then("discount", { percent: 15 })
178
- .build(),
179
- ),
180
- ];
181
-
182
- const context = { data: { amount: 75 } };
183
- const comparison = whatIf(originalRules, modifiedRules, context);
184
-
185
- expect(comparison.original.matchedRules.length).toBe(0);
186
- expect(comparison.modified.matchedRules.length).toBe(1);
187
- expect(comparison.differences.newMatches.length).toBe(1);
188
- });
189
- });
190
-
191
- describe("Serialization", () => {
192
- test("exports and imports rules", () => {
193
- const rules: Rule[] = [
194
- toRule(
195
- rule()
196
- .named("Export Test")
197
- .when(all(num("amount", "gt", 0)))
198
- .then("action", { type: "test" })
199
- .tagged("test")
200
- .build(),
201
- ),
202
- ];
203
-
204
- const json = exportToJson(rules);
205
- expect(json).toContain("Export Test");
206
- expect(json).toContain("action");
207
-
208
- const imported = importFromJson(json);
209
- expect(imported.success).toBe(true);
210
- expect(imported.rules.length).toBe(1);
211
- expect(imported.rules[0]?.name).toBe("Export Test");
212
- });
213
-
214
- test("generates new IDs on import", () => {
215
- const originalRule = toRule(
216
- rule()
217
- .named("ID Test")
218
- .when(all(num("amount", "gt", 0)))
219
- .then("action", {})
220
- .build(),
221
- );
222
-
223
- const json = exportToJson([originalRule]);
224
- const imported = importFromJson(json, { generateNewIds: true });
225
-
226
- expect(imported.rules[0]?.id).not.toBe(originalRule.id);
227
- });
228
- });
229
-
230
- describe("Versioning", () => {
231
- test("tracks rule versions", () => {
232
- const myRule = toRule(
233
- rule()
234
- .named("Versioned")
235
- .when(all(num("amount", "gt", 100)))
236
- .then("action", { version: 1 })
237
- .build(),
238
- );
239
-
240
- let store = createVersionStore();
241
- store = addVersion(store, myRule, "create", { comment: "v1" });
242
-
243
- const latest = getLatestVersion(store, myRule.id);
244
- expect(latest?.version).toBe(1);
245
- expect(latest?.comment).toBe("v1");
246
- });
247
-
248
- test("supports rollback", () => {
249
- const myRule = toRule(
250
- rule()
251
- .named("Rollback Test")
252
- .when(all(num("amount", "gt", 100)))
253
- .then("action", { v: 1 })
254
- .build(),
255
- );
256
-
257
- let store = createVersionStore();
258
- store = addVersion(store, myRule, "create");
259
-
260
- const updatedRule: Rule = {
261
- ...myRule,
262
- consequences: [{ type: "action", payload: { v: 2 } }],
263
- updatedAt: new Date(),
264
- };
265
- store = addVersion(store, updatedRule, "update");
266
-
267
- const { store: rolledBack } = rollbackToVersion(store, myRule.id, 1);
268
- const latest = getLatestVersion(rolledBack, myRule.id);
269
-
270
- expect(latest?.version).toBe(3);
271
- });
272
- });
273
-
274
- describe("Indexing", () => {
275
- test("builds and queries index", () => {
276
- const rules: Rule[] = [
277
- toRule(
278
- rule()
279
- .named("Amount Rule")
280
- .when(all(num("amount", "gt", 100)))
281
- .then("action", {})
282
- .build(),
283
- ),
284
- toRule(
285
- rule()
286
- .named("Count Rule")
287
- .when(all(num("itemCount", "gt", 5)))
288
- .then("action", {})
289
- .build(),
290
- ),
291
- ];
292
-
293
- const index = buildIndex(rules);
294
-
295
- expect(getRulesByField(index, "amount").length).toBe(1);
296
- expect(getRulesByField(index, "itemCount").length).toBe(1);
297
- expect(getRulesByField(index, "nonexistent").length).toBe(0);
298
- });
299
- });
300
-
301
- describe("Analysis", () => {
302
- test("analyzes rule set", () => {
303
- const rules: Rule[] = [
304
- toRule(
305
- rule()
306
- .named("Rule 1")
307
- .when(all(num("amount", "gt", 100)))
308
- .then("action", {})
309
- .inCategory("pricing")
310
- .build(),
311
- ),
312
- toRule(
313
- rule()
314
- .named("Rule 2")
315
- .when(
316
- and((c) =>
317
- c
318
- .number("amount", "gt", 50)
319
- .number("itemCount", "gt", 3),
320
- ),
321
- )
322
- .then("action", {})
323
- .then("notify", { msg: "test" })
324
- .inCategory("pricing")
325
- .build(),
326
- ),
327
- ];
328
-
329
- const analysis = analyzeRuleSet(rules);
330
-
331
- expect(analysis.ruleCount).toBe(2);
332
- expect(analysis.uniqueCategories).toContain("pricing");
333
- expect(analysis.ruleComplexities.length).toBe(2);
334
- });
335
- });
336
-
337
- describe("Conflict Detection", () => {
338
- test("detects duplicate conditions", () => {
339
- const conditions: ConditionGroup = {
340
- id: "g1",
341
- operator: "AND",
342
- conditions: [
343
- {
344
- id: "c1",
345
- type: "number",
346
- field: "amount",
347
- operator: "gt",
348
- value: 100,
349
- },
350
- ],
351
- };
352
-
353
- const rules: Rule[] = [
354
- {
355
- id: "rule-1",
356
- name: "Rule 1",
357
- conditions,
358
- consequences: [{ type: "a", payload: {} }],
359
- priority: 50,
360
- enabled: true,
361
- stopOnMatch: false,
362
- tags: [],
363
- createdAt: new Date(),
364
- updatedAt: new Date(),
365
- },
366
- {
367
- id: "rule-2",
368
- name: "Rule 2",
369
- conditions,
370
- consequences: [{ type: "b", payload: {} }],
371
- priority: 50,
372
- enabled: true,
373
- stopOnMatch: false,
374
- tags: [],
375
- createdAt: new Date(),
376
- updatedAt: new Date(),
377
- },
378
- ];
379
-
380
- const conflicts = detectConflicts(rules);
381
- expect(conflicts.some((c) => c.type === "DUPLICATE_CONDITIONS")).toBe(
382
- true,
383
- );
384
- });
385
- });
386
-
387
- describe("Validation", () => {
388
- test("validates rule structure", () => {
389
- const validRule = toRule(
390
- rule()
391
- .named("Valid Rule")
392
- .when(all(num("amount", "gt", 0)))
393
- .then("action", {})
394
- .build(),
395
- );
396
-
397
- const result = validateRule(validRule);
398
- expect(result.valid).toBe(true);
399
- });
400
-
401
- test("checks integrity", () => {
402
- const rules: Rule[] = [
403
- toRule(
404
- rule()
405
- .named("Warning Rule")
406
- .when(all(num("amount", "gt", 0)))
407
- .then("action", {})
408
- .withPriority(-10)
409
- .build(),
410
- ),
411
- ];
412
-
413
- const integrity = checkIntegrity(rules);
414
- expect(
415
- integrity.issues.some((i) => i.code === "NEGATIVE_PRIORITY"),
416
- ).toBe(true);
417
- });
418
- });
419
- });
@@ -1,149 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { createEngine } from "../src/engine/engine";
3
- import { moneyOperators } from "@f-o-t/money/plugins/operators";
4
- import { z } from "zod";
5
-
6
- type TransactionContext = {
7
- amount: {
8
- amount: string;
9
- currency: string;
10
- };
11
- accountBalance: {
12
- amount: string;
13
- currency: string;
14
- };
15
- };
16
-
17
- const TransactionConsequences = {
18
- approve: z.object({ approved: z.boolean() }),
19
- require_review: z.object({ reason: z.string() }),
20
- reject: z.object({ reason: z.string() }),
21
- } as const;
22
-
23
- describe("Money operators integration", () => {
24
- test("should use money_gt operator in rules", async () => {
25
- const engine = createEngine<TransactionContext, typeof TransactionConsequences>({
26
- consequences: TransactionConsequences,
27
- operators: moneyOperators,
28
- });
29
-
30
- engine.addRule({
31
- name: "high-value-transaction",
32
- conditions: {
33
- id: "g1",
34
- operator: "AND",
35
- conditions: [
36
- {
37
- id: "c1",
38
- type: "custom",
39
- field: "amount",
40
- operator: "money_gt",
41
- value: { amount: "1000.00", currency: "BRL" },
42
- },
43
- ],
44
- },
45
- consequences: [
46
- {
47
- type: "require_review",
48
- payload: { reason: "High value transaction" },
49
- },
50
- ],
51
- });
52
-
53
- const result = await engine.evaluate({
54
- amount: { amount: "1500.00", currency: "BRL" },
55
- accountBalance: { amount: "10000.00", currency: "BRL" },
56
- });
57
-
58
- expect(result.matchedRules).toHaveLength(1);
59
- expect(result.matchedRules[0]?.name).toBe("high-value-transaction");
60
- expect(result.consequences).toHaveLength(1);
61
- expect(result.consequences[0]?.type).toBe("require_review");
62
- });
63
-
64
- test("should use money_between operator", async () => {
65
- const engine = createEngine<TransactionContext, typeof TransactionConsequences>({
66
- consequences: TransactionConsequences,
67
- operators: moneyOperators,
68
- });
69
-
70
- engine.addRule({
71
- name: "medium-value-transaction",
72
- conditions: {
73
- id: "g1",
74
- operator: "AND",
75
- conditions: [
76
- {
77
- id: "c1",
78
- type: "custom",
79
- field: "amount",
80
- operator: "money_between",
81
- value: [
82
- { amount: "100.00", currency: "BRL" },
83
- { amount: "1000.00", currency: "BRL" },
84
- ],
85
- },
86
- ],
87
- },
88
- consequences: [
89
- {
90
- type: "approve",
91
- payload: { approved: true },
92
- },
93
- ],
94
- });
95
-
96
- const result = await engine.evaluate({
97
- amount: { amount: "500.00", currency: "BRL" },
98
- accountBalance: { amount: "10000.00", currency: "BRL" },
99
- });
100
-
101
- expect(result.matchedRules).toHaveLength(1);
102
- expect(result.consequences[0]?.type).toBe("approve");
103
- });
104
-
105
- test("should handle multiple money conditions", async () => {
106
- const engine = createEngine<TransactionContext, typeof TransactionConsequences>({
107
- consequences: TransactionConsequences,
108
- operators: moneyOperators,
109
- });
110
-
111
- engine.addRule({
112
- name: "insufficient-funds",
113
- conditions: {
114
- id: "g1",
115
- operator: "AND",
116
- conditions: [
117
- {
118
- id: "c1",
119
- type: "custom",
120
- field: "amount",
121
- operator: "money_gt",
122
- value: { amount: "0.00", currency: "BRL" },
123
- },
124
- {
125
- id: "c2",
126
- type: "custom",
127
- field: "accountBalance",
128
- operator: "money_lt",
129
- value: { amount: "100.00", currency: "BRL" },
130
- },
131
- ],
132
- },
133
- consequences: [
134
- {
135
- type: "reject",
136
- payload: { reason: "Insufficient funds" },
137
- },
138
- ],
139
- });
140
-
141
- const result = await engine.evaluate({
142
- amount: { amount: "50.00", currency: "BRL" },
143
- accountBalance: { amount: "25.00", currency: "BRL" },
144
- });
145
-
146
- expect(result.matchedRules).toHaveLength(1);
147
- expect(result.consequences[0]?.type).toBe("reject");
148
- });
149
- });