@multiplier-labs/stepflow 0.3.1 → 0.3.4
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/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +17 -5
- package/.github/workflows/security.yml +29 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -0
- package/SECURITY.md +40 -0
- package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +60 -7
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
- package/dist/index.d.ts +15 -23
- package/dist/index.js +784 -547
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +4 -4
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +322 -56
- package/dist/storage/index.js +4 -2
- package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
- package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
- package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
- package/docs/stepflow-api-reference.md +5 -4
- package/package.json +12 -8
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +536 -522
- package/src/core/engine.ts +116 -235
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator/persistence.ts +165 -0
- package/src/core/orchestrator/plan-resolution.ts +140 -0
- package/src/core/orchestrator/state-transitions.ts +109 -0
- package/src/core/orchestrator/step-runner.ts +427 -0
- package/src/core/orchestrator.ts +57 -395
- package/src/core/types.ts +7 -29
- package/src/events/index.ts +4 -4
- package/src/events/memory.test.ts +39 -41
- package/src/events/memory.ts +7 -10
- package/src/events/socketio.test.ts +112 -162
- package/src/events/socketio.ts +20 -27
- package/src/events/types.ts +16 -19
- package/src/events/webhook.test.ts +489 -235
- package/src/events/webhook.ts +355 -139
- package/src/index.ts +19 -19
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +45 -152
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +20 -106
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +149 -150
- package/src/scheduler/cron.ts +58 -69
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +89 -105
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +40 -22
- package/src/scheduler/types.ts +4 -9
- package/src/storage/index.ts +5 -5
- package/src/storage/memory.test.ts +133 -183
- package/src/storage/memory.ts +21 -42
- package/src/storage/postgres-core.ts +550 -0
- package/src/storage/postgres-migration.test.ts +69 -0
- package/src/storage/postgres-transaction-pagination.test.ts +107 -0
- package/src/storage/postgres.ts +222 -937
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +330 -281
- package/src/storage/sqlite.ts +225 -170
- package/src/storage/types.ts +33 -58
- package/src/utils/errors.test.ts +78 -80
- package/src/utils/errors.ts +27 -57
- package/src/utils/id.ts +3 -3
- package/src/utils/index.ts +6 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +20 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- package/src/utils/safe-json.test.ts +166 -0
- package/src/utils/safe-json.ts +107 -0
- package/src/utils/webhook-signature.test.ts +111 -0
- package/src/utils/webhook-signature.ts +74 -0
- package/dist/chunk-FQCT3I2A.js.map +0 -1
- package/dist/chunk-GXY66V7Q.js.map +0 -1
- package/dist/chunk-U5QGN3RT.js.map +0 -1
- package/dist/chunk-XCF4PLN7.js.map +0 -1
- package/src/scheduler/postgres-persistence.test.ts +0 -906
package/src/index.ts
CHANGED
|
@@ -73,14 +73,14 @@ export {
|
|
|
73
73
|
type WorkflowStep,
|
|
74
74
|
type WorkflowHooks,
|
|
75
75
|
type WorkflowDefinition,
|
|
76
|
-
} from
|
|
76
|
+
} from './core/types';
|
|
77
77
|
|
|
78
78
|
export {
|
|
79
79
|
// Main Engine
|
|
80
80
|
WorkflowEngine,
|
|
81
81
|
type WorkflowEngineConfig,
|
|
82
82
|
type StartRunOptions,
|
|
83
|
-
} from
|
|
83
|
+
} from './core/engine';
|
|
84
84
|
|
|
85
85
|
// ============================================================================
|
|
86
86
|
// Storage
|
|
@@ -108,25 +108,25 @@ export {
|
|
|
108
108
|
type StepflowRunsTable,
|
|
109
109
|
type StepflowStepResultsTable,
|
|
110
110
|
type StepflowDatabase,
|
|
111
|
-
} from
|
|
111
|
+
} from './storage/types';
|
|
112
112
|
|
|
113
113
|
export {
|
|
114
114
|
// In-Memory Storage
|
|
115
115
|
MemoryStorageAdapter,
|
|
116
|
-
} from
|
|
116
|
+
} from './storage/memory';
|
|
117
117
|
|
|
118
118
|
export {
|
|
119
119
|
// SQLite Storage
|
|
120
120
|
SQLiteStorageAdapter,
|
|
121
121
|
type SQLiteStorageConfig,
|
|
122
|
-
} from
|
|
122
|
+
} from './storage/sqlite';
|
|
123
123
|
|
|
124
124
|
export {
|
|
125
125
|
// PostgreSQL Storage
|
|
126
126
|
PostgresStorageAdapter,
|
|
127
127
|
PostgresStorageAdapter as PostgresStorage,
|
|
128
128
|
type PostgresStorageConfig,
|
|
129
|
-
} from
|
|
129
|
+
} from './storage/postgres';
|
|
130
130
|
|
|
131
131
|
// ============================================================================
|
|
132
132
|
// Events
|
|
@@ -140,12 +140,12 @@ export {
|
|
|
140
140
|
type BuiltInEventType,
|
|
141
141
|
type EventCallback,
|
|
142
142
|
type Unsubscribe,
|
|
143
|
-
} from
|
|
143
|
+
} from './events/types';
|
|
144
144
|
|
|
145
145
|
export {
|
|
146
146
|
// In-Memory Event Transport
|
|
147
147
|
MemoryEventTransport,
|
|
148
|
-
} from
|
|
148
|
+
} from './events/memory';
|
|
149
149
|
|
|
150
150
|
export {
|
|
151
151
|
// Socket.IO Event Transport
|
|
@@ -154,7 +154,7 @@ export {
|
|
|
154
154
|
type SocketIOServer,
|
|
155
155
|
type SocketIOSocket,
|
|
156
156
|
type SocketIOAuthorizeFn,
|
|
157
|
-
} from
|
|
157
|
+
} from './events/socketio';
|
|
158
158
|
|
|
159
159
|
export {
|
|
160
160
|
// Webhook Event Transport
|
|
@@ -162,7 +162,7 @@ export {
|
|
|
162
162
|
type WebhookEventTransportConfig,
|
|
163
163
|
type WebhookEndpoint,
|
|
164
164
|
type WebhookPayload,
|
|
165
|
-
} from
|
|
165
|
+
} from './events/webhook';
|
|
166
166
|
|
|
167
167
|
// ============================================================================
|
|
168
168
|
// Scheduler
|
|
@@ -172,23 +172,23 @@ export {
|
|
|
172
172
|
type TriggerType,
|
|
173
173
|
type WorkflowSchedule,
|
|
174
174
|
type Scheduler,
|
|
175
|
-
} from
|
|
175
|
+
} from './scheduler/types';
|
|
176
176
|
|
|
177
177
|
export {
|
|
178
178
|
CronScheduler,
|
|
179
179
|
type CronSchedulerConfig,
|
|
180
180
|
type SchedulePersistence,
|
|
181
|
-
} from
|
|
181
|
+
} from './scheduler/cron';
|
|
182
182
|
|
|
183
183
|
export {
|
|
184
184
|
SQLiteSchedulePersistence,
|
|
185
185
|
type SQLiteSchedulePersistenceConfig,
|
|
186
|
-
} from
|
|
186
|
+
} from './scheduler/sqlite-persistence';
|
|
187
187
|
|
|
188
188
|
export {
|
|
189
189
|
PostgresSchedulePersistence,
|
|
190
190
|
type PostgresSchedulePersistenceConfig,
|
|
191
|
-
} from
|
|
191
|
+
} from './scheduler/postgres-persistence';
|
|
192
192
|
|
|
193
193
|
// ============================================================================
|
|
194
194
|
// Utilities
|
|
@@ -197,7 +197,7 @@ export {
|
|
|
197
197
|
export {
|
|
198
198
|
// ID Generation
|
|
199
199
|
generateId,
|
|
200
|
-
} from
|
|
200
|
+
} from './utils/id';
|
|
201
201
|
|
|
202
202
|
export {
|
|
203
203
|
// Errors
|
|
@@ -210,7 +210,7 @@ export {
|
|
|
210
210
|
WaitForRunTimeoutError,
|
|
211
211
|
WorkflowCanceledError,
|
|
212
212
|
WorkflowTimeoutError,
|
|
213
|
-
} from
|
|
213
|
+
} from './utils/errors';
|
|
214
214
|
|
|
215
215
|
export {
|
|
216
216
|
// Logger
|
|
@@ -219,7 +219,7 @@ export {
|
|
|
219
219
|
createScopedLogger,
|
|
220
220
|
sanitizeErrorForStorage,
|
|
221
221
|
type LogLevel,
|
|
222
|
-
} from
|
|
222
|
+
} from './utils/logger';
|
|
223
223
|
|
|
224
224
|
export {
|
|
225
225
|
// Retry
|
|
@@ -228,7 +228,7 @@ export {
|
|
|
228
228
|
sleep,
|
|
229
229
|
withRetry,
|
|
230
230
|
calculateRetryDelay,
|
|
231
|
-
} from
|
|
231
|
+
} from './utils/retry';
|
|
232
232
|
|
|
233
233
|
// ============================================================================
|
|
234
234
|
// Planning
|
|
@@ -267,4 +267,4 @@ export {
|
|
|
267
267
|
type CombinedRegistry,
|
|
268
268
|
RuleBasedPlanner,
|
|
269
269
|
type RuleBasedPlannerConfig,
|
|
270
|
-
} from
|
|
270
|
+
} from './planning';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
evaluateCondition,
|
|
4
|
+
getNestedValue,
|
|
5
|
+
MAX_REGEX_PATTERN_LENGTH,
|
|
6
|
+
} from './evaluate-condition';
|
|
7
|
+
|
|
8
|
+
describe('getNestedValue', () => {
|
|
9
|
+
it('returns top-level values', () => {
|
|
10
|
+
expect(getNestedValue({ a: 1 }, 'a')).toBe(1);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns nested values via dot notation', () => {
|
|
14
|
+
expect(getNestedValue({ a: { b: { c: 'deep' } } }, 'a.b.c')).toBe('deep');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns undefined for missing paths', () => {
|
|
18
|
+
expect(getNestedValue({ a: 1 }, 'b')).toBeUndefined();
|
|
19
|
+
expect(getNestedValue({ a: { b: 1 } }, 'a.c')).toBeUndefined();
|
|
20
|
+
expect(getNestedValue({ a: { b: 1 } }, 'a.b.c')).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns undefined when traversing through a non-object', () => {
|
|
24
|
+
expect(getNestedValue({ a: 5 }, 'a.b')).toBeUndefined();
|
|
25
|
+
expect(getNestedValue({ a: null }, 'a.b')).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('preserves falsy values (0, false, empty string)', () => {
|
|
29
|
+
expect(getNestedValue({ a: 0 }, 'a')).toBe(0);
|
|
30
|
+
expect(getNestedValue({ a: false }, 'a')).toBe(false);
|
|
31
|
+
expect(getNestedValue({ a: '' }, 'a')).toBe('');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('evaluateCondition', () => {
|
|
36
|
+
describe('eq / neq', () => {
|
|
37
|
+
it('uses strict equality', () => {
|
|
38
|
+
expect(evaluateCondition('eq', 1, 1)).toBe(true);
|
|
39
|
+
expect(evaluateCondition('eq', 1, '1')).toBe(false);
|
|
40
|
+
expect(evaluateCondition('eq', 'a', 'a')).toBe(true);
|
|
41
|
+
expect(evaluateCondition('neq', 1, 2)).toBe(true);
|
|
42
|
+
expect(evaluateCondition('neq', 'a', 'a')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('numeric comparisons', () => {
|
|
47
|
+
it('gt / gte / lt / lte work for numbers', () => {
|
|
48
|
+
expect(evaluateCondition('gt', 5, 3)).toBe(true);
|
|
49
|
+
expect(evaluateCondition('gt', 3, 5)).toBe(false);
|
|
50
|
+
expect(evaluateCondition('gt', 5, 5)).toBe(false);
|
|
51
|
+
expect(evaluateCondition('gte', 5, 5)).toBe(true);
|
|
52
|
+
expect(evaluateCondition('lt', 3, 5)).toBe(true);
|
|
53
|
+
expect(evaluateCondition('lt', 5, 5)).toBe(false);
|
|
54
|
+
expect(evaluateCondition('lte', 5, 5)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns false when either side is not a number', () => {
|
|
58
|
+
expect(evaluateCondition('gt', '5', 3)).toBe(false);
|
|
59
|
+
expect(evaluateCondition('gt', 5, '3')).toBe(false);
|
|
60
|
+
expect(evaluateCondition('gte', null, 0)).toBe(false);
|
|
61
|
+
expect(evaluateCondition('lt', undefined, 1)).toBe(false);
|
|
62
|
+
expect(evaluateCondition('lte', true, 0)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('contains', () => {
|
|
67
|
+
it('matches substrings in strings', () => {
|
|
68
|
+
expect(evaluateCondition('contains', 'hello world', 'world')).toBe(true);
|
|
69
|
+
expect(evaluateCondition('contains', 'hello world', 'xyz')).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('matches elements in arrays', () => {
|
|
73
|
+
expect(evaluateCondition('contains', [1, 2, 3], 2)).toBe(true);
|
|
74
|
+
expect(evaluateCondition('contains', ['a', 'b'], 'c')).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns false for unsupported types', () => {
|
|
78
|
+
expect(evaluateCondition('contains', 42, 4)).toBe(false);
|
|
79
|
+
expect(evaluateCondition('contains', { a: 1 }, 'a')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('matches', () => {
|
|
84
|
+
it('returns true for matching regex', () => {
|
|
85
|
+
expect(evaluateCondition('matches', 'hello world', '^hello')).toBe(true);
|
|
86
|
+
expect(evaluateCondition('matches', 'foo123', '\\d+')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns false for non-matching regex', () => {
|
|
90
|
+
expect(evaluateCondition('matches', 'hello', '^world')).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns false on invalid regex (does not throw)', () => {
|
|
94
|
+
expect(evaluateCondition('matches', 'hello', '[invalid')).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns false when either side is not a string', () => {
|
|
98
|
+
expect(evaluateCondition('matches', 123, '\\d+')).toBe(false);
|
|
99
|
+
expect(evaluateCondition('matches', 'abc', /abc/)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('rejects unsafe RE2-incompatible patterns rather than throwing', () => {
|
|
103
|
+
// Backreferences are unsupported in RE2 — verify we degrade gracefully.
|
|
104
|
+
expect(evaluateCondition('matches', 'aa', '(a)\\1')).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rejects patterns longer than MAX_REGEX_PATTERN_LENGTH without compiling', () => {
|
|
108
|
+
// A pattern equal to the limit should still compile and match.
|
|
109
|
+
const atLimit = 'a'.repeat(MAX_REGEX_PATTERN_LENGTH);
|
|
110
|
+
expect(evaluateCondition('matches', atLimit, atLimit)).toBe(true);
|
|
111
|
+
|
|
112
|
+
// One character over the limit should be rejected (returns false, no throw).
|
|
113
|
+
const overLimit = 'a'.repeat(MAX_REGEX_PATTERN_LENGTH + 1);
|
|
114
|
+
expect(evaluateCondition('matches', overLimit, overLimit)).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('exists / notExists', () => {
|
|
119
|
+
it('exists is true for any non-empty value', () => {
|
|
120
|
+
expect(evaluateCondition('exists', 'value', undefined)).toBe(true);
|
|
121
|
+
expect(evaluateCondition('exists', 0, undefined)).toBe(true);
|
|
122
|
+
expect(evaluateCondition('exists', false, undefined)).toBe(true);
|
|
123
|
+
expect(evaluateCondition('exists', [], undefined)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('exists is false for undefined, null, or empty string', () => {
|
|
127
|
+
expect(evaluateCondition('exists', undefined, undefined)).toBe(false);
|
|
128
|
+
expect(evaluateCondition('exists', null, undefined)).toBe(false);
|
|
129
|
+
expect(evaluateCondition('exists', '', undefined)).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('notExists mirrors exists', () => {
|
|
133
|
+
expect(evaluateCondition('notExists', undefined, undefined)).toBe(true);
|
|
134
|
+
expect(evaluateCondition('notExists', null, undefined)).toBe(true);
|
|
135
|
+
expect(evaluateCondition('notExists', '', undefined)).toBe(true);
|
|
136
|
+
expect(evaluateCondition('notExists', 'value', undefined)).toBe(false);
|
|
137
|
+
expect(evaluateCondition('notExists', 0, undefined)).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('unknown operator', () => {
|
|
142
|
+
it('returns false for an unrecognized operator', () => {
|
|
143
|
+
// Cast through unknown to test the default branch.
|
|
144
|
+
expect(
|
|
145
|
+
evaluateCondition(
|
|
146
|
+
'bogus' as unknown as Parameters<typeof evaluateCondition>[0],
|
|
147
|
+
1,
|
|
148
|
+
1
|
|
149
|
+
)
|
|
150
|
+
).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared condition evaluation helpers used by the planner and registry.
|
|
3
|
+
*
|
|
4
|
+
* Extracted to a single module so the planner's recipe scoring and the
|
|
5
|
+
* registry's `query({ matchConditions })` filter cannot drift apart.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import RE2 from 're2';
|
|
9
|
+
import type { ConditionOperator } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maximum length (in characters) accepted for a `matches` regex pattern.
|
|
13
|
+
*
|
|
14
|
+
* Recipe conditions can come from user-supplied configuration; even with RE2's
|
|
15
|
+
* linear-time guarantee, very large patterns waste CPU/memory at compile time
|
|
16
|
+
* and inflate the per-evaluation cost. Cap the source length so a pathological
|
|
17
|
+
* recipe cannot hand the planner a megabyte of regex.
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_REGEX_PATTERN_LENGTH = 512;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get a nested value from an object using dot notation.
|
|
23
|
+
*/
|
|
24
|
+
export function getNestedValue(
|
|
25
|
+
obj: Record<string, unknown>,
|
|
26
|
+
path: string
|
|
27
|
+
): unknown {
|
|
28
|
+
return path.split('.').reduce<unknown>((current, key) => {
|
|
29
|
+
if (current && typeof current === 'object' && key in current) {
|
|
30
|
+
return (current as Record<string, unknown>)[key];
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}, obj);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Evaluate a single condition against a value.
|
|
38
|
+
*
|
|
39
|
+
* The `matches` operator uses RE2 (not the JS RegExp engine) to avoid
|
|
40
|
+
* catastrophic-backtracking DoS from untrusted recipe inputs.
|
|
41
|
+
*/
|
|
42
|
+
export function evaluateCondition(
|
|
43
|
+
operator: ConditionOperator,
|
|
44
|
+
fieldValue: unknown,
|
|
45
|
+
conditionValue: unknown
|
|
46
|
+
): boolean {
|
|
47
|
+
switch (operator) {
|
|
48
|
+
case 'eq':
|
|
49
|
+
return fieldValue === conditionValue;
|
|
50
|
+
|
|
51
|
+
case 'neq':
|
|
52
|
+
return fieldValue !== conditionValue;
|
|
53
|
+
|
|
54
|
+
case 'gt':
|
|
55
|
+
return typeof fieldValue === 'number' && typeof conditionValue === 'number'
|
|
56
|
+
? fieldValue > conditionValue
|
|
57
|
+
: false;
|
|
58
|
+
|
|
59
|
+
case 'gte':
|
|
60
|
+
return typeof fieldValue === 'number' && typeof conditionValue === 'number'
|
|
61
|
+
? fieldValue >= conditionValue
|
|
62
|
+
: false;
|
|
63
|
+
|
|
64
|
+
case 'lt':
|
|
65
|
+
return typeof fieldValue === 'number' && typeof conditionValue === 'number'
|
|
66
|
+
? fieldValue < conditionValue
|
|
67
|
+
: false;
|
|
68
|
+
|
|
69
|
+
case 'lte':
|
|
70
|
+
return typeof fieldValue === 'number' && typeof conditionValue === 'number'
|
|
71
|
+
? fieldValue <= conditionValue
|
|
72
|
+
: false;
|
|
73
|
+
|
|
74
|
+
case 'contains':
|
|
75
|
+
if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
|
|
76
|
+
return fieldValue.includes(conditionValue);
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(fieldValue)) {
|
|
79
|
+
return fieldValue.includes(conditionValue);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
|
|
83
|
+
case 'matches':
|
|
84
|
+
if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
|
|
85
|
+
if (conditionValue.length > MAX_REGEX_PATTERN_LENGTH) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return new RE2(conditionValue).test(fieldValue);
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
|
|
96
|
+
case 'exists':
|
|
97
|
+
return fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
|
|
98
|
+
|
|
99
|
+
case 'notExists':
|
|
100
|
+
return fieldValue === undefined || fieldValue === null || fieldValue === '';
|
|
101
|
+
|
|
102
|
+
default:
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/planning/index.ts
CHANGED
|
@@ -39,7 +39,7 @@ export type {
|
|
|
39
39
|
StepHandlerRegistry,
|
|
40
40
|
RecipeQueryOptions,
|
|
41
41
|
RecipeRegistry,
|
|
42
|
-
} from
|
|
42
|
+
} from './types';
|
|
43
43
|
|
|
44
44
|
// Implementations
|
|
45
45
|
export {
|
|
@@ -47,6 +47,9 @@ export {
|
|
|
47
47
|
MemoryRecipeRegistry,
|
|
48
48
|
createRegistry,
|
|
49
49
|
type CombinedRegistry,
|
|
50
|
-
} from
|
|
50
|
+
} from './registry';
|
|
51
51
|
|
|
52
|
-
export {
|
|
52
|
+
export {
|
|
53
|
+
RuleBasedPlanner,
|
|
54
|
+
type RuleBasedPlannerConfig,
|
|
55
|
+
} from './planner';
|