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

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 (45) hide show
  1. package/package.json +4 -1
  2. package/CHANGELOG.md +0 -168
  3. package/__tests__/builder.test.ts +0 -363
  4. package/__tests__/cache.test.ts +0 -130
  5. package/__tests__/config.test.ts +0 -35
  6. package/__tests__/engine.test.ts +0 -1213
  7. package/__tests__/evaluate.test.ts +0 -339
  8. package/__tests__/exports.test.ts +0 -30
  9. package/__tests__/filter-sort.test.ts +0 -303
  10. package/__tests__/integration.test.ts +0 -419
  11. package/__tests__/money-integration.test.ts +0 -149
  12. package/__tests__/validation.test.ts +0 -862
  13. package/biome.json +0 -39
  14. package/docs/MIGRATION-v3.md +0 -118
  15. package/fot.config.ts +0 -5
  16. package/src/analyzer/analysis.ts +0 -401
  17. package/src/builder/conditions.ts +0 -321
  18. package/src/builder/rule.ts +0 -192
  19. package/src/cache/cache.ts +0 -135
  20. package/src/cache/noop.ts +0 -20
  21. package/src/core/evaluate.ts +0 -185
  22. package/src/core/filter.ts +0 -85
  23. package/src/core/group.ts +0 -103
  24. package/src/core/sort.ts +0 -90
  25. package/src/engine/engine.ts +0 -462
  26. package/src/engine/hooks.ts +0 -235
  27. package/src/engine/state.ts +0 -322
  28. package/src/index.ts +0 -303
  29. package/src/optimizer/index-builder.ts +0 -381
  30. package/src/serialization/serializer.ts +0 -408
  31. package/src/simulation/simulator.ts +0 -359
  32. package/src/types/config.ts +0 -184
  33. package/src/types/consequence.ts +0 -38
  34. package/src/types/evaluation.ts +0 -87
  35. package/src/types/rule.ts +0 -112
  36. package/src/types/state.ts +0 -116
  37. package/src/utils/conditions.ts +0 -108
  38. package/src/utils/hash.ts +0 -30
  39. package/src/utils/id.ts +0 -6
  40. package/src/utils/time.ts +0 -42
  41. package/src/validation/conflicts.ts +0 -440
  42. package/src/validation/integrity.ts +0 -473
  43. package/src/validation/schema.ts +0 -386
  44. package/src/versioning/version-store.ts +0 -337
  45. 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
- });