@startsimpli/funnels 0.4.6 → 0.4.8
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 +1 -1
- package/src/api/client.test.ts +76 -76
- package/src/api/client.ts +16 -16
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +24 -24
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +39 -39
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +1 -1
- package/src/components/FilterRuleEditor/RuleRow.tsx +8 -8
- package/src/components/FunnelCard/FunnelCard.stories.tsx +118 -118
- package/src/components/FunnelCard/FunnelCard.test.ts +48 -48
- package/src/components/FunnelCard/FunnelCard.test.tsx +40 -40
- package/src/components/FunnelCard/FunnelCard.tsx +3 -3
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +63 -63
- package/src/components/FunnelPreview/FunnelPreview.tsx +11 -11
- package/src/components/FunnelPreview/StageBreakdown.tsx +5 -5
- package/src/components/FunnelPreview/example.tsx +43 -43
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +70 -70
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +63 -63
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +12 -12
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +5 -5
- package/src/components/FunnelRunHistory/RunFilters.tsx +8 -8
- package/src/components/FunnelRunHistory/RunRow.tsx +7 -7
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +9 -9
- package/src/components/FunnelRunHistory/types.ts +6 -6
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +41 -41
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +6 -6
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +5 -5
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +7 -7
- package/src/components/FunnelStageBuilder/StageActions.tsx +4 -4
- package/src/components/FunnelStageBuilder/StageCard.tsx +3 -3
- package/src/components/FunnelStageBuilder/StageForm.tsx +14 -14
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +42 -42
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +31 -31
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +4 -4
- package/src/components/FunnelVisualFlow/StageNode.tsx +8 -8
- package/src/components/FunnelVisualFlow/example.tsx +7 -7
- package/src/core/engine.test.ts +271 -271
- package/src/core/engine.ts +60 -60
- package/src/core/evaluator.example.ts +31 -31
- package/src/core/evaluator.test.ts +92 -92
- package/src/core/evaluator.ts +23 -23
- package/src/core/field-resolver.example.ts +1 -1
- package/src/core/field-resolver.test.ts +4 -4
- package/src/store/create-funnel-store.test.ts +32 -32
- package/src/store/create-funnel-store.ts +3 -3
- package/src/store/types.ts +2 -2
- package/src/types/index.ts +64 -64
|
@@ -29,7 +29,7 @@ interface TestEntity {
|
|
|
29
29
|
score?: number;
|
|
30
30
|
active?: boolean;
|
|
31
31
|
tags?: string[];
|
|
32
|
-
|
|
32
|
+
createdAt?: Date | string;
|
|
33
33
|
firm?: {
|
|
34
34
|
name?: string;
|
|
35
35
|
stage?: string;
|
|
@@ -53,7 +53,7 @@ const sampleEntity: TestEntity = {
|
|
|
53
53
|
score: 85.5,
|
|
54
54
|
active: true,
|
|
55
55
|
tags: ['qualified', 'active', 'premium'],
|
|
56
|
-
|
|
56
|
+
createdAt: new Date('2024-01-15'),
|
|
57
57
|
firm: {
|
|
58
58
|
name: 'Acme Ventures',
|
|
59
59
|
stage: 'Series A',
|
|
@@ -75,32 +75,32 @@ const sampleEntity: TestEntity = {
|
|
|
75
75
|
|
|
76
76
|
describe('Equality Operators', () => {
|
|
77
77
|
it('eq: should match equal values', () => {
|
|
78
|
-
const rule: FilterRule = {
|
|
78
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'eq', value: 'John Doe' };
|
|
79
79
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
it('eq: should not match different values', () => {
|
|
83
|
-
const rule: FilterRule = {
|
|
83
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'eq', value: 'Jane Doe' };
|
|
84
84
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
it('eq: should match numbers', () => {
|
|
88
|
-
const rule: FilterRule = {
|
|
88
|
+
const rule: FilterRule = { fieldPath: 'age', operator: 'eq', value: 35 };
|
|
89
89
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
it('eq: should match nested fields', () => {
|
|
93
|
-
const rule: FilterRule = {
|
|
93
|
+
const rule: FilterRule = { fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' };
|
|
94
94
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
it('ne: should match different values', () => {
|
|
98
|
-
const rule: FilterRule = {
|
|
98
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'ne', value: 'Jane Doe' };
|
|
99
99
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it('ne: should not match equal values', () => {
|
|
103
|
-
const rule: FilterRule = {
|
|
103
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'ne', value: 'John Doe' };
|
|
104
104
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
105
105
|
});
|
|
106
106
|
});
|
|
@@ -111,56 +111,56 @@ describe('Equality Operators', () => {
|
|
|
111
111
|
|
|
112
112
|
describe('Comparison Operators', () => {
|
|
113
113
|
it('gt: should match greater values', () => {
|
|
114
|
-
const rule: FilterRule = {
|
|
114
|
+
const rule: FilterRule = { fieldPath: 'age', operator: 'gt', value: 30 };
|
|
115
115
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
it('gt: should not match equal or lesser values', () => {
|
|
119
|
-
const rule: FilterRule = {
|
|
119
|
+
const rule: FilterRule = { fieldPath: 'age', operator: 'gt', value: 35 };
|
|
120
120
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it('lt: should match lesser values', () => {
|
|
124
|
-
const rule: FilterRule = {
|
|
124
|
+
const rule: FilterRule = { fieldPath: 'age', operator: 'lt', value: 40 };
|
|
125
125
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
it('lt: should not match equal or greater values', () => {
|
|
129
|
-
const rule: FilterRule = {
|
|
129
|
+
const rule: FilterRule = { fieldPath: 'age', operator: 'lt', value: 35 };
|
|
130
130
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
it('gte: should match greater or equal values', () => {
|
|
134
|
-
const rule1: FilterRule = {
|
|
135
|
-
const rule2: FilterRule = {
|
|
134
|
+
const rule1: FilterRule = { fieldPath: 'age', operator: 'gte', value: 35 };
|
|
135
|
+
const rule2: FilterRule = { fieldPath: 'age', operator: 'gte', value: 30 };
|
|
136
136
|
expect(evaluateRule(sampleEntity, rule1)).toBe(true);
|
|
137
137
|
expect(evaluateRule(sampleEntity, rule2)).toBe(true);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
it('gte: should not match lesser values', () => {
|
|
141
|
-
const rule: FilterRule = {
|
|
141
|
+
const rule: FilterRule = { fieldPath: 'age', operator: 'gte', value: 40 };
|
|
142
142
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
145
|
it('lte: should match lesser or equal values', () => {
|
|
146
|
-
const rule1: FilterRule = {
|
|
147
|
-
const rule2: FilterRule = {
|
|
146
|
+
const rule1: FilterRule = { fieldPath: 'age', operator: 'lte', value: 35 };
|
|
147
|
+
const rule2: FilterRule = { fieldPath: 'age', operator: 'lte', value: 40 };
|
|
148
148
|
expect(evaluateRule(sampleEntity, rule1)).toBe(true);
|
|
149
149
|
expect(evaluateRule(sampleEntity, rule2)).toBe(true);
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
it('lte: should not match greater values', () => {
|
|
153
|
-
const rule: FilterRule = {
|
|
153
|
+
const rule: FilterRule = { fieldPath: 'age', operator: 'lte', value: 30 };
|
|
154
154
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
it('should compare decimal numbers', () => {
|
|
158
|
-
const rule: FilterRule = {
|
|
158
|
+
const rule: FilterRule = { fieldPath: 'score', operator: 'gt', value: 85 };
|
|
159
159
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
it('should compare nested numbers', () => {
|
|
163
|
-
const rule: FilterRule = {
|
|
163
|
+
const rule: FilterRule = { fieldPath: 'firm.aum', operator: 'gte', value: 100000000 };
|
|
164
164
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
165
165
|
});
|
|
166
166
|
});
|
|
@@ -171,57 +171,57 @@ describe('Comparison Operators', () => {
|
|
|
171
171
|
|
|
172
172
|
describe('String Operators', () => {
|
|
173
173
|
it('contains: should match substring (case-insensitive)', () => {
|
|
174
|
-
const rule: FilterRule = {
|
|
174
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'contains', value: 'john' };
|
|
175
175
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
it('contains: should not match non-existent substring', () => {
|
|
179
|
-
const rule: FilterRule = {
|
|
179
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'contains', value: 'jane' };
|
|
180
180
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
181
181
|
});
|
|
182
182
|
|
|
183
183
|
it('contains: should match nested string', () => {
|
|
184
|
-
const rule: FilterRule = {
|
|
184
|
+
const rule: FilterRule = { fieldPath: 'firm.name', operator: 'contains', value: 'acme' };
|
|
185
185
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
it('not_contains: should match when substring absent', () => {
|
|
189
|
-
const rule: FilterRule = {
|
|
189
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'not_contains', value: 'jane' };
|
|
190
190
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
it('not_contains: should not match when substring present', () => {
|
|
194
|
-
const rule: FilterRule = {
|
|
194
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'not_contains', value: 'john' };
|
|
195
195
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
it('startswith: should match prefix (case-insensitive)', () => {
|
|
199
|
-
const rule: FilterRule = {
|
|
199
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'startswith', value: 'john' };
|
|
200
200
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
203
|
it('startswith: should not match non-prefix', () => {
|
|
204
|
-
const rule: FilterRule = {
|
|
204
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'startswith', value: 'doe' };
|
|
205
205
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
206
206
|
});
|
|
207
207
|
|
|
208
208
|
it('endswith: should match suffix (case-insensitive)', () => {
|
|
209
|
-
const rule: FilterRule = {
|
|
209
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'endswith', value: 'doe' };
|
|
210
210
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
it('endswith: should not match non-suffix', () => {
|
|
214
|
-
const rule: FilterRule = {
|
|
214
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'endswith', value: 'john' };
|
|
215
215
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
216
216
|
});
|
|
217
217
|
|
|
218
218
|
it('matches: should match regex pattern', () => {
|
|
219
|
-
const rule: FilterRule = {
|
|
219
|
+
const rule: FilterRule = { fieldPath: 'email', operator: 'matches', value: '^[a-z]+@example\\.com$' };
|
|
220
220
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
221
221
|
});
|
|
222
222
|
|
|
223
223
|
it('matches: should not match non-matching pattern', () => {
|
|
224
|
-
const rule: FilterRule = {
|
|
224
|
+
const rule: FilterRule = { fieldPath: 'email', operator: 'matches', value: '^[0-9]+@' };
|
|
225
225
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
226
226
|
});
|
|
227
227
|
});
|
|
@@ -232,42 +232,42 @@ describe('String Operators', () => {
|
|
|
232
232
|
|
|
233
233
|
describe('Array Operators', () => {
|
|
234
234
|
it('in: should match value in array', () => {
|
|
235
|
-
const rule: FilterRule = {
|
|
235
|
+
const rule: FilterRule = { fieldPath: 'firm.stage', operator: 'in', value: ['Seed', 'Series A', 'Series B'] };
|
|
236
236
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
237
237
|
});
|
|
238
238
|
|
|
239
239
|
it('in: should not match value not in array', () => {
|
|
240
|
-
const rule: FilterRule = {
|
|
240
|
+
const rule: FilterRule = { fieldPath: 'firm.stage', operator: 'in', value: ['Seed', 'Series B'] };
|
|
241
241
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
242
242
|
});
|
|
243
243
|
|
|
244
244
|
it('not_in: should match value not in array', () => {
|
|
245
|
-
const rule: FilterRule = {
|
|
245
|
+
const rule: FilterRule = { fieldPath: 'firm.stage', operator: 'not_in', value: ['Seed', 'Series B'] };
|
|
246
246
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
it('not_in: should not match value in array', () => {
|
|
250
|
-
const rule: FilterRule = {
|
|
250
|
+
const rule: FilterRule = { fieldPath: 'firm.stage', operator: 'not_in', value: ['Series A', 'Series B'] };
|
|
251
251
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
252
252
|
});
|
|
253
253
|
|
|
254
254
|
it('has_any: should match when array has any value', () => {
|
|
255
|
-
const rule: FilterRule = {
|
|
255
|
+
const rule: FilterRule = { fieldPath: 'tags', operator: 'has_any', value: ['qualified', 'inactive'] };
|
|
256
256
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
257
257
|
});
|
|
258
258
|
|
|
259
259
|
it('has_any: should not match when array has no values', () => {
|
|
260
|
-
const rule: FilterRule = {
|
|
260
|
+
const rule: FilterRule = { fieldPath: 'tags', operator: 'has_any', value: ['inactive', 'suspended'] };
|
|
261
261
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
it('has_all: should match when array has all values', () => {
|
|
265
|
-
const rule: FilterRule = {
|
|
265
|
+
const rule: FilterRule = { fieldPath: 'tags', operator: 'has_all', value: ['qualified', 'active'] };
|
|
266
266
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
267
267
|
});
|
|
268
268
|
|
|
269
269
|
it('has_all: should not match when array missing some values', () => {
|
|
270
|
-
const rule: FilterRule = {
|
|
270
|
+
const rule: FilterRule = { fieldPath: 'tags', operator: 'has_all', value: ['qualified', 'inactive'] };
|
|
271
271
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
272
272
|
});
|
|
273
273
|
});
|
|
@@ -286,37 +286,37 @@ describe('Null Operators', () => {
|
|
|
286
286
|
};
|
|
287
287
|
|
|
288
288
|
it('isnull: should match null values', () => {
|
|
289
|
-
const rule: FilterRule = {
|
|
289
|
+
const rule: FilterRule = { fieldPath: 'email', operator: 'isnull', value: null };
|
|
290
290
|
expect(evaluateRule(entityWithNulls, rule)).toBe(true);
|
|
291
291
|
});
|
|
292
292
|
|
|
293
293
|
it('isnull: should match undefined values', () => {
|
|
294
|
-
const rule: FilterRule = {
|
|
294
|
+
const rule: FilterRule = { fieldPath: 'age', operator: 'isnull', value: null };
|
|
295
295
|
expect(evaluateRule(entityWithNulls, rule)).toBe(true);
|
|
296
296
|
});
|
|
297
297
|
|
|
298
298
|
it('isnull: should not match defined values', () => {
|
|
299
|
-
const rule: FilterRule = {
|
|
299
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'isnull', value: null };
|
|
300
300
|
expect(evaluateRule(entityWithNulls, rule)).toBe(false);
|
|
301
301
|
});
|
|
302
302
|
|
|
303
303
|
it('isnull: should match nested null', () => {
|
|
304
|
-
const rule: FilterRule = {
|
|
304
|
+
const rule: FilterRule = { fieldPath: 'profile.linkedin_url', operator: 'isnull', value: null };
|
|
305
305
|
expect(evaluateRule(entityWithNulls, rule)).toBe(true);
|
|
306
306
|
});
|
|
307
307
|
|
|
308
308
|
it('isnotnull: should match defined values', () => {
|
|
309
|
-
const rule: FilterRule = {
|
|
309
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'isnotnull', value: null };
|
|
310
310
|
expect(evaluateRule(entityWithNulls, rule)).toBe(true);
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
it('isnotnull: should not match null values', () => {
|
|
314
|
-
const rule: FilterRule = {
|
|
314
|
+
const rule: FilterRule = { fieldPath: 'email', operator: 'isnotnull', value: null };
|
|
315
315
|
expect(evaluateRule(entityWithNulls, rule)).toBe(false);
|
|
316
316
|
});
|
|
317
317
|
|
|
318
318
|
it('isnotnull: should not match undefined values', () => {
|
|
319
|
-
const rule: FilterRule = {
|
|
319
|
+
const rule: FilterRule = { fieldPath: 'age', operator: 'isnotnull', value: null };
|
|
320
320
|
expect(evaluateRule(entityWithNulls, rule)).toBe(false);
|
|
321
321
|
});
|
|
322
322
|
});
|
|
@@ -327,22 +327,22 @@ describe('Null Operators', () => {
|
|
|
327
327
|
|
|
328
328
|
describe('Tag Operators', () => {
|
|
329
329
|
it('has_tag: should match tag in array (case-insensitive)', () => {
|
|
330
|
-
const rule: FilterRule = {
|
|
330
|
+
const rule: FilterRule = { fieldPath: 'tags', operator: 'has_tag', value: 'QUALIFIED' };
|
|
331
331
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
332
332
|
});
|
|
333
333
|
|
|
334
334
|
it('has_tag: should not match missing tag', () => {
|
|
335
|
-
const rule: FilterRule = {
|
|
335
|
+
const rule: FilterRule = { fieldPath: 'tags', operator: 'has_tag', value: 'inactive' };
|
|
336
336
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
337
337
|
});
|
|
338
338
|
|
|
339
339
|
it('not_has_tag: should match when tag absent', () => {
|
|
340
|
-
const rule: FilterRule = {
|
|
340
|
+
const rule: FilterRule = { fieldPath: 'tags', operator: 'not_has_tag', value: 'inactive' };
|
|
341
341
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
342
342
|
});
|
|
343
343
|
|
|
344
344
|
it('not_has_tag: should not match when tag present', () => {
|
|
345
|
-
const rule: FilterRule = {
|
|
345
|
+
const rule: FilterRule = { fieldPath: 'tags', operator: 'not_has_tag', value: 'qualified' };
|
|
346
346
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
347
347
|
});
|
|
348
348
|
});
|
|
@@ -353,24 +353,24 @@ describe('Tag Operators', () => {
|
|
|
353
353
|
|
|
354
354
|
describe('Boolean Operators', () => {
|
|
355
355
|
it('is_true: should match true values', () => {
|
|
356
|
-
const rule: FilterRule = {
|
|
356
|
+
const rule: FilterRule = { fieldPath: 'active', operator: 'is_true', value: null };
|
|
357
357
|
expect(evaluateRule(sampleEntity, rule)).toBe(true);
|
|
358
358
|
});
|
|
359
359
|
|
|
360
360
|
it('is_true: should not match false values', () => {
|
|
361
361
|
const entity = { ...sampleEntity, active: false };
|
|
362
|
-
const rule: FilterRule = {
|
|
362
|
+
const rule: FilterRule = { fieldPath: 'active', operator: 'is_true', value: null };
|
|
363
363
|
expect(evaluateRule(entity, rule)).toBe(false);
|
|
364
364
|
});
|
|
365
365
|
|
|
366
366
|
it('is_false: should match false values', () => {
|
|
367
367
|
const entity = { ...sampleEntity, active: false };
|
|
368
|
-
const rule: FilterRule = {
|
|
368
|
+
const rule: FilterRule = { fieldPath: 'active', operator: 'is_false', value: null };
|
|
369
369
|
expect(evaluateRule(entity, rule)).toBe(true);
|
|
370
370
|
});
|
|
371
371
|
|
|
372
372
|
it('is_false: should not match true values', () => {
|
|
373
|
-
const rule: FilterRule = {
|
|
373
|
+
const rule: FilterRule = { fieldPath: 'active', operator: 'is_false', value: null };
|
|
374
374
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
375
375
|
});
|
|
376
376
|
});
|
|
@@ -382,7 +382,7 @@ describe('Boolean Operators', () => {
|
|
|
382
382
|
describe('Date Operators', () => {
|
|
383
383
|
it('should compare Date objects', () => {
|
|
384
384
|
const rule: FilterRule = {
|
|
385
|
-
|
|
385
|
+
fieldPath: 'createdAt',
|
|
386
386
|
operator: 'gt',
|
|
387
387
|
value: new Date('2024-01-01'),
|
|
388
388
|
};
|
|
@@ -390,9 +390,9 @@ describe('Date Operators', () => {
|
|
|
390
390
|
});
|
|
391
391
|
|
|
392
392
|
it('should compare date strings', () => {
|
|
393
|
-
const entity = { ...sampleEntity,
|
|
393
|
+
const entity = { ...sampleEntity, createdAt: '2024-01-15' };
|
|
394
394
|
const rule: FilterRule = {
|
|
395
|
-
|
|
395
|
+
fieldPath: 'createdAt',
|
|
396
396
|
operator: 'gte',
|
|
397
397
|
value: '2024-01-15',
|
|
398
398
|
};
|
|
@@ -400,9 +400,9 @@ describe('Date Operators', () => {
|
|
|
400
400
|
});
|
|
401
401
|
|
|
402
402
|
it('should handle date equality', () => {
|
|
403
|
-
const entity = { ...sampleEntity,
|
|
403
|
+
const entity = { ...sampleEntity, createdAt: '2024-01-15' };
|
|
404
404
|
const rule: FilterRule = {
|
|
405
|
-
|
|
405
|
+
fieldPath: 'createdAt',
|
|
406
406
|
operator: 'eq',
|
|
407
407
|
value: new Date('2024-01-15'),
|
|
408
408
|
};
|
|
@@ -417,7 +417,7 @@ describe('Date Operators', () => {
|
|
|
417
417
|
describe('Negate Flag', () => {
|
|
418
418
|
it('should negate equality', () => {
|
|
419
419
|
const rule: FilterRule = {
|
|
420
|
-
|
|
420
|
+
fieldPath: 'name',
|
|
421
421
|
operator: 'eq',
|
|
422
422
|
value: 'John Doe',
|
|
423
423
|
negate: true,
|
|
@@ -427,7 +427,7 @@ describe('Negate Flag', () => {
|
|
|
427
427
|
|
|
428
428
|
it('should negate inequality', () => {
|
|
429
429
|
const rule: FilterRule = {
|
|
430
|
-
|
|
430
|
+
fieldPath: 'name',
|
|
431
431
|
operator: 'ne',
|
|
432
432
|
value: 'John Doe',
|
|
433
433
|
negate: true,
|
|
@@ -437,7 +437,7 @@ describe('Negate Flag', () => {
|
|
|
437
437
|
|
|
438
438
|
it('should negate contains', () => {
|
|
439
439
|
const rule: FilterRule = {
|
|
440
|
-
|
|
440
|
+
fieldPath: 'name',
|
|
441
441
|
operator: 'contains',
|
|
442
442
|
value: 'john',
|
|
443
443
|
negate: true,
|
|
@@ -452,31 +452,31 @@ describe('Negate Flag', () => {
|
|
|
452
452
|
|
|
453
453
|
describe('Edge Cases', () => {
|
|
454
454
|
it('should handle missing nested paths', () => {
|
|
455
|
-
const rule: FilterRule = {
|
|
455
|
+
const rule: FilterRule = { fieldPath: 'firm.missing.field', operator: 'eq', value: 'test' };
|
|
456
456
|
expect(evaluateRule(sampleEntity, rule)).toBe(false);
|
|
457
457
|
});
|
|
458
458
|
|
|
459
459
|
it('should handle empty strings', () => {
|
|
460
460
|
const entity = { name: '' };
|
|
461
|
-
const rule: FilterRule = {
|
|
461
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'eq', value: '' };
|
|
462
462
|
expect(evaluateRule(entity, rule)).toBe(true);
|
|
463
463
|
});
|
|
464
464
|
|
|
465
465
|
it('should handle empty arrays', () => {
|
|
466
466
|
const entity = { tags: [] };
|
|
467
|
-
const rule: FilterRule = {
|
|
467
|
+
const rule: FilterRule = { fieldPath: 'tags', operator: 'has_tag', value: 'test' };
|
|
468
468
|
expect(evaluateRule(entity, rule)).toBe(false);
|
|
469
469
|
});
|
|
470
470
|
|
|
471
471
|
it('should handle zero values', () => {
|
|
472
472
|
const entity = { score: 0 };
|
|
473
|
-
const rule: FilterRule = {
|
|
473
|
+
const rule: FilterRule = { fieldPath: 'score', operator: 'eq', value: 0 };
|
|
474
474
|
expect(evaluateRule(entity, rule)).toBe(true);
|
|
475
475
|
});
|
|
476
476
|
|
|
477
477
|
it('should handle numeric strings in comparisons', () => {
|
|
478
478
|
const entity = { age: '35' };
|
|
479
|
-
const rule: FilterRule = {
|
|
479
|
+
const rule: FilterRule = { fieldPath: 'age', operator: 'gt', value: 30 };
|
|
480
480
|
expect(evaluateRule(entity, rule)).toBe(true);
|
|
481
481
|
});
|
|
482
482
|
});
|
|
@@ -487,9 +487,9 @@ describe('Edge Cases', () => {
|
|
|
487
487
|
|
|
488
488
|
describe('Multiple Rules', () => {
|
|
489
489
|
const rules: FilterRule[] = [
|
|
490
|
-
{
|
|
491
|
-
{
|
|
492
|
-
{
|
|
490
|
+
{ fieldPath: 'age', operator: 'gte', value: 30 },
|
|
491
|
+
{ fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
492
|
+
{ fieldPath: 'tags', operator: 'has_tag', value: 'qualified' },
|
|
493
493
|
];
|
|
494
494
|
|
|
495
495
|
it('AND: should match when all rules pass', () => {
|
|
@@ -499,23 +499,23 @@ describe('Multiple Rules', () => {
|
|
|
499
499
|
it('AND: should not match when any rule fails', () => {
|
|
500
500
|
const failingRules = [
|
|
501
501
|
...rules,
|
|
502
|
-
{
|
|
502
|
+
{ fieldPath: 'age', operator: 'lt', value: 30 } as FilterRule,
|
|
503
503
|
];
|
|
504
504
|
expect(evaluateRulesAND(sampleEntity, failingRules)).toBe(false);
|
|
505
505
|
});
|
|
506
506
|
|
|
507
507
|
it('OR: should match when any rule passes', () => {
|
|
508
508
|
const orRules: FilterRule[] = [
|
|
509
|
-
{
|
|
510
|
-
{
|
|
509
|
+
{ fieldPath: 'age', operator: 'lt', value: 30 }, // false
|
|
510
|
+
{ fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' }, // true
|
|
511
511
|
];
|
|
512
512
|
expect(evaluateRulesOR(sampleEntity, orRules)).toBe(true);
|
|
513
513
|
});
|
|
514
514
|
|
|
515
515
|
it('OR: should not match when all rules fail', () => {
|
|
516
516
|
const orRules: FilterRule[] = [
|
|
517
|
-
{
|
|
518
|
-
{
|
|
517
|
+
{ fieldPath: 'age', operator: 'lt', value: 30 },
|
|
518
|
+
{ fieldPath: 'firm.stage', operator: 'eq', value: 'Seed' },
|
|
519
519
|
];
|
|
520
520
|
expect(evaluateRulesOR(sampleEntity, orRules)).toBe(false);
|
|
521
521
|
});
|
|
@@ -526,8 +526,8 @@ describe('Multiple Rules', () => {
|
|
|
526
526
|
|
|
527
527
|
it('evaluateRules: should use specified logic', () => {
|
|
528
528
|
const orRules: FilterRule[] = [
|
|
529
|
-
{
|
|
530
|
-
{
|
|
529
|
+
{ fieldPath: 'age', operator: 'lt', value: 30 },
|
|
530
|
+
{ fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
531
531
|
];
|
|
532
532
|
expect(evaluateRules(sampleEntity, orRules, 'OR')).toBe(true);
|
|
533
533
|
});
|
|
@@ -544,35 +544,35 @@ describe('Multiple Rules', () => {
|
|
|
544
544
|
|
|
545
545
|
describe('Detailed Results', () => {
|
|
546
546
|
it('should return detailed rule result', () => {
|
|
547
|
-
const rule: FilterRule = {
|
|
547
|
+
const rule: FilterRule = { fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' };
|
|
548
548
|
const result = evaluateRuleWithResult(sampleEntity, rule);
|
|
549
549
|
|
|
550
550
|
expect(result).toEqual({
|
|
551
|
-
|
|
551
|
+
fieldPath: 'firm.stage',
|
|
552
552
|
operator: 'eq',
|
|
553
553
|
value: 'Series A',
|
|
554
|
-
|
|
554
|
+
actualValue: 'Series A',
|
|
555
555
|
matched: true,
|
|
556
556
|
});
|
|
557
557
|
});
|
|
558
558
|
|
|
559
559
|
it('should return detailed results for multiple rules', () => {
|
|
560
560
|
const rules: FilterRule[] = [
|
|
561
|
-
{
|
|
562
|
-
{
|
|
561
|
+
{ fieldPath: 'age', operator: 'gte', value: 30 },
|
|
562
|
+
{ fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
563
563
|
];
|
|
564
564
|
|
|
565
565
|
const result = evaluateRulesWithResults(sampleEntity, rules, 'AND');
|
|
566
566
|
|
|
567
567
|
expect(result.matched).toBe(true);
|
|
568
568
|
expect(result.logic).toBe('AND');
|
|
569
|
-
expect(result.
|
|
570
|
-
expect(result.
|
|
571
|
-
expect(result.
|
|
569
|
+
expect(result.ruleResults).toHaveLength(2);
|
|
570
|
+
expect(result.ruleResults[0].matched).toBe(true);
|
|
571
|
+
expect(result.ruleResults[1].matched).toBe(true);
|
|
572
572
|
});
|
|
573
573
|
|
|
574
574
|
it('should handle errors in rule evaluation', () => {
|
|
575
|
-
const rule: FilterRule = {
|
|
575
|
+
const rule: FilterRule = { fieldPath: 'name', operator: 'matches' as any, value: '[invalid-regex' };
|
|
576
576
|
const result = evaluateRuleWithResult(sampleEntity, rule);
|
|
577
577
|
|
|
578
578
|
expect(result.matched).toBe(false);
|
|
@@ -593,7 +593,7 @@ describe('Filter Entities', () => {
|
|
|
593
593
|
|
|
594
594
|
it('should filter entities with single rule', () => {
|
|
595
595
|
const rules: FilterRule[] = [
|
|
596
|
-
{
|
|
596
|
+
{ fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
597
597
|
];
|
|
598
598
|
|
|
599
599
|
const result = filterEntities(entities, rules);
|
|
@@ -603,8 +603,8 @@ describe('Filter Entities', () => {
|
|
|
603
603
|
|
|
604
604
|
it('should filter entities with multiple AND rules', () => {
|
|
605
605
|
const rules: FilterRule[] = [
|
|
606
|
-
{
|
|
607
|
-
{
|
|
606
|
+
{ fieldPath: 'age', operator: 'gte', value: 30 },
|
|
607
|
+
{ fieldPath: 'firm.stage', operator: 'in', value: ['Series A', 'Series B'] },
|
|
608
608
|
];
|
|
609
609
|
|
|
610
610
|
const result = filterEntities(entities, rules, 'AND');
|
|
@@ -614,8 +614,8 @@ describe('Filter Entities', () => {
|
|
|
614
614
|
|
|
615
615
|
it('should filter entities with OR rules', () => {
|
|
616
616
|
const rules: FilterRule[] = [
|
|
617
|
-
{
|
|
618
|
-
{
|
|
617
|
+
{ fieldPath: 'age', operator: 'lt', value: 30 },
|
|
618
|
+
{ fieldPath: 'firm.stage', operator: 'eq', value: 'Series B' },
|
|
619
619
|
];
|
|
620
620
|
|
|
621
621
|
const result = filterEntities(entities, rules, 'OR');
|
|
@@ -630,7 +630,7 @@ describe('Filter Entities', () => {
|
|
|
630
630
|
|
|
631
631
|
it('should return empty array for no matches', () => {
|
|
632
632
|
const rules: FilterRule[] = [
|
|
633
|
-
{
|
|
633
|
+
{ fieldPath: 'age', operator: 'gt', value: 100 },
|
|
634
634
|
];
|
|
635
635
|
|
|
636
636
|
const result = filterEntities(entities, rules);
|