@startsimpli/funnels 0.1.3 → 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 -3243
- 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 -3196
- 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 -21
- 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 -19
- 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 -391
- 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 -388
- package/dist/store/index.js.map +0 -1
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @startsimpli/funnels - BRUTALLY GENERIC Funnel Type System
|
|
3
|
+
*
|
|
4
|
+
* This type system works for ANY entity type:
|
|
5
|
+
* - Investors, firms, contacts, organizations
|
|
6
|
+
* - Recipes, ingredients, users
|
|
7
|
+
* - Leads, tasks, projects
|
|
8
|
+
* - GitHub repos, pull requests, issues
|
|
9
|
+
*
|
|
10
|
+
* Zero domain-specific types. All filtering is based on field paths and operators.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Core Operators
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Filter operators - works with any data type
|
|
19
|
+
*/
|
|
20
|
+
export type Operator =
|
|
21
|
+
// Equality
|
|
22
|
+
| 'eq' // Equal to
|
|
23
|
+
| 'ne' // Not equal to
|
|
24
|
+
|
|
25
|
+
// Comparison (numbers, dates, strings)
|
|
26
|
+
| 'gt' // Greater than
|
|
27
|
+
| 'lt' // Less than
|
|
28
|
+
| 'gte' // Greater than or equal
|
|
29
|
+
| 'lte' // Less than or equal
|
|
30
|
+
|
|
31
|
+
// String operations
|
|
32
|
+
| 'contains' // String contains substring
|
|
33
|
+
| 'not_contains' // String does not contain substring
|
|
34
|
+
| 'startswith' // String starts with
|
|
35
|
+
| 'endswith' // String ends with
|
|
36
|
+
| 'matches' // Regex match
|
|
37
|
+
|
|
38
|
+
// Array/Set operations
|
|
39
|
+
| 'in' // Value is in array
|
|
40
|
+
| 'not_in' // Value is not in array
|
|
41
|
+
| 'has_any' // Array has any of these values
|
|
42
|
+
| 'has_all' // Array has all of these values
|
|
43
|
+
|
|
44
|
+
// Null checks
|
|
45
|
+
| 'isnull' // Field is null/undefined
|
|
46
|
+
| 'isnotnull' // Field is not null/undefined
|
|
47
|
+
|
|
48
|
+
// Tag operations
|
|
49
|
+
| 'has_tag' // Entity has tag
|
|
50
|
+
| 'not_has_tag' // Entity does not have tag
|
|
51
|
+
|
|
52
|
+
// Boolean
|
|
53
|
+
| 'is_true' // Boolean is true
|
|
54
|
+
| 'is_false'; // Boolean is false
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Field data types - for operator validation
|
|
58
|
+
*/
|
|
59
|
+
export type FieldType =
|
|
60
|
+
| 'string'
|
|
61
|
+
| 'number'
|
|
62
|
+
| 'boolean'
|
|
63
|
+
| 'date'
|
|
64
|
+
| 'array'
|
|
65
|
+
| 'object'
|
|
66
|
+
| 'tag'
|
|
67
|
+
| 'any';
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Filter Rules
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A single filter rule
|
|
75
|
+
*
|
|
76
|
+
* Examples:
|
|
77
|
+
* - { field_path: 'firm.stage', operator: 'eq', value: 'Series A' }
|
|
78
|
+
* - { field_path: 'recipe.cuisine', operator: 'in', value: ['Italian', 'French'] }
|
|
79
|
+
* - { field_path: 'contact.email', operator: 'isnotnull', value: null }
|
|
80
|
+
* - { field_path: 'organization.tags', operator: 'has_tag', value: 'enterprise' }
|
|
81
|
+
*/
|
|
82
|
+
export interface FilterRule {
|
|
83
|
+
/**
|
|
84
|
+
* Dot-notation path to field
|
|
85
|
+
* Examples: 'name', 'firm.stage', 'profile.linkedin_url', 'tags', 'metrics.arr_usd'
|
|
86
|
+
*/
|
|
87
|
+
field_path: string;
|
|
88
|
+
|
|
89
|
+
/** Comparison operator */
|
|
90
|
+
operator: Operator;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Value to compare against
|
|
94
|
+
* Type depends on operator:
|
|
95
|
+
* - eq/ne/gt/lt/gte/lte: any primitive
|
|
96
|
+
* - contains/startswith/endswith: string
|
|
97
|
+
* - in/not_in: array
|
|
98
|
+
* - isnull/isnotnull: null (value ignored)
|
|
99
|
+
* - has_tag/not_has_tag: string (tag name)
|
|
100
|
+
*/
|
|
101
|
+
value: any;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Optional: negate the rule result
|
|
105
|
+
* Default: false
|
|
106
|
+
*/
|
|
107
|
+
negate?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Filter logic for combining rules
|
|
112
|
+
*/
|
|
113
|
+
export type FilterLogic = 'AND' | 'OR';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Action to take when stage rules match
|
|
117
|
+
*/
|
|
118
|
+
export type MatchAction =
|
|
119
|
+
| 'continue' // Continue to next stage
|
|
120
|
+
| 'tag' // Add tags and stop processing
|
|
121
|
+
| 'tag_continue' // Add tags and continue to next stage
|
|
122
|
+
| 'output'; // Add to output and stop processing
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Action to take when stage rules don't match
|
|
126
|
+
*/
|
|
127
|
+
export type NoMatchAction =
|
|
128
|
+
| 'continue' // Continue to next stage
|
|
129
|
+
| 'exclude' // Exclude from output and stop processing
|
|
130
|
+
| 'tag_exclude'; // Add tags, exclude from output, stop processing
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Funnel Stages
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A single stage in a funnel
|
|
138
|
+
*
|
|
139
|
+
* Generic over entity type TEntity
|
|
140
|
+
*/
|
|
141
|
+
export interface FunnelStage<TEntity = any> {
|
|
142
|
+
/** Unique stage identifier */
|
|
143
|
+
id: string;
|
|
144
|
+
|
|
145
|
+
/** Stage execution order (0-indexed) */
|
|
146
|
+
order: number;
|
|
147
|
+
|
|
148
|
+
/** Human-readable stage name */
|
|
149
|
+
name: string;
|
|
150
|
+
|
|
151
|
+
/** Optional description */
|
|
152
|
+
description?: string;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* How to combine filter rules
|
|
156
|
+
* - AND: All rules must match
|
|
157
|
+
* - OR: At least one rule must match
|
|
158
|
+
*/
|
|
159
|
+
filter_logic: FilterLogic;
|
|
160
|
+
|
|
161
|
+
/** Filter rules to evaluate */
|
|
162
|
+
rules: FilterRule[];
|
|
163
|
+
|
|
164
|
+
/** Action when rules match */
|
|
165
|
+
match_action: MatchAction;
|
|
166
|
+
|
|
167
|
+
/** Action when rules don't match */
|
|
168
|
+
no_match_action: NoMatchAction;
|
|
169
|
+
|
|
170
|
+
/** Tags to add when rules match */
|
|
171
|
+
match_tags?: string[];
|
|
172
|
+
|
|
173
|
+
/** Tags to add when rules don't match */
|
|
174
|
+
no_match_tags?: string[];
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Context to add to entity when rules match
|
|
178
|
+
* Can be used to track why entity matched
|
|
179
|
+
* Examples:
|
|
180
|
+
* - { stage: 'qualified_leads', reason: 'high_fit_score' }
|
|
181
|
+
* - { tier: 'premium', discount: 0.2 }
|
|
182
|
+
*/
|
|
183
|
+
match_context?: Record<string, any>;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Optional: Custom evaluation function
|
|
187
|
+
* For complex logic that can't be expressed with rules
|
|
188
|
+
*/
|
|
189
|
+
custom_evaluator?: (entity: TEntity) => boolean;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// Funnel Definition
|
|
194
|
+
// ============================================================================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Funnel status
|
|
198
|
+
*/
|
|
199
|
+
export type FunnelStatus = 'draft' | 'active' | 'paused' | 'archived';
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Input entity types
|
|
203
|
+
*/
|
|
204
|
+
export type InputType = 'contacts' | 'organizations' | 'both' | 'any';
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A complete funnel definition
|
|
208
|
+
*
|
|
209
|
+
* Generic over entity type TEntity
|
|
210
|
+
*/
|
|
211
|
+
export interface Funnel<TEntity = any> {
|
|
212
|
+
/** Unique funnel identifier */
|
|
213
|
+
id: string;
|
|
214
|
+
|
|
215
|
+
/** Human-readable funnel name */
|
|
216
|
+
name: string;
|
|
217
|
+
|
|
218
|
+
/** Optional description */
|
|
219
|
+
description?: string;
|
|
220
|
+
|
|
221
|
+
/** Funnel status */
|
|
222
|
+
status: FunnelStatus;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Type of entities this funnel processes
|
|
226
|
+
* Used for field registry lookup
|
|
227
|
+
*/
|
|
228
|
+
input_type: InputType;
|
|
229
|
+
|
|
230
|
+
/** Ordered stages */
|
|
231
|
+
stages: FunnelStage<TEntity>[];
|
|
232
|
+
|
|
233
|
+
/** Created timestamp */
|
|
234
|
+
created_at: Date | string;
|
|
235
|
+
|
|
236
|
+
/** Last updated timestamp */
|
|
237
|
+
updated_at: Date | string;
|
|
238
|
+
|
|
239
|
+
/** Owner user ID */
|
|
240
|
+
owner_id?: string;
|
|
241
|
+
|
|
242
|
+
/** Team/company ID for multi-tenancy */
|
|
243
|
+
team_id?: string;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Optional: Tags to automatically add to all entities
|
|
247
|
+
* that complete the funnel
|
|
248
|
+
*/
|
|
249
|
+
completion_tags?: string[];
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Optional: Metadata for custom integrations
|
|
253
|
+
*/
|
|
254
|
+
metadata?: Record<string, any>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// Funnel Execution
|
|
259
|
+
// ============================================================================
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Funnel run status
|
|
263
|
+
*/
|
|
264
|
+
export type FunnelRunStatus =
|
|
265
|
+
| 'pending' // Queued, not started
|
|
266
|
+
| 'running' // Currently executing
|
|
267
|
+
| 'completed' // Finished successfully
|
|
268
|
+
| 'failed' // Error occurred
|
|
269
|
+
| 'cancelled'; // Manually stopped
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* How the funnel run was triggered
|
|
273
|
+
*/
|
|
274
|
+
export type TriggerType =
|
|
275
|
+
| 'manual' // User initiated
|
|
276
|
+
| 'scheduled' // Cron/scheduled run
|
|
277
|
+
| 'webhook' // External event
|
|
278
|
+
| 'api'; // API call
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Statistics for a single stage execution
|
|
282
|
+
*/
|
|
283
|
+
export interface StageStats {
|
|
284
|
+
/** Stage ID */
|
|
285
|
+
stage_id: string;
|
|
286
|
+
|
|
287
|
+
/** Stage name */
|
|
288
|
+
stage_name: string;
|
|
289
|
+
|
|
290
|
+
/** Entities that entered this stage */
|
|
291
|
+
input_count: number;
|
|
292
|
+
|
|
293
|
+
/** Entities that matched rules */
|
|
294
|
+
matched_count: number;
|
|
295
|
+
|
|
296
|
+
/** Entities that didn't match rules */
|
|
297
|
+
not_matched_count: number;
|
|
298
|
+
|
|
299
|
+
/** Entities excluded at this stage */
|
|
300
|
+
excluded_count: number;
|
|
301
|
+
|
|
302
|
+
/** Entities tagged at this stage */
|
|
303
|
+
tagged_count: number;
|
|
304
|
+
|
|
305
|
+
/** Entities that continued to next stage */
|
|
306
|
+
continued_count: number;
|
|
307
|
+
|
|
308
|
+
/** Execution time in milliseconds */
|
|
309
|
+
duration_ms?: number;
|
|
310
|
+
|
|
311
|
+
/** Error count */
|
|
312
|
+
error_count?: number;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* A funnel execution run
|
|
317
|
+
*/
|
|
318
|
+
export interface FunnelRun {
|
|
319
|
+
/** Unique run identifier */
|
|
320
|
+
id: string;
|
|
321
|
+
|
|
322
|
+
/** Funnel ID */
|
|
323
|
+
funnel_id: string;
|
|
324
|
+
|
|
325
|
+
/** Funnel snapshot (copy of funnel at run time) */
|
|
326
|
+
funnel?: Funnel;
|
|
327
|
+
|
|
328
|
+
/** Run status */
|
|
329
|
+
status: FunnelRunStatus;
|
|
330
|
+
|
|
331
|
+
/** How run was triggered */
|
|
332
|
+
trigger_type: TriggerType;
|
|
333
|
+
|
|
334
|
+
/** User who triggered (if manual) */
|
|
335
|
+
triggered_by?: string;
|
|
336
|
+
|
|
337
|
+
/** Started timestamp */
|
|
338
|
+
started_at: Date | string;
|
|
339
|
+
|
|
340
|
+
/** Completed timestamp */
|
|
341
|
+
completed_at?: Date | string;
|
|
342
|
+
|
|
343
|
+
/** Duration in milliseconds */
|
|
344
|
+
duration_ms?: number;
|
|
345
|
+
|
|
346
|
+
/** Total entities input */
|
|
347
|
+
total_input: number;
|
|
348
|
+
|
|
349
|
+
/** Total entities matched (in output) */
|
|
350
|
+
total_matched: number;
|
|
351
|
+
|
|
352
|
+
/** Total entities excluded */
|
|
353
|
+
total_excluded: number;
|
|
354
|
+
|
|
355
|
+
/** Total entities tagged */
|
|
356
|
+
total_tagged: number;
|
|
357
|
+
|
|
358
|
+
/** Per-stage statistics */
|
|
359
|
+
stage_stats: Record<string, StageStats>;
|
|
360
|
+
|
|
361
|
+
/** Error message (if failed) */
|
|
362
|
+
error?: string;
|
|
363
|
+
|
|
364
|
+
/** Stack trace (if failed) */
|
|
365
|
+
stack_trace?: string;
|
|
366
|
+
|
|
367
|
+
/** Metadata */
|
|
368
|
+
metadata?: Record<string, any>;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Result for a single entity processed through funnel
|
|
373
|
+
*/
|
|
374
|
+
export interface FunnelResult<TEntity = any> {
|
|
375
|
+
/** The entity that was processed */
|
|
376
|
+
entity: TEntity;
|
|
377
|
+
|
|
378
|
+
/** Whether entity matched and is in output */
|
|
379
|
+
matched: boolean;
|
|
380
|
+
|
|
381
|
+
/** Stage where entity was excluded (if excluded) */
|
|
382
|
+
excluded_at_stage?: string;
|
|
383
|
+
|
|
384
|
+
/** Tags accumulated during processing */
|
|
385
|
+
accumulated_tags: string[];
|
|
386
|
+
|
|
387
|
+
/** Context accumulated during processing */
|
|
388
|
+
context: Record<string, any>;
|
|
389
|
+
|
|
390
|
+
/** Stage-by-stage results */
|
|
391
|
+
stage_results?: StageResult[];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Result for a single stage evaluation
|
|
396
|
+
*/
|
|
397
|
+
export interface StageResult {
|
|
398
|
+
/** Stage ID */
|
|
399
|
+
stage_id: string;
|
|
400
|
+
|
|
401
|
+
/** Stage name */
|
|
402
|
+
stage_name: string;
|
|
403
|
+
|
|
404
|
+
/** Whether rules matched */
|
|
405
|
+
matched: boolean;
|
|
406
|
+
|
|
407
|
+
/** Rule evaluation results */
|
|
408
|
+
rule_results?: RuleResult[];
|
|
409
|
+
|
|
410
|
+
/** Action taken */
|
|
411
|
+
action: MatchAction | NoMatchAction;
|
|
412
|
+
|
|
413
|
+
/** Tags added */
|
|
414
|
+
tags_added?: string[];
|
|
415
|
+
|
|
416
|
+
/** Context added */
|
|
417
|
+
context_added?: Record<string, any>;
|
|
418
|
+
|
|
419
|
+
/** Whether entity was excluded */
|
|
420
|
+
excluded: boolean;
|
|
421
|
+
|
|
422
|
+
/** Whether entity continued to next stage */
|
|
423
|
+
continued: boolean;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Result for a single rule evaluation
|
|
428
|
+
*/
|
|
429
|
+
export interface RuleResult {
|
|
430
|
+
/** Rule field path */
|
|
431
|
+
field_path: string;
|
|
432
|
+
|
|
433
|
+
/** Rule operator */
|
|
434
|
+
operator: Operator;
|
|
435
|
+
|
|
436
|
+
/** Value compared against */
|
|
437
|
+
value: any;
|
|
438
|
+
|
|
439
|
+
/** Actual value from entity */
|
|
440
|
+
actual_value: any;
|
|
441
|
+
|
|
442
|
+
/** Whether rule matched */
|
|
443
|
+
matched: boolean;
|
|
444
|
+
|
|
445
|
+
/** Error (if evaluation failed) */
|
|
446
|
+
error?: string;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ============================================================================
|
|
450
|
+
// Field Registry
|
|
451
|
+
// ============================================================================
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Valid operators for a field type
|
|
455
|
+
*/
|
|
456
|
+
export type ValidOperators = Operator[];
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Field constraints
|
|
460
|
+
*/
|
|
461
|
+
export interface FieldConstraints {
|
|
462
|
+
/** Minimum value (numbers, dates) */
|
|
463
|
+
min_value?: number | string;
|
|
464
|
+
|
|
465
|
+
/** Maximum value (numbers, dates) */
|
|
466
|
+
max_value?: number | string;
|
|
467
|
+
|
|
468
|
+
/** Allowed values (enums) */
|
|
469
|
+
choices?: any[];
|
|
470
|
+
|
|
471
|
+
/** Regex pattern */
|
|
472
|
+
pattern?: string;
|
|
473
|
+
|
|
474
|
+
/** Required field */
|
|
475
|
+
required?: boolean;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Field definition in registry
|
|
480
|
+
*
|
|
481
|
+
* Describes what fields are available for filtering
|
|
482
|
+
* on a given entity type
|
|
483
|
+
*/
|
|
484
|
+
export interface FieldDefinition {
|
|
485
|
+
/** Unique field identifier (dot-notation path) */
|
|
486
|
+
name: string;
|
|
487
|
+
|
|
488
|
+
/** Human-readable label */
|
|
489
|
+
label: string;
|
|
490
|
+
|
|
491
|
+
/** Field data type */
|
|
492
|
+
type: FieldType;
|
|
493
|
+
|
|
494
|
+
/** Valid operators for this field */
|
|
495
|
+
operators: ValidOperators;
|
|
496
|
+
|
|
497
|
+
/** Field category (for UI grouping) */
|
|
498
|
+
category?: string;
|
|
499
|
+
|
|
500
|
+
/** Optional description */
|
|
501
|
+
description?: string;
|
|
502
|
+
|
|
503
|
+
/** Field constraints */
|
|
504
|
+
constraints?: FieldConstraints;
|
|
505
|
+
|
|
506
|
+
/** Whether field is sortable */
|
|
507
|
+
sortable?: boolean;
|
|
508
|
+
|
|
509
|
+
/** Whether field is searchable */
|
|
510
|
+
searchable?: boolean;
|
|
511
|
+
|
|
512
|
+
/** Example values (for UI help) */
|
|
513
|
+
examples?: any[];
|
|
514
|
+
|
|
515
|
+
/** Related fields (for UI suggestions) */
|
|
516
|
+
related_fields?: string[];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Field registry for an entity type
|
|
521
|
+
*
|
|
522
|
+
* Maps field paths to their definitions
|
|
523
|
+
*/
|
|
524
|
+
export interface FieldRegistry {
|
|
525
|
+
/** Entity type this registry is for */
|
|
526
|
+
entity_type: string;
|
|
527
|
+
|
|
528
|
+
/** Available fields */
|
|
529
|
+
fields: FieldDefinition[];
|
|
530
|
+
|
|
531
|
+
/** Field lookup by name */
|
|
532
|
+
field_map?: Map<string, FieldDefinition>;
|
|
533
|
+
|
|
534
|
+
/** Categories (for UI grouping) */
|
|
535
|
+
categories?: string[];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ============================================================================
|
|
539
|
+
// Utility Types
|
|
540
|
+
// ============================================================================
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Filter configuration (rules + logic)
|
|
544
|
+
*/
|
|
545
|
+
export interface FilterConfig {
|
|
546
|
+
logic: FilterLogic;
|
|
547
|
+
rules: FilterRule[];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Partial type helper
|
|
552
|
+
*/
|
|
553
|
+
export type DeepPartial<T> = {
|
|
554
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Extract field paths from object type
|
|
559
|
+
* Helper for type-safe field path completion
|
|
560
|
+
*/
|
|
561
|
+
export type FieldPath<T, Prefix extends string = ''> = {
|
|
562
|
+
[K in keyof T]: T[K] extends object
|
|
563
|
+
? K extends string
|
|
564
|
+
? `${Prefix}${K}` | FieldPath<T[K], `${Prefix}${K}.`>
|
|
565
|
+
: never
|
|
566
|
+
: K extends string
|
|
567
|
+
? `${Prefix}${K}`
|
|
568
|
+
: never;
|
|
569
|
+
}[keyof T];
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Funnel creation input (omits auto-generated fields)
|
|
573
|
+
*/
|
|
574
|
+
export type CreateFunnelInput<TEntity = any> = Omit<
|
|
575
|
+
Funnel<TEntity>,
|
|
576
|
+
'id' | 'created_at' | 'updated_at'
|
|
577
|
+
>;
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Funnel update input (all fields optional except id)
|
|
581
|
+
*/
|
|
582
|
+
export type UpdateFunnelInput<TEntity = any> = DeepPartial<Funnel<TEntity>> & {
|
|
583
|
+
id: string;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Stage creation input
|
|
588
|
+
*/
|
|
589
|
+
export type CreateStageInput<TEntity = any> = Omit<FunnelStage<TEntity>, 'id'>;
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Stage update input
|
|
593
|
+
*/
|
|
594
|
+
export type UpdateStageInput<TEntity = any> = DeepPartial<FunnelStage<TEntity>> & {
|
|
595
|
+
id: string;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// ============================================================================
|
|
599
|
+
// Type Guards
|
|
600
|
+
// ============================================================================
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Type guard: is value a valid Funnel?
|
|
604
|
+
*/
|
|
605
|
+
export function isFunnel<TEntity = any>(value: unknown): value is Funnel<TEntity> {
|
|
606
|
+
const f = value as Funnel<TEntity>;
|
|
607
|
+
return (
|
|
608
|
+
typeof f === 'object' &&
|
|
609
|
+
f !== null &&
|
|
610
|
+
typeof f.id === 'string' &&
|
|
611
|
+
typeof f.name === 'string' &&
|
|
612
|
+
['draft', 'active', 'paused', 'archived'].includes(f.status) &&
|
|
613
|
+
Array.isArray(f.stages)
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Type guard: is value a valid FunnelStage?
|
|
619
|
+
*/
|
|
620
|
+
export function isStage<TEntity = any>(value: unknown): value is FunnelStage<TEntity> {
|
|
621
|
+
const s = value as FunnelStage<TEntity>;
|
|
622
|
+
return (
|
|
623
|
+
typeof s === 'object' &&
|
|
624
|
+
s !== null &&
|
|
625
|
+
typeof s.id === 'string' &&
|
|
626
|
+
typeof s.name === 'string' &&
|
|
627
|
+
typeof s.order === 'number' &&
|
|
628
|
+
['AND', 'OR'].includes(s.filter_logic) &&
|
|
629
|
+
Array.isArray(s.rules)
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Type guard: is value a valid FilterRule?
|
|
635
|
+
*/
|
|
636
|
+
export function isFilterRule(value: unknown): value is FilterRule {
|
|
637
|
+
const r = value as FilterRule;
|
|
638
|
+
return (
|
|
639
|
+
typeof r === 'object' &&
|
|
640
|
+
r !== null &&
|
|
641
|
+
typeof r.field_path === 'string' &&
|
|
642
|
+
typeof r.operator === 'string' &&
|
|
643
|
+
r.value !== undefined
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Type guard: is value a valid FunnelRun?
|
|
649
|
+
*/
|
|
650
|
+
export function isFunnelRun(value: unknown): value is FunnelRun {
|
|
651
|
+
const r = value as FunnelRun;
|
|
652
|
+
return (
|
|
653
|
+
typeof r === 'object' &&
|
|
654
|
+
r !== null &&
|
|
655
|
+
typeof r.id === 'string' &&
|
|
656
|
+
typeof r.funnel_id === 'string' &&
|
|
657
|
+
['pending', 'running', 'completed', 'failed', 'cancelled'].includes(r.status)
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Type guard: is value a valid FunnelResult?
|
|
663
|
+
*/
|
|
664
|
+
export function isFunnelResult<TEntity = any>(
|
|
665
|
+
value: unknown
|
|
666
|
+
): value is FunnelResult<TEntity> {
|
|
667
|
+
const r = value as FunnelResult<TEntity>;
|
|
668
|
+
return (
|
|
669
|
+
typeof r === 'object' &&
|
|
670
|
+
r !== null &&
|
|
671
|
+
r.entity !== undefined &&
|
|
672
|
+
typeof r.matched === 'boolean' &&
|
|
673
|
+
Array.isArray(r.accumulated_tags)
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Type guard: is value a valid FieldDefinition?
|
|
679
|
+
*/
|
|
680
|
+
export function isFieldDefinition(value: unknown): value is FieldDefinition {
|
|
681
|
+
const f = value as FieldDefinition;
|
|
682
|
+
return (
|
|
683
|
+
typeof f === 'object' &&
|
|
684
|
+
f !== null &&
|
|
685
|
+
typeof f.name === 'string' &&
|
|
686
|
+
typeof f.label === 'string' &&
|
|
687
|
+
typeof f.type === 'string' &&
|
|
688
|
+
Array.isArray(f.operators)
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ============================================================================
|
|
693
|
+
// Validation Helpers
|
|
694
|
+
// ============================================================================
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get valid operators for a field type
|
|
698
|
+
*/
|
|
699
|
+
export function getValidOperators(fieldType: FieldType): ValidOperators {
|
|
700
|
+
switch (fieldType) {
|
|
701
|
+
case 'string':
|
|
702
|
+
return [
|
|
703
|
+
'eq', 'ne', 'contains', 'not_contains', 'startswith', 'endswith',
|
|
704
|
+
'matches', 'in', 'not_in', 'isnull', 'isnotnull'
|
|
705
|
+
];
|
|
706
|
+
|
|
707
|
+
case 'number':
|
|
708
|
+
return [
|
|
709
|
+
'eq', 'ne', 'gt', 'lt', 'gte', 'lte',
|
|
710
|
+
'in', 'not_in', 'isnull', 'isnotnull'
|
|
711
|
+
];
|
|
712
|
+
|
|
713
|
+
case 'boolean':
|
|
714
|
+
return ['eq', 'ne', 'is_true', 'is_false', 'isnull', 'isnotnull'];
|
|
715
|
+
|
|
716
|
+
case 'date':
|
|
717
|
+
return [
|
|
718
|
+
'eq', 'ne', 'gt', 'lt', 'gte', 'lte',
|
|
719
|
+
'isnull', 'isnotnull'
|
|
720
|
+
];
|
|
721
|
+
|
|
722
|
+
case 'array':
|
|
723
|
+
return [
|
|
724
|
+
'in', 'not_in', 'has_any', 'has_all',
|
|
725
|
+
'isnull', 'isnotnull'
|
|
726
|
+
];
|
|
727
|
+
|
|
728
|
+
case 'tag':
|
|
729
|
+
return ['has_tag', 'not_has_tag'];
|
|
730
|
+
|
|
731
|
+
case 'object':
|
|
732
|
+
return ['isnull', 'isnotnull'];
|
|
733
|
+
|
|
734
|
+
case 'any':
|
|
735
|
+
default:
|
|
736
|
+
return [
|
|
737
|
+
'eq', 'ne', 'gt', 'lt', 'gte', 'lte',
|
|
738
|
+
'contains', 'not_contains', 'startswith', 'endswith',
|
|
739
|
+
'in', 'not_in', 'isnull', 'isnotnull'
|
|
740
|
+
];
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Validate operator is allowed for field type
|
|
746
|
+
*/
|
|
747
|
+
export function isValidOperator(
|
|
748
|
+
operator: Operator,
|
|
749
|
+
fieldType: FieldType
|
|
750
|
+
): boolean {
|
|
751
|
+
const validOps = getValidOperators(fieldType);
|
|
752
|
+
return validOps.includes(operator);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Validate filter rule
|
|
757
|
+
*/
|
|
758
|
+
export function validateFilterRule(rule: FilterRule): string[] {
|
|
759
|
+
const errors: string[] = [];
|
|
760
|
+
|
|
761
|
+
if (!rule.field_path) {
|
|
762
|
+
errors.push('field_path is required');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (!rule.operator) {
|
|
766
|
+
errors.push('operator is required');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Value required for most operators
|
|
770
|
+
const nullOps = ['isnull', 'isnotnull', 'is_true', 'is_false'];
|
|
771
|
+
if (!nullOps.includes(rule.operator) && rule.value === undefined) {
|
|
772
|
+
errors.push(`value is required for operator '${rule.operator}'`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return errors;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Validate funnel stage
|
|
780
|
+
*/
|
|
781
|
+
export function validateStage<TEntity = any>(
|
|
782
|
+
stage: FunnelStage<TEntity>
|
|
783
|
+
): string[] {
|
|
784
|
+
const errors: string[] = [];
|
|
785
|
+
|
|
786
|
+
if (!stage.name) {
|
|
787
|
+
errors.push('name is required');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (typeof stage.order !== 'number') {
|
|
791
|
+
errors.push('order must be a number');
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (!['AND', 'OR'].includes(stage.filter_logic)) {
|
|
795
|
+
errors.push('filter_logic must be AND or OR');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (!Array.isArray(stage.rules)) {
|
|
799
|
+
errors.push('rules must be an array');
|
|
800
|
+
} else {
|
|
801
|
+
stage.rules.forEach((rule, i) => {
|
|
802
|
+
const ruleErrors = validateFilterRule(rule);
|
|
803
|
+
ruleErrors.forEach(err => errors.push(`rules[${i}]: ${err}`));
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return errors;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Validate funnel
|
|
812
|
+
*/
|
|
813
|
+
export function validateFunnel<TEntity = any>(
|
|
814
|
+
funnel: Funnel<TEntity>
|
|
815
|
+
): string[] {
|
|
816
|
+
const errors: string[] = [];
|
|
817
|
+
|
|
818
|
+
if (!funnel.name) {
|
|
819
|
+
errors.push('name is required');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (!['draft', 'active', 'paused', 'archived'].includes(funnel.status)) {
|
|
823
|
+
errors.push('status must be draft, active, paused, or archived');
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (!Array.isArray(funnel.stages)) {
|
|
827
|
+
errors.push('stages must be an array');
|
|
828
|
+
} else {
|
|
829
|
+
funnel.stages.forEach((stage, i) => {
|
|
830
|
+
const stageErrors = validateStage(stage);
|
|
831
|
+
stageErrors.forEach(err => errors.push(`stages[${i}]: ${err}`));
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// Check stage order is sequential
|
|
835
|
+
const orders = funnel.stages.map(s => s.order).sort((a, b) => a - b);
|
|
836
|
+
const expectedOrders = Array.from({ length: orders.length }, (_, i) => i);
|
|
837
|
+
if (JSON.stringify(orders) !== JSON.stringify(expectedOrders)) {
|
|
838
|
+
errors.push('stage orders must be sequential starting from 0');
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return errors;
|
|
843
|
+
}
|