@bernierllc/nevar-types 0.0.1 → 0.1.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/.eslintrc.js +29 -0
- package/LICENSE +5 -0
- package/README.md +128 -43
- package/__tests__/errors.test.ts +189 -0
- package/__tests__/type-guards.test.ts +152 -0
- package/dist/actions.d.ts +71 -0
- package/dist/actions.js +9 -0
- package/dist/conditions.d.ts +24 -0
- package/dist/conditions.js +35 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +9 -0
- package/dist/context.d.ts +8 -0
- package/dist/context.js +9 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +95 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +23 -0
- package/dist/models.d.ts +56 -0
- package/dist/models.js +9 -0
- package/dist/operators.d.ts +24 -0
- package/dist/operators.js +9 -0
- package/dist/results.d.ts +79 -0
- package/dist/results.js +9 -0
- package/dist/storage.d.ts +100 -0
- package/dist/storage.js +9 -0
- package/dist/triggers.d.ts +11 -0
- package/dist/triggers.js +9 -0
- package/jest.config.cjs +29 -0
- package/package.json +35 -7
- package/proof-coverage.json +41 -0
- package/src/actions.ts +91 -0
- package/src/conditions.ts +55 -0
- package/src/config.ts +78 -0
- package/src/context.ts +16 -0
- package/src/errors.ts +102 -0
- package/src/index.ts +79 -0
- package/src/models.ts +68 -0
- package/src/operators.ts +33 -0
- package/src/results.ts +91 -0
- package/src/storage.ts +118 -0
- package/src/triggers.ts +19 -0
- package/tsconfig.json +22 -0
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
parser: '@typescript-eslint/parser',
|
|
11
|
+
parserOptions: {
|
|
12
|
+
ecmaVersion: 2022,
|
|
13
|
+
sourceType: 'module'
|
|
14
|
+
},
|
|
15
|
+
extends: [
|
|
16
|
+
'eslint:recommended',
|
|
17
|
+
'plugin:@typescript-eslint/recommended'
|
|
18
|
+
],
|
|
19
|
+
plugins: ['@typescript-eslint'],
|
|
20
|
+
env: {
|
|
21
|
+
node: true,
|
|
22
|
+
jest: true
|
|
23
|
+
},
|
|
24
|
+
rules: {
|
|
25
|
+
'@typescript-eslint/explicit-function-return-type': 'warn',
|
|
26
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
27
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]
|
|
28
|
+
}
|
|
29
|
+
};
|
package/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (c) 2025 Bernier LLC
|
|
2
|
+
|
|
3
|
+
This software is licensed to the client under a limited-use license.
|
|
4
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
5
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
package/README.md
CHANGED
|
@@ -1,45 +1,130 @@
|
|
|
1
1
|
# @bernierllc/nevar-types
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
This
|
|
8
|
-
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
3
|
+
Shared type definitions, interfaces, error classes, and type guards for the Nevar rules engine suite.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides the foundational types that all Nevar packages depend on. It has zero runtime dependencies — it exports only TypeScript types, interfaces, error classes, and type guard functions.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @bernierllc/nevar-types
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import type {
|
|
19
|
+
Rule,
|
|
20
|
+
ConditionGroup,
|
|
21
|
+
ActionDefinition,
|
|
22
|
+
NevarConfig,
|
|
23
|
+
EvaluationResult,
|
|
24
|
+
NevarStorageAdapter,
|
|
25
|
+
} from '@bernierllc/nevar-types';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
isConditionGroup,
|
|
29
|
+
isCondition,
|
|
30
|
+
NevarError,
|
|
31
|
+
NevarEvaluationError,
|
|
32
|
+
} from '@bernierllc/nevar-types';
|
|
33
|
+
|
|
34
|
+
// Type guard usage
|
|
35
|
+
const node: ConditionGroup | Condition = getNode();
|
|
36
|
+
if (isConditionGroup(node)) {
|
|
37
|
+
console.log(node.operator, node.conditions);
|
|
38
|
+
} else if (isCondition(node)) {
|
|
39
|
+
console.log(node.field, node.op, node.value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Error handling with cause chain
|
|
43
|
+
try {
|
|
44
|
+
evaluateRule(rule, context);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof NevarEvaluationError) {
|
|
47
|
+
console.log(error.code, error.context, error.cause);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### Types and Interfaces
|
|
55
|
+
|
|
56
|
+
- **`Rule`** - Complete rule definition with trigger type, condition tree, actions, and execution settings
|
|
57
|
+
- **`RuleGroup`** - Named collection of rules
|
|
58
|
+
- **`ConditionGroup`** - Tree node with `operator` (AND/OR/NOT) and nested `conditions`
|
|
59
|
+
- **`Condition`** - Leaf node with `field`, `op`, and `value`
|
|
60
|
+
- **`ActionDefinition`** - Action type, config, retry settings, and execution order
|
|
61
|
+
- **`ActionIntent`** - Resolved action ready for execution, tied to a specific rule
|
|
62
|
+
- **`ActionResult`** - Outcome of executing an action intent
|
|
63
|
+
- **`TriggerDefinition`** - Trigger type definition with payload schema
|
|
64
|
+
- **`OperatorDefinition`** - Operator with evaluate function, label, category, and field types
|
|
65
|
+
- **`NevarStorageAdapter`** - Storage interface for rules, groups, and execution logs
|
|
66
|
+
- **`NevarConfig`** - Full engine configuration including circuit breaker, loop, and audit settings
|
|
67
|
+
- **`EvaluationResult`** - Match result with evaluation trace
|
|
68
|
+
- **`LoopHandle`** - Runtime loop state with heartbeat tracking
|
|
69
|
+
|
|
70
|
+
### Error Classes
|
|
71
|
+
|
|
72
|
+
All errors extend `NevarError` and support ES2022 `Error.cause` chaining via `NevarErrorOptions`:
|
|
73
|
+
|
|
74
|
+
- **`NevarError`** - Base error with `code: string` and `context?: Record<string, unknown>`
|
|
75
|
+
- **`NevarTriggerError`** - Trigger validation and dispatch failures
|
|
76
|
+
- **`NevarEvaluationError`** - Condition evaluation failures (unknown operators, provider errors)
|
|
77
|
+
- **`NevarActionError`** - Action execution failures
|
|
78
|
+
- **`NevarCircuitBreakerError`** - Circuit breaker limit violations
|
|
79
|
+
- **`NevarLoopError`** - Loop detection and control failures
|
|
80
|
+
- **`NevarStorageError`** - Storage adapter failures
|
|
81
|
+
- **`NevarValidationError`** - Schema and configuration validation failures
|
|
82
|
+
|
|
83
|
+
### Type Guards
|
|
84
|
+
|
|
85
|
+
- **`isConditionGroup(value)`** - Returns `true` if value has `operator` and `conditions` fields
|
|
86
|
+
- **`isCondition(value)`** - Returns `true` if value has `field`, `op`, and `value` fields
|
|
87
|
+
|
|
88
|
+
## Exports
|
|
89
|
+
|
|
90
|
+
### Types & Interfaces
|
|
91
|
+
|
|
92
|
+
- **Models**: `Rule`, `RuleGroup`, `LogLevel`, `EvaluationTrace`
|
|
93
|
+
- **Conditions**: `ConditionGroup`, `Condition`
|
|
94
|
+
- **Actions**: `ActionDefinition`, `ActionRetryConfig`, `ActionIntent`, `ActionResult`, `MatchedRule`, `DeferredTrigger`, `ActionFailureMode`
|
|
95
|
+
- **Triggers**: `TriggerDefinition`
|
|
96
|
+
- **Context**: `ContextProviderDefinition`
|
|
97
|
+
- **Operators**: `OperatorDefinition`, `OperatorInfo`
|
|
98
|
+
- **Storage**: `NevarStorageAdapter`, `GroupFilters`, `RuleFilters`, `LogFilters`, `PaginatedResult`, `ExecutionLogEntry`
|
|
99
|
+
- **Config**: `NevarConfig`, `CircuitBreakerConfig`, `LoopConfig`, `AuditLoggerConfig`, `SeedStrategy`
|
|
100
|
+
- **Results**: `EmitResult`, `PreviewResult`, `EvaluationResult`, `LoopHandle`, `LoopAnalysis`, `DetectedLoop`
|
|
101
|
+
|
|
102
|
+
### Error Classes
|
|
103
|
+
|
|
104
|
+
All errors extend `NevarError` and follow the ES2022 `Error.cause` pattern:
|
|
105
|
+
|
|
106
|
+
- `NevarError` (base)
|
|
107
|
+
- `NevarTriggerError`
|
|
108
|
+
- `NevarEvaluationError`
|
|
109
|
+
- `NevarActionError`
|
|
110
|
+
- `NevarCircuitBreakerError`
|
|
111
|
+
- `NevarLoopError`
|
|
112
|
+
- `NevarStorageError`
|
|
113
|
+
- `NevarValidationError`
|
|
114
|
+
|
|
115
|
+
### Type Guards
|
|
116
|
+
|
|
117
|
+
- `isConditionGroup(value)` — checks for `operator` and `conditions` fields
|
|
118
|
+
- `isCondition(value)` — checks for `field`, `op`, `value` fields
|
|
119
|
+
|
|
120
|
+
## Integration Documentation
|
|
121
|
+
|
|
122
|
+
### Logger Integration
|
|
123
|
+
This package does not integrate with `@bernierllc/logger`. As a core package, logger integration is optional and not included by default. Consumers should handle logging at the service layer.
|
|
124
|
+
|
|
125
|
+
### NeverHub Integration
|
|
126
|
+
This package does not integrate with `@bernierllc/neverhub-adapter`. As a core package, NeverHub integration is not applicable. NeverHub registration should be handled by service-layer packages that compose this package.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
Copyright (c) 2025 Bernier LLC. All rights reserved.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
NevarError,
|
|
11
|
+
NevarTriggerError,
|
|
12
|
+
NevarEvaluationError,
|
|
13
|
+
NevarActionError,
|
|
14
|
+
NevarCircuitBreakerError,
|
|
15
|
+
NevarLoopError,
|
|
16
|
+
NevarStorageError,
|
|
17
|
+
NevarValidationError,
|
|
18
|
+
} from '../src/errors';
|
|
19
|
+
|
|
20
|
+
describe('NevarError', () => {
|
|
21
|
+
it('should create with message only', () => {
|
|
22
|
+
const error = new NevarError('something went wrong');
|
|
23
|
+
expect(error.message).toBe('something went wrong');
|
|
24
|
+
expect(error.code).toBe('NEVAR_ERROR');
|
|
25
|
+
expect(error.name).toBe('NevarError');
|
|
26
|
+
expect(error.context).toBeUndefined();
|
|
27
|
+
expect(error.cause).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should create with custom code and context', () => {
|
|
31
|
+
const error = new NevarError('fail', {
|
|
32
|
+
code: 'CUSTOM_CODE',
|
|
33
|
+
context: { ruleId: 'r-1' },
|
|
34
|
+
});
|
|
35
|
+
expect(error.code).toBe('CUSTOM_CODE');
|
|
36
|
+
expect(error.context).toEqual({ ruleId: 'r-1' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should chain cause using ES2022 Error.cause', () => {
|
|
40
|
+
const cause = new Error('root cause');
|
|
41
|
+
const error = new NevarError('wrapper', { cause });
|
|
42
|
+
expect(error.cause).toBe(cause);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should be instanceof Error', () => {
|
|
46
|
+
const error = new NevarError('test');
|
|
47
|
+
expect(error).toBeInstanceOf(Error);
|
|
48
|
+
expect(error).toBeInstanceOf(NevarError);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('NevarTriggerError', () => {
|
|
53
|
+
it('should have correct defaults', () => {
|
|
54
|
+
const error = new NevarTriggerError('trigger failed');
|
|
55
|
+
expect(error.name).toBe('NevarTriggerError');
|
|
56
|
+
expect(error.code).toBe('NEVAR_TRIGGER_ERROR');
|
|
57
|
+
expect(error.message).toBe('trigger failed');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should be instanceof NevarError and Error', () => {
|
|
61
|
+
const error = new NevarTriggerError('test');
|
|
62
|
+
expect(error).toBeInstanceOf(NevarError);
|
|
63
|
+
expect(error).toBeInstanceOf(Error);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should support cause chaining', () => {
|
|
67
|
+
const cause = new Error('underlying');
|
|
68
|
+
const error = new NevarTriggerError('trigger failed', { cause });
|
|
69
|
+
expect(error.cause).toBe(cause);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should support custom code override', () => {
|
|
73
|
+
const error = new NevarTriggerError('fail', { code: 'CUSTOM' });
|
|
74
|
+
expect(error.code).toBe('CUSTOM');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should support context', () => {
|
|
78
|
+
const error = new NevarTriggerError('fail', { context: { triggerKey: 'order.created' } });
|
|
79
|
+
expect(error.context).toEqual({ triggerKey: 'order.created' });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('NevarEvaluationError', () => {
|
|
84
|
+
it('should have correct defaults', () => {
|
|
85
|
+
const error = new NevarEvaluationError('eval failed');
|
|
86
|
+
expect(error.name).toBe('NevarEvaluationError');
|
|
87
|
+
expect(error.code).toBe('NEVAR_EVALUATION_ERROR');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should be instanceof NevarError', () => {
|
|
91
|
+
expect(new NevarEvaluationError('test')).toBeInstanceOf(NevarError);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should chain cause', () => {
|
|
95
|
+
const cause = new Error('root');
|
|
96
|
+
const error = new NevarEvaluationError('fail', { cause, context: { ruleId: 'r-1' } });
|
|
97
|
+
expect(error.cause).toBe(cause);
|
|
98
|
+
expect(error.context).toEqual({ ruleId: 'r-1' });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('NevarActionError', () => {
|
|
103
|
+
it('should have correct defaults', () => {
|
|
104
|
+
const error = new NevarActionError('action failed');
|
|
105
|
+
expect(error.name).toBe('NevarActionError');
|
|
106
|
+
expect(error.code).toBe('NEVAR_ACTION_ERROR');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should be instanceof NevarError', () => {
|
|
110
|
+
expect(new NevarActionError('test')).toBeInstanceOf(NevarError);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should support all options', () => {
|
|
114
|
+
const cause = new Error('http timeout');
|
|
115
|
+
const error = new NevarActionError('send email failed', {
|
|
116
|
+
cause,
|
|
117
|
+
code: 'ACTION_TIMEOUT',
|
|
118
|
+
context: { actionType: 'send-email' },
|
|
119
|
+
});
|
|
120
|
+
expect(error.cause).toBe(cause);
|
|
121
|
+
expect(error.code).toBe('ACTION_TIMEOUT');
|
|
122
|
+
expect(error.context).toEqual({ actionType: 'send-email' });
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('NevarCircuitBreakerError', () => {
|
|
127
|
+
it('should have correct defaults', () => {
|
|
128
|
+
const error = new NevarCircuitBreakerError('circuit open');
|
|
129
|
+
expect(error.name).toBe('NevarCircuitBreakerError');
|
|
130
|
+
expect(error.code).toBe('NEVAR_CIRCUIT_BREAKER_ERROR');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should be instanceof NevarError', () => {
|
|
134
|
+
expect(new NevarCircuitBreakerError('test')).toBeInstanceOf(NevarError);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('NevarLoopError', () => {
|
|
139
|
+
it('should have correct defaults', () => {
|
|
140
|
+
const error = new NevarLoopError('loop detected');
|
|
141
|
+
expect(error.name).toBe('NevarLoopError');
|
|
142
|
+
expect(error.code).toBe('NEVAR_LOOP_ERROR');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should be instanceof NevarError', () => {
|
|
146
|
+
expect(new NevarLoopError('test')).toBeInstanceOf(NevarError);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should carry loop context', () => {
|
|
150
|
+
const error = new NevarLoopError('loop detected at depth 3', {
|
|
151
|
+
context: { depth: 3, triggerKey: 'order.updated', ruleIds: ['r-1', 'r-2'] },
|
|
152
|
+
});
|
|
153
|
+
expect(error.context).toEqual({ depth: 3, triggerKey: 'order.updated', ruleIds: ['r-1', 'r-2'] });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('NevarStorageError', () => {
|
|
158
|
+
it('should have correct defaults', () => {
|
|
159
|
+
const error = new NevarStorageError('db connection failed');
|
|
160
|
+
expect(error.name).toBe('NevarStorageError');
|
|
161
|
+
expect(error.code).toBe('NEVAR_STORAGE_ERROR');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should be instanceof NevarError', () => {
|
|
165
|
+
expect(new NevarStorageError('test')).toBeInstanceOf(NevarError);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('NevarValidationError', () => {
|
|
170
|
+
it('should have correct defaults', () => {
|
|
171
|
+
const error = new NevarValidationError('invalid rule');
|
|
172
|
+
expect(error.name).toBe('NevarValidationError');
|
|
173
|
+
expect(error.code).toBe('NEVAR_VALIDATION_ERROR');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should be instanceof NevarError', () => {
|
|
177
|
+
expect(new NevarValidationError('test')).toBeInstanceOf(NevarError);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should support all options', () => {
|
|
181
|
+
const cause = new TypeError('expected string');
|
|
182
|
+
const error = new NevarValidationError('rule name is required', {
|
|
183
|
+
cause,
|
|
184
|
+
context: { field: 'name', received: undefined },
|
|
185
|
+
});
|
|
186
|
+
expect(error.cause).toBe(cause);
|
|
187
|
+
expect(error.context).toEqual({ field: 'name', received: undefined });
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isConditionGroup, isCondition } from '../src/conditions';
|
|
10
|
+
|
|
11
|
+
describe('isConditionGroup', () => {
|
|
12
|
+
it('should return true for a valid AND group', () => {
|
|
13
|
+
expect(isConditionGroup({
|
|
14
|
+
operator: 'AND',
|
|
15
|
+
conditions: [{ field: 'age', op: 'gt', value: 18 }],
|
|
16
|
+
})).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should return true for a valid OR group', () => {
|
|
20
|
+
expect(isConditionGroup({
|
|
21
|
+
operator: 'OR',
|
|
22
|
+
conditions: [],
|
|
23
|
+
})).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return true for a valid NOT group', () => {
|
|
27
|
+
expect(isConditionGroup({
|
|
28
|
+
operator: 'NOT',
|
|
29
|
+
conditions: [{ field: 'status', op: 'eq', value: 'banned' }],
|
|
30
|
+
})).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return true for nested groups', () => {
|
|
34
|
+
expect(isConditionGroup({
|
|
35
|
+
operator: 'AND',
|
|
36
|
+
conditions: [
|
|
37
|
+
{ operator: 'OR', conditions: [] },
|
|
38
|
+
{ field: 'x', op: 'eq', value: 1 },
|
|
39
|
+
],
|
|
40
|
+
})).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return false for null', () => {
|
|
44
|
+
expect(isConditionGroup(null)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return false for undefined', () => {
|
|
48
|
+
expect(isConditionGroup(undefined)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return false for a string', () => {
|
|
52
|
+
expect(isConditionGroup('AND')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return false for a number', () => {
|
|
56
|
+
expect(isConditionGroup(42)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return false when operator is missing', () => {
|
|
60
|
+
expect(isConditionGroup({ conditions: [] })).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return false when conditions is missing', () => {
|
|
64
|
+
expect(isConditionGroup({ operator: 'AND' })).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return false when operator is not AND/OR/NOT', () => {
|
|
68
|
+
expect(isConditionGroup({ operator: 'XOR', conditions: [] })).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return false when conditions is not an array', () => {
|
|
72
|
+
expect(isConditionGroup({ operator: 'AND', conditions: 'not-array' })).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return false for an empty object', () => {
|
|
76
|
+
expect(isConditionGroup({})).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return false for a Condition (not a group)', () => {
|
|
80
|
+
expect(isConditionGroup({ field: 'age', op: 'gt', value: 18 })).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('isCondition', () => {
|
|
85
|
+
it('should return true for a valid condition', () => {
|
|
86
|
+
expect(isCondition({ field: 'age', op: 'gt', value: 18 })).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return true when value is null', () => {
|
|
90
|
+
expect(isCondition({ field: 'name', op: 'is_null', value: null })).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return true when value is undefined', () => {
|
|
94
|
+
expect(isCondition({ field: 'name', op: 'is_undefined', value: undefined })).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return true when value is 0', () => {
|
|
98
|
+
expect(isCondition({ field: 'count', op: 'eq', value: 0 })).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return true when value is false', () => {
|
|
102
|
+
expect(isCondition({ field: 'active', op: 'eq', value: false })).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should return true when value is empty string', () => {
|
|
106
|
+
expect(isCondition({ field: 'name', op: 'eq', value: '' })).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return false for null', () => {
|
|
110
|
+
expect(isCondition(null)).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should return false for undefined', () => {
|
|
114
|
+
expect(isCondition(undefined)).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return false for a string', () => {
|
|
118
|
+
expect(isCondition('field:age')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should return false for a number', () => {
|
|
122
|
+
expect(isCondition(42)).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return false when field is missing', () => {
|
|
126
|
+
expect(isCondition({ op: 'gt', value: 18 })).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return false when op is missing', () => {
|
|
130
|
+
expect(isCondition({ field: 'age', value: 18 })).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should return false when value key is missing', () => {
|
|
134
|
+
expect(isCondition({ field: 'age', op: 'gt' })).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should return false when field is not a string', () => {
|
|
138
|
+
expect(isCondition({ field: 123, op: 'gt', value: 18 })).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return false when op is not a string', () => {
|
|
142
|
+
expect(isCondition({ field: 'age', op: 123, value: 18 })).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should return false for an empty object', () => {
|
|
146
|
+
expect(isCondition({})).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return false for a ConditionGroup', () => {
|
|
150
|
+
expect(isCondition({ operator: 'AND', conditions: [] })).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for retrying a failed action with exponential backoff.
|
|
3
|
+
*/
|
|
4
|
+
export interface ActionRetryConfig {
|
|
5
|
+
maxAttempts: number;
|
|
6
|
+
backoffMs: number;
|
|
7
|
+
backoffMultiplier: number;
|
|
8
|
+
maxBackoffMs: number;
|
|
9
|
+
retryableErrors?: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Defines an action to execute when a rule matches.
|
|
13
|
+
*/
|
|
14
|
+
export interface ActionDefinition {
|
|
15
|
+
actionType: string;
|
|
16
|
+
config: Record<string, unknown>;
|
|
17
|
+
executionOrder: number;
|
|
18
|
+
retry?: ActionRetryConfig;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* An intent represents a planned action derived from a matched rule.
|
|
22
|
+
*/
|
|
23
|
+
export interface ActionIntent {
|
|
24
|
+
actionType: string;
|
|
25
|
+
config: Record<string, unknown>;
|
|
26
|
+
ruleId: string;
|
|
27
|
+
ruleName: string;
|
|
28
|
+
retry?: ActionRetryConfig;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A trigger that should be fired as a result of an action execution.
|
|
32
|
+
*/
|
|
33
|
+
export interface DeferredTrigger {
|
|
34
|
+
triggerKey: string;
|
|
35
|
+
payload: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The result of executing a single action.
|
|
39
|
+
*/
|
|
40
|
+
export interface ActionResult {
|
|
41
|
+
intent: ActionIntent;
|
|
42
|
+
status: 'success' | 'error';
|
|
43
|
+
output: Record<string, unknown>;
|
|
44
|
+
deferredTriggers?: DeferredTrigger[];
|
|
45
|
+
durationMs: number;
|
|
46
|
+
attempts: number;
|
|
47
|
+
lastError?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Controls how action failures are handled within a rule's action list.
|
|
51
|
+
*/
|
|
52
|
+
export type ActionFailureMode = 'fail-fast' | 'best-effort';
|
|
53
|
+
/**
|
|
54
|
+
* Defines an action handler that can be registered with the engine.
|
|
55
|
+
* Includes the handler function, metadata for discovery, and a config schema.
|
|
56
|
+
*/
|
|
57
|
+
export interface ActionHandlerDefinition {
|
|
58
|
+
label: string;
|
|
59
|
+
category: string;
|
|
60
|
+
canDefer: boolean;
|
|
61
|
+
configSchema: unknown;
|
|
62
|
+
handler: (config: Record<string, unknown>, context: Record<string, unknown>, priorResults: ActionResult[]) => Promise<Record<string, unknown>>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The aggregate result of executing a set of action intents.
|
|
66
|
+
*/
|
|
67
|
+
export interface ExecutionResult {
|
|
68
|
+
results: ActionResult[];
|
|
69
|
+
deferredTriggers: DeferredTrigger[];
|
|
70
|
+
durationMs: number;
|
|
71
|
+
}
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A group of conditions combined with a logical operator.
|
|
3
|
+
* NOT groups evaluate children as AND, then negate the result.
|
|
4
|
+
*/
|
|
5
|
+
export interface ConditionGroup {
|
|
6
|
+
operator: 'AND' | 'OR' | 'NOT';
|
|
7
|
+
conditions: (ConditionGroup | Condition)[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A single condition that compares a field value using an operator.
|
|
11
|
+
*/
|
|
12
|
+
export interface Condition {
|
|
13
|
+
field: string;
|
|
14
|
+
op: string;
|
|
15
|
+
value: unknown;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Type guard: checks whether the given value is a ConditionGroup.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isConditionGroup(value: unknown): value is ConditionGroup;
|
|
21
|
+
/**
|
|
22
|
+
* Type guard: checks whether the given value is a Condition.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isCondition(value: unknown): value is Condition;
|