@startsimpli/funnels 0.1.4 → 0.1.6

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.
Files changed (151) hide show
  1. package/package.json +9 -31
  2. package/src/api/README.md +507 -0
  3. package/src/api/adapter.ts +106 -0
  4. package/src/api/client.test.ts +640 -0
  5. package/src/api/client.ts +385 -0
  6. package/src/api/default-adapter.ts +243 -0
  7. package/src/api/index.ts +24 -0
  8. package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
  9. package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
  10. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
  11. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
  12. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
  13. package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
  14. package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
  15. package/src/components/FilterRuleEditor/README.md +291 -0
  16. package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
  17. package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
  18. package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
  19. package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
  20. package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
  21. package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
  22. package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
  23. package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
  24. package/src/components/FilterRuleEditor/constants.ts +64 -0
  25. package/src/components/FilterRuleEditor/index.ts +14 -0
  26. package/src/components/FunnelCard/DESIGN.md +447 -0
  27. package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
  28. package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
  29. package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
  30. package/src/components/FunnelCard/FunnelCard.tsx +204 -0
  31. package/src/components/FunnelCard/FunnelStats.tsx +68 -0
  32. package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
  33. package/src/components/FunnelCard/INSTALLATION.md +304 -0
  34. package/src/components/FunnelCard/MatchBar.tsx +49 -0
  35. package/src/components/FunnelCard/README.md +294 -0
  36. package/src/components/FunnelCard/StageIndicator.tsx +62 -0
  37. package/src/components/FunnelCard/StatusBadge.tsx +52 -0
  38. package/src/components/FunnelCard/index.ts +14 -0
  39. package/src/components/FunnelPreview/EntityCard.tsx +72 -0
  40. package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
  41. package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
  42. package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
  43. package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
  44. package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
  45. package/src/components/FunnelPreview/README.md +337 -0
  46. package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
  47. package/src/components/FunnelPreview/example.tsx +286 -0
  48. package/src/components/FunnelPreview/index.ts +14 -0
  49. package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
  50. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
  51. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
  52. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
  53. package/src/components/FunnelRunHistory/README.md +325 -0
  54. package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
  55. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
  56. package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
  57. package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
  58. package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
  59. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
  60. package/src/components/FunnelRunHistory/index.ts +51 -0
  61. package/src/components/FunnelRunHistory/types.ts +40 -0
  62. package/src/components/FunnelRunHistory/utils.test.ts +126 -0
  63. package/src/components/FunnelRunHistory/utils.ts +100 -0
  64. package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
  65. package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
  66. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
  67. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
  68. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
  69. package/src/components/FunnelStageBuilder/README.md +341 -0
  70. package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
  71. package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
  72. package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
  73. package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
  74. package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
  75. package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
  76. package/src/components/FunnelStageBuilder/index.ts +21 -0
  77. package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
  78. package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
  79. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
  80. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
  81. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
  82. package/src/components/FunnelVisualFlow/README.md +323 -0
  83. package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
  84. package/src/components/FunnelVisualFlow/example.tsx +227 -0
  85. package/src/components/FunnelVisualFlow/index.ts +10 -0
  86. package/src/components/index.ts +102 -0
  87. package/src/core/README.md +307 -0
  88. package/src/core/engine.test.ts +1087 -0
  89. package/src/core/engine.ts +329 -0
  90. package/src/core/evaluator.example.ts +353 -0
  91. package/src/core/evaluator.test.ts +639 -0
  92. package/src/core/evaluator.ts +261 -0
  93. package/src/core/field-resolver.example.ts +175 -0
  94. package/src/core/field-resolver.test.ts +541 -0
  95. package/src/core/field-resolver.ts +247 -0
  96. package/src/core/index.ts +34 -0
  97. package/src/core/operators.test.ts +539 -0
  98. package/src/core/operators.ts +241 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/useDebouncedValue.ts +28 -0
  101. package/src/index.ts +155 -0
  102. package/src/store/README.md +342 -0
  103. package/src/store/create-funnel-store.test.ts +686 -0
  104. package/src/store/create-funnel-store.ts +538 -0
  105. package/src/store/index.ts +9 -0
  106. package/src/store/types.ts +294 -0
  107. package/src/stories/CrossDomain.stories.tsx +149 -0
  108. package/src/stories/Welcome.stories.tsx +81 -0
  109. package/src/stories/demo-data/index.ts +3 -0
  110. package/src/stories/demo-data/investors.ts +216 -0
  111. package/src/stories/demo-data/leads.ts +223 -0
  112. package/src/stories/demo-data/recipes.ts +217 -0
  113. package/src/test/setup.ts +5 -0
  114. package/src/types/index.ts +843 -0
  115. package/dist/client-3ESO2NHy.d.ts +0 -310
  116. package/dist/client-CZu03ACp.d.cts +0 -310
  117. package/dist/components/index.cjs +0 -3241
  118. package/dist/components/index.cjs.map +0 -1
  119. package/dist/components/index.css.map +0 -1
  120. package/dist/components/index.d.cts +0 -726
  121. package/dist/components/index.d.ts +0 -726
  122. package/dist/components/index.js +0 -3194
  123. package/dist/components/index.js.map +0 -1
  124. package/dist/core/index.cjs +0 -500
  125. package/dist/core/index.cjs.map +0 -1
  126. package/dist/core/index.d.cts +0 -359
  127. package/dist/core/index.d.ts +0 -359
  128. package/dist/core/index.js +0 -486
  129. package/dist/core/index.js.map +0 -1
  130. package/dist/hooks/index.cjs +0 -20
  131. package/dist/hooks/index.cjs.map +0 -1
  132. package/dist/hooks/index.d.cts +0 -11
  133. package/dist/hooks/index.d.ts +0 -11
  134. package/dist/hooks/index.js +0 -18
  135. package/dist/hooks/index.js.map +0 -1
  136. package/dist/index-BGDEXbuz.d.cts +0 -434
  137. package/dist/index-BGDEXbuz.d.ts +0 -434
  138. package/dist/index.cjs +0 -4499
  139. package/dist/index.cjs.map +0 -1
  140. package/dist/index.css +0 -198
  141. package/dist/index.css.map +0 -1
  142. package/dist/index.d.cts +0 -99
  143. package/dist/index.d.ts +0 -99
  144. package/dist/index.js +0 -4421
  145. package/dist/index.js.map +0 -1
  146. package/dist/store/index.cjs +0 -389
  147. package/dist/store/index.cjs.map +0 -1
  148. package/dist/store/index.d.cts +0 -225
  149. package/dist/store/index.d.ts +0 -225
  150. package/dist/store/index.js +0 -386
  151. 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.4",
3
+ "version": "0.1.6",
4
4
  "description": "Brutally generic filtering pipeline package for any Simpli product",
5
5
  "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
9
8
  "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
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
- "dist",
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
+ }