@startsimpli/funnels 0.1.4 → 0.1.5
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 +9 -31
- package/src/api/README.md +507 -0
- package/src/api/adapter.ts +106 -0
- package/src/api/client.test.ts +640 -0
- package/src/api/client.ts +385 -0
- package/src/api/default-adapter.ts +243 -0
- package/src/api/index.ts +24 -0
- package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
- package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
- package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
- package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
- package/src/components/FilterRuleEditor/README.md +291 -0
- package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
- package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
- package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
- package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
- package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
- package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
- package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
- package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
- package/src/components/FilterRuleEditor/constants.ts +64 -0
- package/src/components/FilterRuleEditor/index.ts +14 -0
- package/src/components/FunnelCard/DESIGN.md +447 -0
- package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
- package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
- package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
- package/src/components/FunnelCard/FunnelCard.tsx +204 -0
- package/src/components/FunnelCard/FunnelStats.tsx +68 -0
- package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
- package/src/components/FunnelCard/INSTALLATION.md +304 -0
- package/src/components/FunnelCard/MatchBar.tsx +49 -0
- package/src/components/FunnelCard/README.md +294 -0
- package/src/components/FunnelCard/StageIndicator.tsx +62 -0
- package/src/components/FunnelCard/StatusBadge.tsx +52 -0
- package/src/components/FunnelCard/index.ts +14 -0
- package/src/components/FunnelPreview/EntityCard.tsx +72 -0
- package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
- package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
- package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
- package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
- package/src/components/FunnelPreview/README.md +337 -0
- package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
- package/src/components/FunnelPreview/example.tsx +286 -0
- package/src/components/FunnelPreview/index.ts +14 -0
- package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
- package/src/components/FunnelRunHistory/README.md +325 -0
- package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
- package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
- package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
- package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
- package/src/components/FunnelRunHistory/index.ts +51 -0
- package/src/components/FunnelRunHistory/types.ts +40 -0
- package/src/components/FunnelRunHistory/utils.test.ts +126 -0
- package/src/components/FunnelRunHistory/utils.ts +100 -0
- package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
- package/src/components/FunnelStageBuilder/README.md +341 -0
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
- package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
- package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
- package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
- package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
- package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
- package/src/components/FunnelStageBuilder/index.ts +21 -0
- package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
- package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
- package/src/components/FunnelVisualFlow/README.md +323 -0
- package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
- package/src/components/FunnelVisualFlow/example.tsx +227 -0
- package/src/components/FunnelVisualFlow/index.ts +10 -0
- package/src/components/index.ts +102 -0
- package/src/core/README.md +307 -0
- package/src/core/engine.test.ts +1087 -0
- package/src/core/engine.ts +329 -0
- package/src/core/evaluator.example.ts +353 -0
- package/src/core/evaluator.test.ts +639 -0
- package/src/core/evaluator.ts +261 -0
- package/src/core/field-resolver.example.ts +175 -0
- package/src/core/field-resolver.test.ts +541 -0
- package/src/core/field-resolver.ts +247 -0
- package/src/core/index.ts +34 -0
- package/src/core/operators.test.ts +539 -0
- package/src/core/operators.ts +241 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useDebouncedValue.ts +28 -0
- package/src/index.ts +155 -0
- package/src/store/README.md +342 -0
- package/src/store/create-funnel-store.test.ts +686 -0
- package/src/store/create-funnel-store.ts +538 -0
- package/src/store/index.ts +9 -0
- package/src/store/types.ts +294 -0
- package/src/stories/CrossDomain.stories.tsx +149 -0
- package/src/stories/Welcome.stories.tsx +81 -0
- package/src/stories/demo-data/index.ts +3 -0
- package/src/stories/demo-data/investors.ts +216 -0
- package/src/stories/demo-data/leads.ts +223 -0
- package/src/stories/demo-data/recipes.ts +217 -0
- package/src/test/setup.ts +5 -0
- package/src/types/index.ts +843 -0
- package/dist/client-3ESO2NHy.d.ts +0 -310
- package/dist/client-CZu03ACp.d.cts +0 -310
- package/dist/components/index.cjs +0 -3241
- package/dist/components/index.cjs.map +0 -1
- package/dist/components/index.css.map +0 -1
- package/dist/components/index.d.cts +0 -726
- package/dist/components/index.d.ts +0 -726
- package/dist/components/index.js +0 -3194
- package/dist/components/index.js.map +0 -1
- package/dist/core/index.cjs +0 -500
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -359
- package/dist/core/index.d.ts +0 -359
- package/dist/core/index.js +0 -486
- package/dist/core/index.js.map +0 -1
- package/dist/hooks/index.cjs +0 -20
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.d.cts +0 -11
- package/dist/hooks/index.d.ts +0 -11
- package/dist/hooks/index.js +0 -18
- package/dist/hooks/index.js.map +0 -1
- package/dist/index-BGDEXbuz.d.cts +0 -434
- package/dist/index-BGDEXbuz.d.ts +0 -434
- package/dist/index.cjs +0 -4499
- package/dist/index.cjs.map +0 -1
- package/dist/index.css +0 -198
- package/dist/index.css.map +0 -1
- package/dist/index.d.cts +0 -99
- package/dist/index.d.ts +0 -99
- package/dist/index.js +0 -4421
- package/dist/index.js.map +0 -1
- package/dist/store/index.cjs +0 -389
- package/dist/store/index.cjs.map +0 -1
- package/dist/store/index.d.cts +0 -225
- package/dist/store/index.d.ts +0 -225
- package/dist/store/index.js +0 -386
- package/dist/store/index.js.map +0 -1
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @startsimpli/funnels - Rule Evaluator
|
|
3
|
+
*
|
|
4
|
+
* BRUTALLY GENERIC rule evaluation engine.
|
|
5
|
+
*
|
|
6
|
+
* Evaluates FilterRule against ANY entity type by:
|
|
7
|
+
* 1. Resolving field value from entity
|
|
8
|
+
* 2. Applying operator comparison
|
|
9
|
+
* 3. Handling negate flag
|
|
10
|
+
* 4. Returning boolean result
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { FilterRule, RuleResult } from '../types';
|
|
14
|
+
import { resolveField } from './field-resolver';
|
|
15
|
+
import { applyOperator } from './operators';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Evaluate a single filter rule against an entity
|
|
19
|
+
*
|
|
20
|
+
* @param entity - The entity to evaluate
|
|
21
|
+
* @param rule - The filter rule to apply
|
|
22
|
+
* @returns Whether the rule matches
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const investor = { name: 'John', firm: { stage: 'Series A' } };
|
|
27
|
+
*
|
|
28
|
+
* evaluateRule(investor, {
|
|
29
|
+
* field_path: 'firm.stage',
|
|
30
|
+
* operator: 'eq',
|
|
31
|
+
* value: 'Series A'
|
|
32
|
+
* }); // true
|
|
33
|
+
*
|
|
34
|
+
* evaluateRule(investor, {
|
|
35
|
+
* field_path: 'firm.stage',
|
|
36
|
+
* operator: 'eq',
|
|
37
|
+
* value: 'Seed',
|
|
38
|
+
* negate: true
|
|
39
|
+
* }); // true (negated: stage !== 'Seed')
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function evaluateRule<T>(entity: T, rule: FilterRule): boolean {
|
|
43
|
+
// Resolve field value from entity
|
|
44
|
+
const actualValue = resolveField(entity, rule.field_path);
|
|
45
|
+
|
|
46
|
+
// Apply operator
|
|
47
|
+
const result = applyOperator(rule.operator, actualValue, rule.value);
|
|
48
|
+
|
|
49
|
+
// Handle negate flag
|
|
50
|
+
return rule.negate ? !result : result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Evaluate a rule and return detailed result with diagnostics
|
|
55
|
+
*
|
|
56
|
+
* @param entity - The entity to evaluate
|
|
57
|
+
* @param rule - The filter rule to apply
|
|
58
|
+
* @returns Detailed rule result with actual value and match status
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const result = evaluateRuleWithResult(investor, {
|
|
63
|
+
* field_path: 'firm.stage',
|
|
64
|
+
* operator: 'eq',
|
|
65
|
+
* value: 'Series A'
|
|
66
|
+
* });
|
|
67
|
+
* // {
|
|
68
|
+
* // field_path: 'firm.stage',
|
|
69
|
+
* // operator: 'eq',
|
|
70
|
+
* // value: 'Series A',
|
|
71
|
+
* // actual_value: 'Series A',
|
|
72
|
+
* // matched: true
|
|
73
|
+
* // }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function evaluateRuleWithResult<T>(
|
|
77
|
+
entity: T,
|
|
78
|
+
rule: FilterRule
|
|
79
|
+
): RuleResult {
|
|
80
|
+
try {
|
|
81
|
+
// Resolve field value
|
|
82
|
+
const actualValue = resolveField(entity, rule.field_path);
|
|
83
|
+
|
|
84
|
+
// Apply operator
|
|
85
|
+
const operatorResult = applyOperator(rule.operator, actualValue, rule.value);
|
|
86
|
+
|
|
87
|
+
// Handle negate flag
|
|
88
|
+
const matched = rule.negate ? !operatorResult : operatorResult;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
field_path: rule.field_path,
|
|
92
|
+
operator: rule.operator,
|
|
93
|
+
value: rule.value,
|
|
94
|
+
actual_value: actualValue,
|
|
95
|
+
matched,
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// Handle evaluation errors gracefully
|
|
99
|
+
return {
|
|
100
|
+
field_path: rule.field_path,
|
|
101
|
+
operator: rule.operator,
|
|
102
|
+
value: rule.value,
|
|
103
|
+
actual_value: undefined,
|
|
104
|
+
matched: false,
|
|
105
|
+
error: error instanceof Error ? error.message : String(error),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Evaluate multiple rules with AND logic
|
|
112
|
+
*
|
|
113
|
+
* @param entity - The entity to evaluate
|
|
114
|
+
* @param rules - Array of filter rules
|
|
115
|
+
* @returns Whether ALL rules match
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* const investor = { name: 'John', firm: { stage: 'Series A', aum: 100000000 } };
|
|
120
|
+
*
|
|
121
|
+
* evaluateRulesAND(investor, [
|
|
122
|
+
* { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
123
|
+
* { field_path: 'firm.aum', operator: 'gte', value: 50000000 }
|
|
124
|
+
* ]); // true (both rules match)
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export function evaluateRulesAND<T>(entity: T, rules: FilterRule[]): boolean {
|
|
128
|
+
if (!rules || rules.length === 0) return true; // Empty rules = match
|
|
129
|
+
|
|
130
|
+
return rules.every(rule => evaluateRule(entity, rule));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Evaluate multiple rules with OR logic
|
|
135
|
+
*
|
|
136
|
+
* @param entity - The entity to evaluate
|
|
137
|
+
* @param rules - Array of filter rules
|
|
138
|
+
* @returns Whether AT LEAST ONE rule matches
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* const investor = { name: 'John', firm: { stage: 'Seed' } };
|
|
143
|
+
*
|
|
144
|
+
* evaluateRulesOR(investor, [
|
|
145
|
+
* { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
146
|
+
* { field_path: 'firm.stage', operator: 'eq', value: 'Seed' }
|
|
147
|
+
* ]); // true (second rule matches)
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function evaluateRulesOR<T>(entity: T, rules: FilterRule[]): boolean {
|
|
151
|
+
if (!rules || rules.length === 0) return true; // Empty rules = match
|
|
152
|
+
|
|
153
|
+
return rules.some(rule => evaluateRule(entity, rule));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Evaluate multiple rules with specified logic (AND/OR)
|
|
158
|
+
*
|
|
159
|
+
* @param entity - The entity to evaluate
|
|
160
|
+
* @param rules - Array of filter rules
|
|
161
|
+
* @param logic - Combination logic ('AND' or 'OR')
|
|
162
|
+
* @returns Whether rules match based on logic
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* evaluateRules(investor, rules, 'AND'); // All must match
|
|
167
|
+
* evaluateRules(investor, rules, 'OR'); // At least one must match
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export function evaluateRules<T>(
|
|
171
|
+
entity: T,
|
|
172
|
+
rules: FilterRule[],
|
|
173
|
+
logic: 'AND' | 'OR' = 'AND'
|
|
174
|
+
): boolean {
|
|
175
|
+
return logic === 'AND'
|
|
176
|
+
? evaluateRulesAND(entity, rules)
|
|
177
|
+
: evaluateRulesOR(entity, rules);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Evaluate rules and return detailed results for each rule
|
|
182
|
+
*
|
|
183
|
+
* @param entity - The entity to evaluate
|
|
184
|
+
* @param rules - Array of filter rules
|
|
185
|
+
* @param logic - Combination logic ('AND' or 'OR')
|
|
186
|
+
* @returns Object with overall match status and per-rule results
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* const result = evaluateRulesWithResults(investor, [
|
|
191
|
+
* { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
192
|
+
* { field_path: 'firm.aum', operator: 'gte', value: 50000000 }
|
|
193
|
+
* ], 'AND');
|
|
194
|
+
* // {
|
|
195
|
+
* // matched: true,
|
|
196
|
+
* // logic: 'AND',
|
|
197
|
+
* // rule_results: [
|
|
198
|
+
* // { field_path: 'firm.stage', operator: 'eq', value: 'Series A', actual_value: 'Series A', matched: true },
|
|
199
|
+
* // { field_path: 'firm.aum', operator: 'gte', value: 50000000, actual_value: 100000000, matched: true }
|
|
200
|
+
* // ]
|
|
201
|
+
* // }
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export function evaluateRulesWithResults<T>(
|
|
205
|
+
entity: T,
|
|
206
|
+
rules: FilterRule[],
|
|
207
|
+
logic: 'AND' | 'OR' = 'AND'
|
|
208
|
+
): {
|
|
209
|
+
matched: boolean;
|
|
210
|
+
logic: 'AND' | 'OR';
|
|
211
|
+
rule_results: RuleResult[];
|
|
212
|
+
} {
|
|
213
|
+
const ruleResults = rules.map(rule => evaluateRuleWithResult(entity, rule));
|
|
214
|
+
|
|
215
|
+
const matched =
|
|
216
|
+
logic === 'AND'
|
|
217
|
+
? ruleResults.every(r => r.matched)
|
|
218
|
+
: ruleResults.some(r => r.matched);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
matched,
|
|
222
|
+
logic,
|
|
223
|
+
rule_results: ruleResults,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Filter an array of entities using rules
|
|
229
|
+
*
|
|
230
|
+
* @param entities - Array of entities to filter
|
|
231
|
+
* @param rules - Filter rules to apply
|
|
232
|
+
* @param logic - Combination logic ('AND' or 'OR')
|
|
233
|
+
* @returns Entities that match the rules
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* const investors = [
|
|
238
|
+
* { name: 'John', firm: { stage: 'Seed' } },
|
|
239
|
+
* { name: 'Jane', firm: { stage: 'Series A' } },
|
|
240
|
+
* { name: 'Bob', firm: { stage: 'Series B' } }
|
|
241
|
+
* ];
|
|
242
|
+
*
|
|
243
|
+
* filterEntities(investors, [
|
|
244
|
+
* { field_path: 'firm.stage', operator: 'in', value: ['Series A', 'Series B'] }
|
|
245
|
+
* ]);
|
|
246
|
+
* // [
|
|
247
|
+
* // { name: 'Jane', firm: { stage: 'Series A' } },
|
|
248
|
+
* // { name: 'Bob', firm: { stage: 'Series B' } }
|
|
249
|
+
* // ]
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
export function filterEntities<T>(
|
|
253
|
+
entities: T[],
|
|
254
|
+
rules: FilterRule[],
|
|
255
|
+
logic: 'AND' | 'OR' = 'AND'
|
|
256
|
+
): T[] {
|
|
257
|
+
if (!entities || entities.length === 0) return [];
|
|
258
|
+
if (!rules || rules.length === 0) return entities; // No rules = return all
|
|
259
|
+
|
|
260
|
+
return entities.filter(entity => evaluateRules(entity, rules, logic));
|
|
261
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @startsimpli/funnels - Field Resolver Usage Examples
|
|
3
|
+
*
|
|
4
|
+
* This file demonstrates how to use the field resolver utilities
|
|
5
|
+
* for accessing and modifying entity properties.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolveField, setField, hasField, getFields } from './field-resolver';
|
|
9
|
+
|
|
10
|
+
// Example 1: Simple investor entity
|
|
11
|
+
const investor = {
|
|
12
|
+
id: '123',
|
|
13
|
+
name: 'John Doe',
|
|
14
|
+
email: 'john@example.com',
|
|
15
|
+
firm: {
|
|
16
|
+
id: 'firm-456',
|
|
17
|
+
name: 'Sequoia Capital',
|
|
18
|
+
stage: 'Series A',
|
|
19
|
+
aum_usd: 100000000,
|
|
20
|
+
},
|
|
21
|
+
tags: ['active', 'qualified', 'enterprise'],
|
|
22
|
+
profile: {
|
|
23
|
+
linkedin_url: 'https://linkedin.com/in/johndoe',
|
|
24
|
+
bio: 'Venture capitalist specializing in early-stage fintech',
|
|
25
|
+
},
|
|
26
|
+
metrics: {
|
|
27
|
+
response_rate: 0.85,
|
|
28
|
+
deal_count: 12,
|
|
29
|
+
},
|
|
30
|
+
created_at: new Date('2024-01-01'),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Resolve simple fields
|
|
34
|
+
console.log('Name:', resolveField(investor, 'name')); // 'John Doe'
|
|
35
|
+
console.log('Email:', resolveField(investor, 'email')); // 'john@example.com'
|
|
36
|
+
|
|
37
|
+
// Resolve nested fields
|
|
38
|
+
console.log('Firm Name:', resolveField(investor, 'firm.name')); // 'Sequoia Capital'
|
|
39
|
+
console.log('AUM:', resolveField(investor, 'firm.aum_usd')); // 100000000
|
|
40
|
+
console.log('LinkedIn:', resolveField(investor, 'profile.linkedin_url'));
|
|
41
|
+
|
|
42
|
+
// Resolve array elements
|
|
43
|
+
console.log('First Tag:', resolveField(investor, 'tags[0]')); // 'active'
|
|
44
|
+
console.log('Second Tag:', resolveField(investor, 'tags[1]')); // 'qualified'
|
|
45
|
+
|
|
46
|
+
// Check field existence
|
|
47
|
+
console.log('Has email?', hasField(investor, 'email')); // true
|
|
48
|
+
console.log('Has phone?', hasField(investor, 'phone')); // false
|
|
49
|
+
|
|
50
|
+
// Get multiple fields at once
|
|
51
|
+
const fields = getFields(investor, [
|
|
52
|
+
'name',
|
|
53
|
+
'firm.stage',
|
|
54
|
+
'metrics.response_rate',
|
|
55
|
+
'tags[0]',
|
|
56
|
+
]);
|
|
57
|
+
console.log('Multiple fields:', fields);
|
|
58
|
+
// {
|
|
59
|
+
// name: 'John Doe',
|
|
60
|
+
// 'firm.stage': 'Series A',
|
|
61
|
+
// 'metrics.response_rate': 0.85,
|
|
62
|
+
// 'tags[0]': 'active'
|
|
63
|
+
// }
|
|
64
|
+
|
|
65
|
+
// Example 2: Recipe entity
|
|
66
|
+
const recipe = {
|
|
67
|
+
id: 'recipe-789',
|
|
68
|
+
title: 'Chocolate Chip Cookies',
|
|
69
|
+
cuisine: 'American',
|
|
70
|
+
difficulty: 'easy',
|
|
71
|
+
ingredients: [
|
|
72
|
+
{ name: 'flour', amount: '2 cups', unit: 'cup' },
|
|
73
|
+
{ name: 'sugar', amount: '1 cup', unit: 'cup' },
|
|
74
|
+
{ name: 'chocolate chips', amount: '2 cups', unit: 'cup' },
|
|
75
|
+
],
|
|
76
|
+
steps: [
|
|
77
|
+
{ order: 1, instruction: 'Mix dry ingredients' },
|
|
78
|
+
{ order: 2, instruction: 'Add wet ingredients' },
|
|
79
|
+
{ order: 3, instruction: 'Bake at 350°F for 12 minutes' },
|
|
80
|
+
],
|
|
81
|
+
metadata: {
|
|
82
|
+
prep_time_minutes: 15,
|
|
83
|
+
cook_time_minutes: 12,
|
|
84
|
+
servings: 24,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Access nested array objects
|
|
89
|
+
console.log('First ingredient:', resolveField(recipe, 'ingredients[0].name')); // 'flour'
|
|
90
|
+
console.log('Ingredient amount:', resolveField(recipe, 'ingredients[2].amount')); // '2 cups'
|
|
91
|
+
console.log('Third step:', resolveField(recipe, 'steps[2].instruction'));
|
|
92
|
+
|
|
93
|
+
// Example 3: Using setField for tag assignment
|
|
94
|
+
const entity: any = {
|
|
95
|
+
id: '456',
|
|
96
|
+
name: 'Jane Smith',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Add tags dynamically
|
|
100
|
+
setField(entity, 'tags', ['new_lead', 'high_priority']);
|
|
101
|
+
console.log('Entity with tags:', entity);
|
|
102
|
+
// { id: '456', name: 'Jane Smith', tags: ['new_lead', 'high_priority'] }
|
|
103
|
+
|
|
104
|
+
// Add nested metadata
|
|
105
|
+
setField(entity, 'metadata.score', 95);
|
|
106
|
+
setField(entity, 'metadata.tier', 'premium');
|
|
107
|
+
console.log('Entity with metadata:', entity);
|
|
108
|
+
// {
|
|
109
|
+
// id: '456',
|
|
110
|
+
// name: 'Jane Smith',
|
|
111
|
+
// tags: ['new_lead', 'high_priority'],
|
|
112
|
+
// metadata: { score: 95, tier: 'premium' }
|
|
113
|
+
// }
|
|
114
|
+
|
|
115
|
+
// Example 4: Real-world funnel rule evaluation
|
|
116
|
+
interface FunnelEntity {
|
|
117
|
+
id: string;
|
|
118
|
+
firm?: {
|
|
119
|
+
stage?: string;
|
|
120
|
+
check_size_usd?: number;
|
|
121
|
+
};
|
|
122
|
+
tags?: string[];
|
|
123
|
+
metrics?: {
|
|
124
|
+
response_rate?: number;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function evaluateRule(entity: FunnelEntity, fieldPath: string, operator: string, value: any): boolean {
|
|
129
|
+
const actualValue = resolveField(entity, fieldPath);
|
|
130
|
+
|
|
131
|
+
switch (operator) {
|
|
132
|
+
case 'eq':
|
|
133
|
+
return actualValue === value;
|
|
134
|
+
case 'gt':
|
|
135
|
+
return actualValue > value;
|
|
136
|
+
case 'in':
|
|
137
|
+
return Array.isArray(value) && value.includes(actualValue);
|
|
138
|
+
case 'has_tag':
|
|
139
|
+
return Array.isArray(actualValue) && actualValue.includes(value);
|
|
140
|
+
default:
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Test the rule evaluator
|
|
146
|
+
const testEntity: FunnelEntity = {
|
|
147
|
+
id: '1',
|
|
148
|
+
firm: {
|
|
149
|
+
stage: 'Series A',
|
|
150
|
+
check_size_usd: 5000000,
|
|
151
|
+
},
|
|
152
|
+
tags: ['qualified', 'enterprise'],
|
|
153
|
+
metrics: {
|
|
154
|
+
response_rate: 0.85,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
console.log('Stage is Series A?', evaluateRule(testEntity, 'firm.stage', 'eq', 'Series A')); // true
|
|
159
|
+
console.log('Check size > $1M?', evaluateRule(testEntity, 'firm.check_size_usd', 'gt', 1000000)); // true
|
|
160
|
+
console.log('Has qualified tag?', evaluateRule(testEntity, 'tags', 'has_tag', 'qualified')); // true
|
|
161
|
+
console.log('Response rate > 0.9?', evaluateRule(testEntity, 'metrics.response_rate', 'gt', 0.9)); // false
|
|
162
|
+
|
|
163
|
+
// Example 5: Safe navigation with missing paths
|
|
164
|
+
const partialEntity = {
|
|
165
|
+
id: '999',
|
|
166
|
+
name: 'Incomplete Entity',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// These all return undefined safely (no errors)
|
|
170
|
+
console.log('Missing firm:', resolveField(partialEntity, 'firm.stage')); // undefined
|
|
171
|
+
console.log('Missing tags:', resolveField(partialEntity, 'tags[0]')); // undefined
|
|
172
|
+
console.log('Deep missing:', resolveField(partialEntity, 'a.b.c.d.e.f')); // undefined
|
|
173
|
+
|
|
174
|
+
// hasField returns false for missing paths
|
|
175
|
+
console.log('Has firm.stage?', hasField(partialEntity, 'firm.stage')); // false
|