@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
package/package.json
CHANGED
|
@@ -1,41 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/funnels",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Brutally generic filtering pipeline package for any Simpli product",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
7
|
-
"
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
9
8
|
"exports": {
|
|
10
|
-
".":
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"./core": {
|
|
16
|
-
"types": "./dist/core/index.d.ts",
|
|
17
|
-
"import": "./dist/core/index.js",
|
|
18
|
-
"require": "./dist/core/index.cjs"
|
|
19
|
-
},
|
|
20
|
-
"./components": {
|
|
21
|
-
"types": "./dist/components/index.d.ts",
|
|
22
|
-
"import": "./dist/components/index.js",
|
|
23
|
-
"require": "./dist/components/index.cjs"
|
|
24
|
-
},
|
|
25
|
-
"./components.css": "./dist/components/index.css",
|
|
26
|
-
"./hooks": {
|
|
27
|
-
"types": "./dist/hooks/index.d.ts",
|
|
28
|
-
"import": "./dist/hooks/index.js",
|
|
29
|
-
"require": "./dist/hooks/index.cjs"
|
|
30
|
-
},
|
|
31
|
-
"./store": {
|
|
32
|
-
"types": "./dist/store/index.d.ts",
|
|
33
|
-
"import": "./dist/store/index.js",
|
|
34
|
-
"require": "./dist/store/index.cjs"
|
|
35
|
-
}
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./core": "./src/core/index.ts",
|
|
11
|
+
"./components": "./src/components/index.ts",
|
|
12
|
+
"./hooks": "./src/hooks/index.ts",
|
|
13
|
+
"./store": "./src/store/index.ts"
|
|
36
14
|
},
|
|
37
15
|
"files": [
|
|
38
|
-
"
|
|
16
|
+
"src",
|
|
39
17
|
"README.md",
|
|
40
18
|
"LICENSE"
|
|
41
19
|
],
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# @simpli/funnels - API Client
|
|
2
|
+
|
|
3
|
+
Generic API client for funnel operations using the adapter pattern for flexible HTTP integration.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The API client provides:
|
|
8
|
+
- **Adapter Pattern**: Inject your own HTTP client with custom auth, headers, error handling
|
|
9
|
+
- **Type Safety**: Full TypeScript support with generics for entity types
|
|
10
|
+
- **Comprehensive Coverage**: All funnel CRUD operations, stage management, run operations
|
|
11
|
+
- **Error Handling**: Standardized error types with status codes
|
|
12
|
+
- **Testing Support**: Mock adapter for easy unit testing
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Basic Usage (Default Adapter)
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { FunnelApiClient, FetchAdapter } from '@simpli/funnels';
|
|
20
|
+
|
|
21
|
+
// Create adapter with default fetch
|
|
22
|
+
const adapter = new FetchAdapter({
|
|
23
|
+
headers: {
|
|
24
|
+
'Authorization': 'Bearer your-jwt-token',
|
|
25
|
+
},
|
|
26
|
+
timeout: 10000, // 10 seconds
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Create client
|
|
30
|
+
const client = new FunnelApiClient(adapter, 'https://api.example.com');
|
|
31
|
+
|
|
32
|
+
// List funnels
|
|
33
|
+
const funnels = await client.listFunnels({ status: 'active' });
|
|
34
|
+
|
|
35
|
+
// Get funnel
|
|
36
|
+
const funnel = await client.getFunnel('funnel-123');
|
|
37
|
+
|
|
38
|
+
// Run funnel
|
|
39
|
+
const run = await client.runFunnel('funnel-123', { trigger_type: 'manual' });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Custom Adapter (Advanced)
|
|
43
|
+
|
|
44
|
+
For apps with custom auth or request handling:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { ApiAdapter, createApiError } from '@simpli/funnels';
|
|
48
|
+
|
|
49
|
+
class CustomAdapter implements ApiAdapter {
|
|
50
|
+
constructor(private authToken: string) {}
|
|
51
|
+
|
|
52
|
+
async get<T>(url: string, params?: Record<string, any>): Promise<T> {
|
|
53
|
+
const response = await fetch(url, {
|
|
54
|
+
headers: {
|
|
55
|
+
'Authorization': `Bearer ${this.authToken}`,
|
|
56
|
+
'X-Tenant-ID': 'my-tenant',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw createApiError('Request failed', response.status);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return response.json();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async post<T>(url: string, data: any): Promise<T> {
|
|
68
|
+
// Custom implementation
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async patch<T>(url: string, data: any): Promise<T> {
|
|
72
|
+
// Custom implementation
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async delete<T>(url: string): Promise<T> {
|
|
76
|
+
// Custom implementation
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Use custom adapter
|
|
81
|
+
const adapter = new CustomAdapter('your-token');
|
|
82
|
+
const client = new FunnelApiClient(adapter, 'https://api.example.com');
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API Reference
|
|
86
|
+
|
|
87
|
+
### Funnel Operations
|
|
88
|
+
|
|
89
|
+
#### List Funnels
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const funnels = await client.listFunnels({
|
|
93
|
+
status: 'active',
|
|
94
|
+
page: 1,
|
|
95
|
+
page_size: 20,
|
|
96
|
+
ordering: '-created_at',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Response
|
|
100
|
+
interface PaginatedResponse<Funnel> {
|
|
101
|
+
count: number;
|
|
102
|
+
next: string | null;
|
|
103
|
+
previous: string | null;
|
|
104
|
+
results: Funnel[];
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Get Funnel
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const funnel = await client.getFunnel('funnel-123');
|
|
112
|
+
// Returns: Funnel<TEntity>
|
|
113
|
+
// Throws: ApiError with status 404 if not found
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Create Funnel
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const newFunnel = await client.createFunnel({
|
|
120
|
+
name: 'Investor Qualification',
|
|
121
|
+
status: 'draft',
|
|
122
|
+
input_type: 'contacts',
|
|
123
|
+
stages: [
|
|
124
|
+
{
|
|
125
|
+
id: 'stage-1',
|
|
126
|
+
order: 0,
|
|
127
|
+
name: 'Check Firm Stage',
|
|
128
|
+
filter_logic: 'AND',
|
|
129
|
+
rules: [
|
|
130
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
131
|
+
],
|
|
132
|
+
match_action: 'continue',
|
|
133
|
+
no_match_action: 'exclude',
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Update Funnel
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const updated = await client.updateFunnel('funnel-123', {
|
|
143
|
+
name: 'Updated Name',
|
|
144
|
+
status: 'active',
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### Delete Funnel
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
await client.deleteFunnel('funnel-123');
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Stage Operations
|
|
155
|
+
|
|
156
|
+
#### Create Stage
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const stage = await client.createStage('funnel-123', {
|
|
160
|
+
order: 1,
|
|
161
|
+
name: 'Geography Check',
|
|
162
|
+
filter_logic: 'AND',
|
|
163
|
+
rules: [
|
|
164
|
+
{ field_path: 'firm.location', operator: 'eq', value: 'San Francisco' },
|
|
165
|
+
],
|
|
166
|
+
match_action: 'tag_continue',
|
|
167
|
+
match_tags: ['sf_based'],
|
|
168
|
+
no_match_action: 'continue',
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Update Stage
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const updated = await client.updateStage('funnel-123', 'stage-1', {
|
|
176
|
+
name: 'Updated Stage Name',
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### Delete Stage
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
await client.deleteStage('funnel-123', 'stage-1');
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Run Operations
|
|
187
|
+
|
|
188
|
+
#### Trigger Run
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const run = await client.runFunnel('funnel-123', {
|
|
192
|
+
trigger_type: 'manual',
|
|
193
|
+
metadata: { source: 'dashboard' },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Returns: FunnelRun
|
|
197
|
+
// {
|
|
198
|
+
// id: 'run-456',
|
|
199
|
+
// funnel_id: 'funnel-123',
|
|
200
|
+
// status: 'pending' | 'running',
|
|
201
|
+
// ...
|
|
202
|
+
// }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### Get Run History
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const runs = await client.getFunnelRuns('funnel-123', {
|
|
209
|
+
status: 'completed',
|
|
210
|
+
page: 1,
|
|
211
|
+
page_size: 10,
|
|
212
|
+
ordering: '-started_at',
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Get Single Run
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const run = await client.getFunnelRun('run-456');
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### Get Run Results
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// All results
|
|
226
|
+
const results = await client.getFunnelResults('run-456');
|
|
227
|
+
|
|
228
|
+
// Only matched entities
|
|
229
|
+
const matched = await client.getFunnelResults('run-456', { matched: true });
|
|
230
|
+
|
|
231
|
+
// Response
|
|
232
|
+
interface FunnelResult<TEntity> {
|
|
233
|
+
entity: TEntity;
|
|
234
|
+
matched: boolean;
|
|
235
|
+
excluded_at_stage?: string;
|
|
236
|
+
accumulated_tags: string[];
|
|
237
|
+
context: Record<string, any>;
|
|
238
|
+
stage_results?: StageResult[];
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### Cancel Run
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
const cancelled = await client.cancelFunnelRun('run-456');
|
|
246
|
+
// Returns: FunnelRun with status 'cancelled'
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Preview Operations
|
|
250
|
+
|
|
251
|
+
#### Server-Side Preview
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const preview = await client.previewFunnelServer('funnel-123', [
|
|
255
|
+
{ id: 'entity-1', firm: { stage: 'Series A' } },
|
|
256
|
+
{ id: 'entity-2', firm: { stage: 'Seed' } },
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
// Returns: PreviewResult
|
|
260
|
+
// {
|
|
261
|
+
// matched_entities: TEntity[],
|
|
262
|
+
// excluded_entities: TEntity[],
|
|
263
|
+
// stage_breakdown: [...],
|
|
264
|
+
// total_input: 2,
|
|
265
|
+
// total_matched: 1,
|
|
266
|
+
// total_excluded: 1,
|
|
267
|
+
// }
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Error Handling
|
|
271
|
+
|
|
272
|
+
All methods throw `ApiError` on failure:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { isApiError } from '@simpli/funnels';
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const funnel = await client.getFunnel('invalid');
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (isApiError(error)) {
|
|
281
|
+
console.error('Status:', error.status); // 404
|
|
282
|
+
console.error('Message:', error.message); // 'Not found'
|
|
283
|
+
console.error('Response:', error.response); // Full response body
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Testing
|
|
289
|
+
|
|
290
|
+
Use `MockAdapter` for unit tests:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { describe, it, expect } from 'vitest';
|
|
294
|
+
import { FunnelApiClient } from '@simpli/funnels';
|
|
295
|
+
import type { ApiAdapter } from '@simpli/funnels';
|
|
296
|
+
|
|
297
|
+
class MockAdapter implements ApiAdapter {
|
|
298
|
+
private responses = new Map<string, any>();
|
|
299
|
+
|
|
300
|
+
mockResponse(url: string, response: any): void {
|
|
301
|
+
this.responses.set(url, response);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async get<T>(url: string): Promise<T> {
|
|
305
|
+
return this.responses.get(url);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Implement other methods...
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
describe('MyFunnelComponent', () => {
|
|
312
|
+
it('should fetch funnels', async () => {
|
|
313
|
+
const adapter = new MockAdapter();
|
|
314
|
+
const client = new FunnelApiClient(adapter, 'https://api.test.com');
|
|
315
|
+
|
|
316
|
+
adapter.mockResponse('https://api.test.com/api/v1/funnels/', {
|
|
317
|
+
count: 1,
|
|
318
|
+
results: [{ id: 'funnel-1', name: 'Test' }],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const funnels = await client.listFunnels();
|
|
322
|
+
expect(funnels.count).toBe(1);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Type Safety with Generics
|
|
328
|
+
|
|
329
|
+
Use type generics for entity-specific types:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
interface Investor {
|
|
333
|
+
id: string;
|
|
334
|
+
name: string;
|
|
335
|
+
firm: {
|
|
336
|
+
stage: string;
|
|
337
|
+
location: string;
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Type-safe client
|
|
342
|
+
const client = new FunnelApiClient(adapter, baseUrl);
|
|
343
|
+
|
|
344
|
+
// Type-safe operations
|
|
345
|
+
const funnel = await client.getFunnel<Investor>('funnel-123');
|
|
346
|
+
// funnel.stages[0] knows about Investor type
|
|
347
|
+
|
|
348
|
+
const results = await client.getFunnelResults<Investor>('run-456');
|
|
349
|
+
// results.results[0].entity is typed as Investor
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Backend API Endpoints
|
|
353
|
+
|
|
354
|
+
The client expects these endpoints on the backend:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
GET /api/v1/funnels/ - List funnels
|
|
358
|
+
GET /api/v1/funnels/{id}/ - Get funnel detail
|
|
359
|
+
POST /api/v1/funnels/ - Create funnel
|
|
360
|
+
PATCH /api/v1/funnels/{id}/ - Update funnel
|
|
361
|
+
DELETE /api/v1/funnels/{id}/ - Delete funnel
|
|
362
|
+
|
|
363
|
+
POST /api/v1/funnels/{id}/stages/ - Create stage
|
|
364
|
+
PATCH /api/v1/funnels/{id}/stages/{sid}/ - Update stage
|
|
365
|
+
DELETE /api/v1/funnels/{id}/stages/{sid}/ - Delete stage
|
|
366
|
+
|
|
367
|
+
POST /api/v1/funnels/{id}/run/ - Trigger run
|
|
368
|
+
GET /api/v1/funnels/{id}/runs/ - Get run history
|
|
369
|
+
GET /api/v1/funnel-runs/{id}/ - Get run detail
|
|
370
|
+
GET /api/v1/funnel-runs/{id}/results/ - Get run results
|
|
371
|
+
POST /api/v1/funnel-runs/{id}/cancel/ - Cancel run
|
|
372
|
+
|
|
373
|
+
POST /api/v1/funnels/{id}/preview/ - Preview with sample data
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Advanced Configuration
|
|
377
|
+
|
|
378
|
+
### FetchAdapter Options
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const adapter = new FetchAdapter({
|
|
382
|
+
// Custom headers (auth, tenant, etc)
|
|
383
|
+
headers: {
|
|
384
|
+
'Authorization': 'Bearer token',
|
|
385
|
+
'X-Tenant-ID': 'tenant-123',
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
// Request timeout (default: 30000ms)
|
|
389
|
+
timeout: 10000,
|
|
390
|
+
|
|
391
|
+
// Custom response parser
|
|
392
|
+
parseResponse: async (response) => {
|
|
393
|
+
const data = await response.json();
|
|
394
|
+
return data.data; // Unwrap nested response
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
// Error callback (logging, monitoring)
|
|
398
|
+
onError: (error) => {
|
|
399
|
+
console.error('API error:', error);
|
|
400
|
+
// Send to monitoring service
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### URL Handling
|
|
406
|
+
|
|
407
|
+
The client automatically:
|
|
408
|
+
- Removes trailing slashes from `baseUrl`
|
|
409
|
+
- Builds full URLs with `baseUrl + path`
|
|
410
|
+
- Serializes query parameters (arrays, nulls, etc)
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// All equivalent
|
|
414
|
+
const client1 = new FunnelApiClient(adapter, 'https://api.example.com');
|
|
415
|
+
const client2 = new FunnelApiClient(adapter, 'https://api.example.com/');
|
|
416
|
+
|
|
417
|
+
await client1.listFunnels({ page: 1, page_size: 10 });
|
|
418
|
+
// GET https://api.example.com/api/v1/funnels/?page=1&page_size=10
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Best Practices
|
|
422
|
+
|
|
423
|
+
1. **Reuse Client Instances**: Create one client per API and reuse it
|
|
424
|
+
2. **Use Type Generics**: Type your entities for better autocomplete and type safety
|
|
425
|
+
3. **Handle Errors**: Always wrap API calls in try/catch
|
|
426
|
+
4. **Mock for Tests**: Use MockAdapter for unit tests, avoid hitting real API
|
|
427
|
+
5. **Configure Timeouts**: Set appropriate timeouts for your use case
|
|
428
|
+
6. **Add Error Callbacks**: Use `onError` for logging and monitoring
|
|
429
|
+
|
|
430
|
+
## Examples
|
|
431
|
+
|
|
432
|
+
### Complete Example: Investor Qualification
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
import { FunnelApiClient, FetchAdapter } from '@simpli/funnels';
|
|
436
|
+
|
|
437
|
+
interface Investor {
|
|
438
|
+
id: string;
|
|
439
|
+
name: string;
|
|
440
|
+
firm: { stage: string; location: string };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Setup
|
|
444
|
+
const adapter = new FetchAdapter({
|
|
445
|
+
headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` },
|
|
446
|
+
});
|
|
447
|
+
const client = new FunnelApiClient(adapter, 'https://api.startsimpli.com');
|
|
448
|
+
|
|
449
|
+
// Create funnel
|
|
450
|
+
const funnel = await client.createFunnel<Investor>({
|
|
451
|
+
name: 'Series A Investor Qualification',
|
|
452
|
+
status: 'active',
|
|
453
|
+
input_type: 'contacts',
|
|
454
|
+
stages: [
|
|
455
|
+
{
|
|
456
|
+
order: 0,
|
|
457
|
+
name: 'Check Firm Stage',
|
|
458
|
+
filter_logic: 'AND',
|
|
459
|
+
rules: [
|
|
460
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
461
|
+
],
|
|
462
|
+
match_action: 'continue',
|
|
463
|
+
no_match_action: 'exclude',
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
order: 1,
|
|
467
|
+
name: 'Check Geography',
|
|
468
|
+
filter_logic: 'OR',
|
|
469
|
+
rules: [
|
|
470
|
+
{ field_path: 'firm.location', operator: 'eq', value: 'San Francisco' },
|
|
471
|
+
{ field_path: 'firm.location', operator: 'eq', value: 'New York' },
|
|
472
|
+
],
|
|
473
|
+
match_action: 'tag_continue',
|
|
474
|
+
match_tags: ['target_geography'],
|
|
475
|
+
no_match_action: 'continue',
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
order: 2,
|
|
479
|
+
name: 'Final Output',
|
|
480
|
+
filter_logic: 'AND',
|
|
481
|
+
rules: [],
|
|
482
|
+
match_action: 'output',
|
|
483
|
+
no_match_action: 'output',
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Run funnel
|
|
489
|
+
const run = await client.runFunnel(funnel.id, { trigger_type: 'manual' });
|
|
490
|
+
|
|
491
|
+
// Poll for completion
|
|
492
|
+
let completed = false;
|
|
493
|
+
while (!completed) {
|
|
494
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
495
|
+
const status = await client.getFunnelRun(run.id);
|
|
496
|
+
if (status.status === 'completed' || status.status === 'failed') {
|
|
497
|
+
completed = true;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Get results
|
|
502
|
+
const results = await client.getFunnelResults<Investor>(run.id, { matched: true });
|
|
503
|
+
console.log(`Matched ${results.count} investors`);
|
|
504
|
+
results.results.forEach((result) => {
|
|
505
|
+
console.log(`- ${result.entity.name} (tags: ${result.accumulated_tags.join(', ')})`);
|
|
506
|
+
});
|
|
507
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApiAdapter Interface
|
|
3
|
+
*
|
|
4
|
+
* Generic HTTP adapter for flexible API integration.
|
|
5
|
+
* Consumers can provide their own implementation to handle:
|
|
6
|
+
* - Custom authentication (JWT, API keys, OAuth)
|
|
7
|
+
* - Custom headers (tenant IDs, correlation IDs)
|
|
8
|
+
* - Custom error handling
|
|
9
|
+
* - Request/response transformation
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generic API adapter interface
|
|
16
|
+
*
|
|
17
|
+
* Allows applications to inject their own HTTP client
|
|
18
|
+
* with custom auth, headers, error handling, etc.
|
|
19
|
+
*/
|
|
20
|
+
export interface ApiAdapter {
|
|
21
|
+
/**
|
|
22
|
+
* HTTP GET request
|
|
23
|
+
*
|
|
24
|
+
* @param url - Full or relative URL to fetch
|
|
25
|
+
* @param params - Optional query parameters
|
|
26
|
+
* @returns Parsed response data
|
|
27
|
+
* @throws ApiError on HTTP error or network failure
|
|
28
|
+
*/
|
|
29
|
+
get<T>(url: string, params?: Record<string, any>): Promise<T>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* HTTP POST request
|
|
33
|
+
*
|
|
34
|
+
* @param url - Full or relative URL
|
|
35
|
+
* @param data - Request body (will be JSON serialized)
|
|
36
|
+
* @returns Parsed response data
|
|
37
|
+
* @throws ApiError on HTTP error or network failure
|
|
38
|
+
*/
|
|
39
|
+
post<T>(url: string, data: any): Promise<T>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* HTTP PATCH request
|
|
43
|
+
*
|
|
44
|
+
* @param url - Full or relative URL
|
|
45
|
+
* @param data - Request body (will be JSON serialized)
|
|
46
|
+
* @returns Parsed response data
|
|
47
|
+
* @throws ApiError on HTTP error or network failure
|
|
48
|
+
*/
|
|
49
|
+
patch<T>(url: string, data: any): Promise<T>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* HTTP DELETE request
|
|
53
|
+
*
|
|
54
|
+
* @param url - Full or relative URL
|
|
55
|
+
* @returns Parsed response data (often empty)
|
|
56
|
+
* @throws ApiError on HTTP error or network failure
|
|
57
|
+
*/
|
|
58
|
+
delete<T>(url: string): Promise<T>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Standard API error structure
|
|
63
|
+
*/
|
|
64
|
+
export interface ApiError extends Error {
|
|
65
|
+
/** HTTP status code (404, 500, etc) */
|
|
66
|
+
status?: number;
|
|
67
|
+
|
|
68
|
+
/** Error code from API */
|
|
69
|
+
code?: string;
|
|
70
|
+
|
|
71
|
+
/** Response body (if available) */
|
|
72
|
+
response?: any;
|
|
73
|
+
|
|
74
|
+
/** Original error (network, parse, etc) */
|
|
75
|
+
cause?: Error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create ApiError from response
|
|
80
|
+
*/
|
|
81
|
+
export function createApiError(
|
|
82
|
+
message: string,
|
|
83
|
+
status?: number,
|
|
84
|
+
response?: any,
|
|
85
|
+
cause?: Error
|
|
86
|
+
): ApiError {
|
|
87
|
+
const error = new Error(message) as ApiError;
|
|
88
|
+
error.name = 'ApiError';
|
|
89
|
+
error.status = status;
|
|
90
|
+
error.response = response;
|
|
91
|
+
error.cause = cause;
|
|
92
|
+
|
|
93
|
+
// Extract code from response if available
|
|
94
|
+
if (response?.code) {
|
|
95
|
+
error.code = response.code;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return error;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Type guard for ApiError
|
|
103
|
+
*/
|
|
104
|
+
export function isApiError(error: unknown): error is ApiError {
|
|
105
|
+
return error instanceof Error && error.name === 'ApiError';
|
|
106
|
+
}
|