@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.
- package/package.json +4 -1
- package/CHANGELOG.md +0 -168
- package/__tests__/builder.test.ts +0 -363
- package/__tests__/cache.test.ts +0 -130
- package/__tests__/config.test.ts +0 -35
- package/__tests__/engine.test.ts +0 -1213
- package/__tests__/evaluate.test.ts +0 -339
- package/__tests__/exports.test.ts +0 -30
- package/__tests__/filter-sort.test.ts +0 -303
- package/__tests__/integration.test.ts +0 -419
- package/__tests__/money-integration.test.ts +0 -149
- package/__tests__/validation.test.ts +0 -862
- package/biome.json +0 -39
- package/docs/MIGRATION-v3.md +0 -118
- package/fot.config.ts +0 -5
- package/src/analyzer/analysis.ts +0 -401
- package/src/builder/conditions.ts +0 -321
- package/src/builder/rule.ts +0 -192
- package/src/cache/cache.ts +0 -135
- package/src/cache/noop.ts +0 -20
- package/src/core/evaluate.ts +0 -185
- package/src/core/filter.ts +0 -85
- package/src/core/group.ts +0 -103
- package/src/core/sort.ts +0 -90
- package/src/engine/engine.ts +0 -462
- package/src/engine/hooks.ts +0 -235
- package/src/engine/state.ts +0 -322
- package/src/index.ts +0 -303
- package/src/optimizer/index-builder.ts +0 -381
- package/src/serialization/serializer.ts +0 -408
- package/src/simulation/simulator.ts +0 -359
- package/src/types/config.ts +0 -184
- package/src/types/consequence.ts +0 -38
- package/src/types/evaluation.ts +0 -87
- package/src/types/rule.ts +0 -112
- package/src/types/state.ts +0 -116
- package/src/utils/conditions.ts +0 -108
- package/src/utils/hash.ts +0 -30
- package/src/utils/id.ts +0 -6
- package/src/utils/time.ts +0 -42
- package/src/validation/conflicts.ts +0 -440
- package/src/validation/integrity.ts +0 -473
- package/src/validation/schema.ts +0 -386
- package/src/versioning/version-store.ts +0 -337
- package/tsconfig.json +0 -29
package/package.json
CHANGED
package/CHANGELOG.md
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [3.0.0] - 2026-01-31
|
|
9
|
-
|
|
10
|
-
### ⚠️ BREAKING CHANGES
|
|
11
|
-
|
|
12
|
-
- Engine now requires `evaluator` or `operators` configuration
|
|
13
|
-
- `evaluateRule()` function signature changed to accept evaluator parameter
|
|
14
|
-
- `evaluateRules()` function signature changed to accept evaluator parameter
|
|
15
|
-
|
|
16
|
-
### 🎉 Features
|
|
17
|
-
|
|
18
|
-
- **Custom operators support**: Use custom operators from any library (e.g., `@f-o-t/money/operators`)
|
|
19
|
-
- **Plugin system integration**: Full integration with `@f-o-t/condition-evaluator` plugin system
|
|
20
|
-
- **Better extensibility**: Easily compose multiple operator sets
|
|
21
|
-
- **Type-safe operators**: Better TypeScript support for custom operator types
|
|
22
|
-
|
|
23
|
-
### 📚 Documentation
|
|
24
|
-
|
|
25
|
-
- Added migration guide for v2 → v3
|
|
26
|
-
- Updated README with custom operator examples
|
|
27
|
-
- Added examples for money operators integration
|
|
28
|
-
|
|
29
|
-
### 🔧 Migration
|
|
30
|
-
|
|
31
|
-
See [MIGRATION-v3.md](./docs/MIGRATION-v3.md) for detailed migration instructions.
|
|
32
|
-
|
|
33
|
-
**Quick migration:**
|
|
34
|
-
```typescript
|
|
35
|
-
// Before (v2.x)
|
|
36
|
-
const engine = createEngine({ consequences: MyConsequences });
|
|
37
|
-
|
|
38
|
-
// After (v3.x)
|
|
39
|
-
const engine = createEngine({
|
|
40
|
-
consequences: MyConsequences,
|
|
41
|
-
evaluator: createEvaluator() // Add this
|
|
42
|
-
});
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## [2.0.2] - 2026-01-25
|
|
48
|
-
|
|
49
|
-
### Changed
|
|
50
|
-
|
|
51
|
-
- Updated dependencies to latest versions
|
|
52
|
-
|
|
53
|
-
## [2.0.1] - 2025-12-31
|
|
54
|
-
|
|
55
|
-
### Changed
|
|
56
|
-
|
|
57
|
-
- Removed unused imports and dead code
|
|
58
|
-
- Removed unused `ConditionGroup` type import from index-builder
|
|
59
|
-
- Removed unused `ConflictResolutionStrategySchema` import from config types
|
|
60
|
-
- Removed unused `_cacheStats` variable from engine stats
|
|
61
|
-
- Removed unused `_collectAllConditionIds` helper function from integrity validation
|
|
62
|
-
|
|
63
|
-
## [2.0.0] - 2025-12-24
|
|
64
|
-
|
|
65
|
-
### Added
|
|
66
|
-
|
|
67
|
-
- **Zod Schema-First Types**: All configuration types now built from Zod schemas
|
|
68
|
-
- `CacheConfigSchema`, `ValidationConfigSchema`, `VersioningConfigSchema`, `LogLevelSchema`
|
|
69
|
-
- `ConflictResolutionStrategySchema`, `EvaluateOptionsSchema`, `EvaluateConfigSchema`
|
|
70
|
-
- `RuleStatsSchema`, `CacheStatsSchema`, `EngineStatsSchema`
|
|
71
|
-
- `ValidationErrorSchema`, `ValidationResultSchema`, `ValidationOptionsSchema`
|
|
72
|
-
- **Hook Error Handling**: New `onHookError` callback for capturing hook errors
|
|
73
|
-
- Previously hooks failed silently; now errors are reported via callback
|
|
74
|
-
- **Hook Timeout Protection**: New `hookTimeoutMs` config option
|
|
75
|
-
- Prevents slow hooks from blocking engine execution indefinitely
|
|
76
|
-
- Timeout errors are reported through `onHookError`
|
|
77
|
-
- **Orphaned Reference Detection**: `ImportResult` now includes `orphanedReferences`
|
|
78
|
-
- Detects when imported RuleSets reference non-existent rule IDs
|
|
79
|
-
- New `OrphanedReference` type exported
|
|
80
|
-
- **Shared Conditions Utility**: New internal `src/utils/conditions.ts`
|
|
81
|
-
- `collectConditionFields()`, `collectConditionOperators()`, `countConditions()`
|
|
82
|
-
- `calculateMaxDepth()`, `countConditionGroups()`
|
|
83
|
-
- **Config Helper Functions**: New functions replace constants
|
|
84
|
-
- `getDefaultCacheConfig()`, `getDefaultValidationConfig()`, `getDefaultVersioningConfig()`
|
|
85
|
-
- `getDefaultLogLevel()`, `getDefaultConflictResolution()`
|
|
86
|
-
- `parseCacheConfig()`, `parseValidationConfig()`, `parseVersioningConfig()`
|
|
87
|
-
|
|
88
|
-
### Changed
|
|
89
|
-
|
|
90
|
-
- **Cache Eviction Performance**: O(n) → O(1) for oldest entry eviction
|
|
91
|
-
- Now uses ES6 Map insertion order for FIFO eviction
|
|
92
|
-
- **Strict Mode Behavior**: `strictMode: true` now implicitly enables consequence validation
|
|
93
|
-
- Previously required both `strictMode: true` and `validateConsequences: true`
|
|
94
|
-
|
|
95
|
-
### Removed
|
|
96
|
-
|
|
97
|
-
- **BREAKING**: Removed FP utility exports from `src/utils/pipe.ts`
|
|
98
|
-
- `pipe()`, `compose()`, `identity()`, `always()`, `tap()`
|
|
99
|
-
- These generic utilities are not domain-specific; use lodash/ramda instead
|
|
100
|
-
- **BREAKING**: Removed `delay()` from `src/utils/time.ts`
|
|
101
|
-
- Low value utility; use `Bun.sleep()` or inline `new Promise(resolve => setTimeout(resolve, ms))`
|
|
102
|
-
- **BREAKING**: Removed `DEFAULT_*` constant exports
|
|
103
|
-
- `DEFAULT_CACHE_CONFIG` → use `getDefaultCacheConfig()`
|
|
104
|
-
- `DEFAULT_VALIDATION_CONFIG` → use `getDefaultValidationConfig()`
|
|
105
|
-
- `DEFAULT_VERSIONING_CONFIG` → use `getDefaultVersioningConfig()`
|
|
106
|
-
- `DEFAULT_ENGINE_CONFIG` → removed (use schema defaults)
|
|
107
|
-
- **BREAKING**: Removed internal mutable type exports
|
|
108
|
-
- `MutableEngineState`, `MutableOptimizerState`, `MutableRuleStats`
|
|
109
|
-
- These are internal implementation details
|
|
110
|
-
|
|
111
|
-
### Fixed
|
|
112
|
-
|
|
113
|
-
- **Silent Hook Errors**: All 11 hook execution functions now report errors via `onHookError`
|
|
114
|
-
- **Redundant Sorting**: Removed unnecessary sort in `evaluate()` (rules already sorted on add/update)
|
|
115
|
-
|
|
116
|
-
## [1.0.0] - 2025-12-10
|
|
117
|
-
|
|
118
|
-
### Added
|
|
119
|
-
|
|
120
|
-
- Initial release of the rules engine library
|
|
121
|
-
- **Engine**: Stateful rule management with `createEngine()`
|
|
122
|
-
- Add, update, remove, enable/disable rules
|
|
123
|
-
- Rule sets for grouping related rules
|
|
124
|
-
- Configurable caching with TTL and max size
|
|
125
|
-
- Lifecycle hooks (onBeforeEvaluation, onAfterEvaluation, onRuleMatch, onRuleError, onCacheHit)
|
|
126
|
-
- Conflict resolution strategies: "all", "first-match", "highest-priority"
|
|
127
|
-
- **Fluent Builders**: Chainable APIs for building rules and conditions
|
|
128
|
-
- `rule()` builder with full configuration options
|
|
129
|
-
- Shorthand condition helpers: `num()`, `str()`, `bool()`, `date()`, `arr()`
|
|
130
|
-
- Logical operators: `all()`, `any()`, `and()`, `or()`
|
|
131
|
-
- **Core Evaluation**: Built on `@f-o-t/condition-evaluator`
|
|
132
|
-
- `evaluateRule()` and `evaluateRules()` functions
|
|
133
|
-
- Filter rules by tags, category, enabled status
|
|
134
|
-
- Sort rules by priority, name, created/updated date
|
|
135
|
-
- Group rules by category, priority, enabled status, or custom function
|
|
136
|
-
- **Validation**: Comprehensive rule validation
|
|
137
|
-
- Schema validation with Zod
|
|
138
|
-
- Conflict detection (duplicate IDs, overlapping conditions, priority collisions, unreachable rules)
|
|
139
|
-
- Integrity checks (negative priority, missing fields, invalid operators)
|
|
140
|
-
- **Simulation**: Test rules without side effects
|
|
141
|
-
- `simulate()` for single context testing
|
|
142
|
-
- `batchSimulate()` for multiple contexts
|
|
143
|
-
- `whatIf()` for comparing rule set changes
|
|
144
|
-
- **Versioning**: Track rule changes with rollback support
|
|
145
|
-
- Version store with full history
|
|
146
|
-
- Rollback to any previous version
|
|
147
|
-
- Prune old versions
|
|
148
|
-
- **Indexing & Optimization**: Fast rule lookups
|
|
149
|
-
- Build indexes by field, tag, category, priority
|
|
150
|
-
- Optimization suggestions for rule sets
|
|
151
|
-
- **Analysis**: Rule set analytics
|
|
152
|
-
- Complexity analysis per rule
|
|
153
|
-
- Field, operator, and consequence usage statistics
|
|
154
|
-
- Find most complex rules
|
|
155
|
-
- **Serialization**: Import/export capabilities
|
|
156
|
-
- JSON export/import with optional ID regeneration
|
|
157
|
-
- Clone rules
|
|
158
|
-
- Merge and diff rule sets
|
|
159
|
-
- **Utilities**: Functional programming helpers
|
|
160
|
-
- `pipe()`, `compose()`, `identity()`, `always()`, `tap()`
|
|
161
|
-
- `measureTime()`, `measureTimeAsync()`, `withTimeout()`, `delay()`
|
|
162
|
-
- `generateId()`, `hashContext()`, `hashRules()`
|
|
163
|
-
|
|
164
|
-
### Changed
|
|
165
|
-
|
|
166
|
-
- **Internal refactor**: Removed 12 internal barrel files
|
|
167
|
-
- All imports now use direct file paths instead of barrel re-exports
|
|
168
|
-
- No public API changes - only internal module structure improvements
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
all,
|
|
5
|
-
and,
|
|
6
|
-
any,
|
|
7
|
-
arr,
|
|
8
|
-
bool,
|
|
9
|
-
conditions,
|
|
10
|
-
date,
|
|
11
|
-
num,
|
|
12
|
-
or,
|
|
13
|
-
resetBuilderIds,
|
|
14
|
-
str,
|
|
15
|
-
} from "../src/builder/conditions";
|
|
16
|
-
import { createRule, rule } from "../src/builder/rule";
|
|
17
|
-
import type { ConsequenceDefinitions } from "../src/types/consequence";
|
|
18
|
-
|
|
19
|
-
type TestContext = {
|
|
20
|
-
age: number;
|
|
21
|
-
country: string;
|
|
22
|
-
premium: boolean;
|
|
23
|
-
score: number;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const TestConsequences = {
|
|
27
|
-
applyDiscount: z.object({
|
|
28
|
-
percentage: z.number(),
|
|
29
|
-
reason: z.string(),
|
|
30
|
-
}),
|
|
31
|
-
sendNotification: z.object({
|
|
32
|
-
type: z.enum(["email", "sms", "push"]),
|
|
33
|
-
message: z.string(),
|
|
34
|
-
}),
|
|
35
|
-
setFlag: z.object({
|
|
36
|
-
name: z.string(),
|
|
37
|
-
value: z.boolean(),
|
|
38
|
-
}),
|
|
39
|
-
} satisfies ConsequenceDefinitions;
|
|
40
|
-
|
|
41
|
-
type TestConsequences = typeof TestConsequences;
|
|
42
|
-
|
|
43
|
-
describe("Condition Builder", () => {
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
resetBuilderIds();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe("conditions()", () => {
|
|
49
|
-
it("should create an empty condition builder", () => {
|
|
50
|
-
const builder = conditions();
|
|
51
|
-
expect(builder.getConditions()).toHaveLength(0);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("should chain number conditions", () => {
|
|
55
|
-
const group = conditions()
|
|
56
|
-
.number("age", "gte", 18)
|
|
57
|
-
.number("score", "gt", 100)
|
|
58
|
-
.build();
|
|
59
|
-
|
|
60
|
-
expect(group.operator).toBe("AND");
|
|
61
|
-
expect(group.conditions).toHaveLength(2);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("should chain string conditions", () => {
|
|
65
|
-
const group = conditions()
|
|
66
|
-
.string("country", "eq", "US")
|
|
67
|
-
.string("status", "in", ["active", "pending"])
|
|
68
|
-
.build();
|
|
69
|
-
|
|
70
|
-
expect(group.conditions).toHaveLength(2);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("should chain boolean conditions", () => {
|
|
74
|
-
const group = conditions()
|
|
75
|
-
.boolean("premium", "eq", true)
|
|
76
|
-
.boolean("verified", "neq", false)
|
|
77
|
-
.build();
|
|
78
|
-
|
|
79
|
-
expect(group.conditions).toHaveLength(2);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("should handle date conditions with Date objects", () => {
|
|
83
|
-
const now = new Date();
|
|
84
|
-
const group = conditions().date("createdAt", "gte", now).build();
|
|
85
|
-
|
|
86
|
-
expect(group.conditions).toHaveLength(1);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("should handle array conditions", () => {
|
|
90
|
-
const group = conditions()
|
|
91
|
-
.array("tags", "contains", "vip")
|
|
92
|
-
.array("items", "is_not_empty", undefined)
|
|
93
|
-
.build();
|
|
94
|
-
|
|
95
|
-
expect(group.conditions).toHaveLength(2);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("should support nested AND groups", () => {
|
|
99
|
-
const group = conditions()
|
|
100
|
-
.number("age", "gte", 18)
|
|
101
|
-
.and((cb) =>
|
|
102
|
-
cb.string("country", "eq", "US").boolean("premium", "eq", true),
|
|
103
|
-
)
|
|
104
|
-
.build();
|
|
105
|
-
|
|
106
|
-
expect(group.conditions).toHaveLength(2);
|
|
107
|
-
const nestedGroup = group.conditions[1] as { operator: string };
|
|
108
|
-
expect(nestedGroup.operator).toBe("AND");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("should support nested OR groups", () => {
|
|
112
|
-
const group = conditions()
|
|
113
|
-
.number("age", "gte", 18)
|
|
114
|
-
.or((cb) =>
|
|
115
|
-
cb.string("country", "eq", "US").string("country", "eq", "CA"),
|
|
116
|
-
)
|
|
117
|
-
.build();
|
|
118
|
-
|
|
119
|
-
expect(group.conditions).toHaveLength(2);
|
|
120
|
-
const nestedGroup = group.conditions[1] as { operator: string };
|
|
121
|
-
expect(nestedGroup.operator).toBe("OR");
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe("and() / or()", () => {
|
|
126
|
-
it("should create AND group from builder function", () => {
|
|
127
|
-
const group = and((cb) =>
|
|
128
|
-
cb.number("age", "gte", 18).string("country", "eq", "US"),
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
expect(group.operator).toBe("AND");
|
|
132
|
-
expect(group.conditions).toHaveLength(2);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("should create OR group from builder function", () => {
|
|
136
|
-
const group = or((cb) =>
|
|
137
|
-
cb.boolean("premium", "eq", true).number("score", "gte", 90),
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
expect(group.operator).toBe("OR");
|
|
141
|
-
expect(group.conditions).toHaveLength(2);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("all() / any()", () => {
|
|
146
|
-
it("should create AND group from conditions", () => {
|
|
147
|
-
const group = all(
|
|
148
|
-
num("age", "gte", 18),
|
|
149
|
-
str("country", "eq", "US"),
|
|
150
|
-
bool("premium", "eq", true),
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
expect(group.operator).toBe("AND");
|
|
154
|
-
expect(group.conditions).toHaveLength(3);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("should create OR group from conditions", () => {
|
|
158
|
-
const group = any(
|
|
159
|
-
bool("premium", "eq", true),
|
|
160
|
-
num("score", "gte", 90),
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
expect(group.operator).toBe("OR");
|
|
164
|
-
expect(group.conditions).toHaveLength(2);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("should support nested groups", () => {
|
|
168
|
-
const group = all(
|
|
169
|
-
num("age", "gte", 18),
|
|
170
|
-
any(bool("premium", "eq", true), num("score", "gte", 90)),
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
expect(group.operator).toBe("AND");
|
|
174
|
-
expect(group.conditions).toHaveLength(2);
|
|
175
|
-
const nestedGroup = group.conditions[1] as { operator: string };
|
|
176
|
-
expect(nestedGroup.operator).toBe("OR");
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
describe("condition helpers", () => {
|
|
181
|
-
it("num() should create number condition", () => {
|
|
182
|
-
const cond = num("age", "gte", 18);
|
|
183
|
-
expect(cond.type).toBe("number");
|
|
184
|
-
expect(cond.field).toBe("age");
|
|
185
|
-
expect(cond.operator).toBe("gte");
|
|
186
|
-
expect(cond.value).toBe(18);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("str() should create string condition", () => {
|
|
190
|
-
const cond = str("country", "eq", "US");
|
|
191
|
-
expect(cond.type).toBe("string");
|
|
192
|
-
expect(cond.field).toBe("country");
|
|
193
|
-
expect(cond.operator).toBe("eq");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("bool() should create boolean condition", () => {
|
|
197
|
-
const cond = bool("premium", "eq", true);
|
|
198
|
-
expect(cond.type).toBe("boolean");
|
|
199
|
-
expect(cond.field).toBe("premium");
|
|
200
|
-
expect(cond.operator).toBe("eq");
|
|
201
|
-
expect(cond.value).toBe(true);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("date() should create date condition", () => {
|
|
205
|
-
const cond = date("createdAt", "gte", "2024-01-01");
|
|
206
|
-
expect(cond.type).toBe("date");
|
|
207
|
-
expect(cond.field).toBe("createdAt");
|
|
208
|
-
expect(cond.operator).toBe("gte");
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("arr() should create array condition", () => {
|
|
212
|
-
const cond = arr("tags", "contains", "vip");
|
|
213
|
-
expect(cond.type).toBe("array");
|
|
214
|
-
expect(cond.field).toBe("tags");
|
|
215
|
-
expect(cond.operator).toBe("contains");
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
describe("Rule Builder", () => {
|
|
221
|
-
beforeEach(() => {
|
|
222
|
-
resetBuilderIds();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe("rule()", () => {
|
|
226
|
-
it("should create a rule builder", () => {
|
|
227
|
-
const builder = rule<TestContext, TestConsequences>();
|
|
228
|
-
const state = builder.getState();
|
|
229
|
-
|
|
230
|
-
expect(state.consequences).toHaveLength(0);
|
|
231
|
-
expect(state.priority).toBe(0);
|
|
232
|
-
expect(state.enabled).toBe(true);
|
|
233
|
-
expect(state.stopOnMatch).toBe(false);
|
|
234
|
-
expect(state.tags).toHaveLength(0);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("should build a complete rule", () => {
|
|
238
|
-
const ruleInput = rule<TestContext, TestConsequences>()
|
|
239
|
-
.id("rule-1")
|
|
240
|
-
.named("Adult Discount")
|
|
241
|
-
.describedAs("Apply discount for adults")
|
|
242
|
-
.when(all(num("age", "gte", 18)))
|
|
243
|
-
.then("applyDiscount", { percentage: 10, reason: "Adult discount" })
|
|
244
|
-
.withPriority(100)
|
|
245
|
-
.tagged("pricing", "adult")
|
|
246
|
-
.inCategory("discounts")
|
|
247
|
-
.build();
|
|
248
|
-
|
|
249
|
-
expect(ruleInput.id).toBe("rule-1");
|
|
250
|
-
expect(ruleInput.name).toBe("Adult Discount");
|
|
251
|
-
expect(ruleInput.description).toBe("Apply discount for adults");
|
|
252
|
-
expect(ruleInput.priority).toBe(100);
|
|
253
|
-
expect(ruleInput.enabled).toBe(true);
|
|
254
|
-
expect(ruleInput.stopOnMatch).toBe(false);
|
|
255
|
-
expect(ruleInput.tags).toContain("pricing");
|
|
256
|
-
expect(ruleInput.tags).toContain("adult");
|
|
257
|
-
expect(ruleInput.category).toBe("discounts");
|
|
258
|
-
expect(ruleInput.consequences).toHaveLength(1);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("should support multiple consequences", () => {
|
|
262
|
-
const ruleInput = rule<TestContext, TestConsequences>()
|
|
263
|
-
.named("Multi-consequence rule")
|
|
264
|
-
.when(all(bool("premium", "eq", true)))
|
|
265
|
-
.then("applyDiscount", { percentage: 20, reason: "VIP" })
|
|
266
|
-
.then("sendNotification", {
|
|
267
|
-
type: "email",
|
|
268
|
-
message: "Discount applied",
|
|
269
|
-
})
|
|
270
|
-
.then("setFlag", { name: "vip_treated", value: true })
|
|
271
|
-
.build();
|
|
272
|
-
|
|
273
|
-
expect(ruleInput.consequences).toHaveLength(3);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("should support disabling rules", () => {
|
|
277
|
-
const ruleInput = rule<TestContext, TestConsequences>()
|
|
278
|
-
.named("Disabled rule")
|
|
279
|
-
.when(all())
|
|
280
|
-
.then("setFlag", { name: "test", value: true })
|
|
281
|
-
.disabled()
|
|
282
|
-
.build();
|
|
283
|
-
|
|
284
|
-
expect(ruleInput.enabled).toBe(false);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it("should support stopOnMatch", () => {
|
|
288
|
-
const ruleInput = rule<TestContext, TestConsequences>()
|
|
289
|
-
.named("Stop rule")
|
|
290
|
-
.when(all())
|
|
291
|
-
.then("setFlag", { name: "test", value: true })
|
|
292
|
-
.stopOnMatch()
|
|
293
|
-
.build();
|
|
294
|
-
|
|
295
|
-
expect(ruleInput.stopOnMatch).toBe(true);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it("should support metadata", () => {
|
|
299
|
-
const ruleInput = rule<TestContext, TestConsequences>()
|
|
300
|
-
.named("Rule with metadata")
|
|
301
|
-
.when(all())
|
|
302
|
-
.then("setFlag", { name: "test", value: true })
|
|
303
|
-
.withMetadata({ author: "admin", version: 1 })
|
|
304
|
-
.build();
|
|
305
|
-
|
|
306
|
-
expect(ruleInput.metadata).toEqual({ author: "admin", version: 1 });
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("should throw error if name is missing", () => {
|
|
310
|
-
expect(() => {
|
|
311
|
-
rule<TestContext, TestConsequences>()
|
|
312
|
-
.when(all())
|
|
313
|
-
.then("setFlag", { name: "test", value: true })
|
|
314
|
-
.build();
|
|
315
|
-
}).toThrow("Rule must have a name");
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it("should throw error if conditions are missing", () => {
|
|
319
|
-
expect(() => {
|
|
320
|
-
rule<TestContext, TestConsequences>()
|
|
321
|
-
.named("Test rule")
|
|
322
|
-
.then("setFlag", { name: "test", value: true })
|
|
323
|
-
.build();
|
|
324
|
-
}).toThrow("Rule must have conditions");
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("should throw error if consequences are missing", () => {
|
|
328
|
-
expect(() => {
|
|
329
|
-
rule<TestContext, TestConsequences>()
|
|
330
|
-
.named("Test rule")
|
|
331
|
-
.when(all())
|
|
332
|
-
.build();
|
|
333
|
-
}).toThrow("Rule must have at least one consequence");
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it("should support builder function in when()", () => {
|
|
337
|
-
const ruleInput = rule<TestContext, TestConsequences>()
|
|
338
|
-
.named("Builder condition")
|
|
339
|
-
.when((cb) =>
|
|
340
|
-
cb.number("age", "gte", 18).boolean("premium", "eq", true),
|
|
341
|
-
)
|
|
342
|
-
.then("setFlag", { name: "test", value: true })
|
|
343
|
-
.build();
|
|
344
|
-
|
|
345
|
-
expect(ruleInput.conditions.operator).toBe("AND");
|
|
346
|
-
expect(ruleInput.conditions.conditions).toHaveLength(2);
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
describe("createRule()", () => {
|
|
351
|
-
it("should create rule using function", () => {
|
|
352
|
-
const ruleInput = createRule<TestContext, TestConsequences>((rb) =>
|
|
353
|
-
rb
|
|
354
|
-
.named("Test rule")
|
|
355
|
-
.when(all(num("age", "gte", 18)))
|
|
356
|
-
.then("setFlag", { name: "adult", value: true }),
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
expect(ruleInput.name).toBe("Test rule");
|
|
360
|
-
expect(ruleInput.consequences).toHaveLength(1);
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
});
|
package/__tests__/cache.test.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { createCache } from "../src/cache/cache";
|
|
3
|
-
import { createNoopCache } from "../src/cache/noop";
|
|
4
|
-
|
|
5
|
-
describe("createCache", () => {
|
|
6
|
-
test("should store and retrieve values", () => {
|
|
7
|
-
const cache = createCache<string>({ ttl: 1000, maxSize: 100 });
|
|
8
|
-
|
|
9
|
-
cache.set("key1", "value1");
|
|
10
|
-
expect(cache.get("key1")).toBe("value1");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test("should return undefined for missing keys", () => {
|
|
14
|
-
const cache = createCache<string>({ ttl: 1000, maxSize: 100 });
|
|
15
|
-
|
|
16
|
-
expect(cache.get("missing")).toBeUndefined();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("should track hits and misses", () => {
|
|
20
|
-
const cache = createCache<string>({ ttl: 1000, maxSize: 100 });
|
|
21
|
-
|
|
22
|
-
cache.set("key1", "value1");
|
|
23
|
-
cache.get("key1");
|
|
24
|
-
cache.get("missing");
|
|
25
|
-
|
|
26
|
-
const stats = cache.getStats();
|
|
27
|
-
expect(stats.hits).toBe(1);
|
|
28
|
-
expect(stats.misses).toBe(1);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("should expire entries after TTL", async () => {
|
|
32
|
-
const cache = createCache<string>({ ttl: 50, maxSize: 100 });
|
|
33
|
-
|
|
34
|
-
cache.set("key1", "value1");
|
|
35
|
-
expect(cache.get("key1")).toBe("value1");
|
|
36
|
-
|
|
37
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
38
|
-
|
|
39
|
-
expect(cache.get("key1")).toBeUndefined();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("should evict oldest entries when maxSize is reached", () => {
|
|
43
|
-
const cache = createCache<string>({ ttl: 10000, maxSize: 3 });
|
|
44
|
-
|
|
45
|
-
cache.set("key1", "value1");
|
|
46
|
-
cache.set("key2", "value2");
|
|
47
|
-
cache.set("key3", "value3");
|
|
48
|
-
cache.set("key4", "value4");
|
|
49
|
-
|
|
50
|
-
expect(cache.get("key1")).toBeUndefined();
|
|
51
|
-
expect(cache.get("key4")).toBe("value4");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("should calculate hit rate correctly", () => {
|
|
55
|
-
const cache = createCache<string>({ ttl: 1000, maxSize: 100 });
|
|
56
|
-
|
|
57
|
-
cache.set("key1", "value1");
|
|
58
|
-
cache.get("key1");
|
|
59
|
-
cache.get("key1");
|
|
60
|
-
cache.get("missing");
|
|
61
|
-
|
|
62
|
-
const stats = cache.getStats();
|
|
63
|
-
expect(stats.hitRate).toBeCloseTo(0.67, 1);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("should check if key exists", () => {
|
|
67
|
-
const cache = createCache<string>({ ttl: 1000, maxSize: 100 });
|
|
68
|
-
|
|
69
|
-
cache.set("key1", "value1");
|
|
70
|
-
expect(cache.has("key1")).toBe(true);
|
|
71
|
-
expect(cache.has("missing")).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("should delete entries", () => {
|
|
75
|
-
const cache = createCache<string>({ ttl: 1000, maxSize: 100 });
|
|
76
|
-
|
|
77
|
-
cache.set("key1", "value1");
|
|
78
|
-
expect(cache.delete("key1")).toBe(true);
|
|
79
|
-
expect(cache.get("key1")).toBeUndefined();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("should clear all entries", () => {
|
|
83
|
-
const cache = createCache<string>({ ttl: 1000, maxSize: 100 });
|
|
84
|
-
|
|
85
|
-
cache.set("key1", "value1");
|
|
86
|
-
cache.set("key2", "value2");
|
|
87
|
-
cache.clear();
|
|
88
|
-
|
|
89
|
-
expect(cache.getStats().size).toBe(0);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("should call onEvict callback", async () => {
|
|
93
|
-
const evicted: string[] = [];
|
|
94
|
-
const cache = createCache<string>({
|
|
95
|
-
ttl: 50,
|
|
96
|
-
maxSize: 100,
|
|
97
|
-
onEvict: (key) => evicted.push(key),
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
cache.set("key1", "value1");
|
|
101
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
102
|
-
cache.get("key1");
|
|
103
|
-
|
|
104
|
-
expect(evicted).toContain("key1");
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe("createNoopCache", () => {
|
|
109
|
-
test("should not store values", () => {
|
|
110
|
-
const cache = createNoopCache<string>();
|
|
111
|
-
|
|
112
|
-
cache.set("key1", "value1");
|
|
113
|
-
expect(cache.get("key1")).toBeUndefined();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("should always return false for has", () => {
|
|
117
|
-
const cache = createNoopCache<string>();
|
|
118
|
-
|
|
119
|
-
expect(cache.has("key1")).toBe(false);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("should return empty stats", () => {
|
|
123
|
-
const cache = createNoopCache<string>();
|
|
124
|
-
|
|
125
|
-
const stats = cache.getStats();
|
|
126
|
-
expect(stats.size).toBe(0);
|
|
127
|
-
expect(stats.hits).toBe(0);
|
|
128
|
-
expect(stats.misses).toBe(0);
|
|
129
|
-
});
|
|
130
|
-
});
|