@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
@@ -0,0 +1,385 @@
1
+ /**
2
+ * FunnelApiClient - Generic API client for funnel operations
3
+ *
4
+ * Adapter-based client that works with any HTTP implementation.
5
+ * Consumers inject their own ApiAdapter for custom auth, headers, error handling.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { ApiAdapter } from './adapter';
11
+ import type {
12
+ Funnel,
13
+ FunnelStage,
14
+ FunnelRun,
15
+ FunnelResult,
16
+ CreateFunnelInput,
17
+ UpdateFunnelInput,
18
+ CreateStageInput,
19
+ UpdateStageInput,
20
+ } from '../types';
21
+
22
+ /**
23
+ * List filters for funnels
24
+ */
25
+ export interface FunnelListFilters {
26
+ /** Filter by status */
27
+ status?: 'draft' | 'active' | 'paused' | 'archived';
28
+
29
+ /** Filter by input type */
30
+ input_type?: string;
31
+
32
+ /** Filter by owner */
33
+ owner_id?: string;
34
+
35
+ /** Filter by team */
36
+ team_id?: string;
37
+
38
+ /** Search by name */
39
+ search?: string;
40
+
41
+ /** Pagination: page number (1-indexed) */
42
+ page?: number;
43
+
44
+ /** Pagination: page size */
45
+ page_size?: number;
46
+
47
+ /** Ordering: field name (prefix with - for descending) */
48
+ ordering?: string;
49
+ }
50
+
51
+ /**
52
+ * List response with pagination
53
+ */
54
+ export interface PaginatedResponse<T> {
55
+ count: number;
56
+ next: string | null;
57
+ previous: string | null;
58
+ results: T[];
59
+ }
60
+
61
+ /**
62
+ * Preview result (client-side evaluation)
63
+ */
64
+ export interface PreviewResult<TEntity = any> {
65
+ /** Sample entities that would match */
66
+ matched_entities: TEntity[];
67
+
68
+ /** Sample entities that would be excluded */
69
+ excluded_entities: TEntity[];
70
+
71
+ /** Stage-by-stage breakdown */
72
+ stage_breakdown: {
73
+ stage_id: string;
74
+ stage_name: string;
75
+ input_count: number;
76
+ matched_count: number;
77
+ excluded_count: number;
78
+ }[];
79
+
80
+ /** Total counts */
81
+ total_input: number;
82
+ total_matched: number;
83
+ total_excluded: number;
84
+ }
85
+
86
+ /**
87
+ * Generic API client for funnel operations
88
+ *
89
+ * Usage:
90
+ * ```ts
91
+ * const adapter = new FetchAdapter({ headers: { 'Authorization': 'Bearer token' } });
92
+ * const client = new FunnelApiClient(adapter, 'https://api.example.com');
93
+ *
94
+ * const funnels = await client.listFunnels({ status: 'active' });
95
+ * const funnel = await client.getFunnel('funnel-123');
96
+ * const run = await client.runFunnel('funnel-123');
97
+ * ```
98
+ */
99
+ export class FunnelApiClient {
100
+ constructor(
101
+ private adapter: ApiAdapter,
102
+ private baseUrl: string
103
+ ) {
104
+ // Remove trailing slash
105
+ this.baseUrl = baseUrl.replace(/\/$/, '');
106
+ }
107
+
108
+ /**
109
+ * Build full URL for endpoint
110
+ */
111
+ private url(path: string): string {
112
+ return `${this.baseUrl}${path}`;
113
+ }
114
+
115
+ // ============================================================================
116
+ // Funnel CRUD
117
+ // ============================================================================
118
+
119
+ /**
120
+ * List funnels with optional filters
121
+ *
122
+ * @param filters - Optional filters (status, owner, pagination, etc)
123
+ * @returns Paginated list of funnels
124
+ */
125
+ async listFunnels<TEntity = any>(
126
+ filters?: FunnelListFilters
127
+ ): Promise<PaginatedResponse<Funnel<TEntity>>> {
128
+ return this.adapter.get<PaginatedResponse<Funnel<TEntity>>>(
129
+ this.url('/api/v1/funnels/'),
130
+ filters
131
+ );
132
+ }
133
+
134
+ /**
135
+ * Get single funnel by ID
136
+ *
137
+ * @param id - Funnel ID
138
+ * @returns Funnel detail
139
+ * @throws ApiError with status 404 if not found
140
+ */
141
+ async getFunnel<TEntity = any>(id: string): Promise<Funnel<TEntity>> {
142
+ return this.adapter.get<Funnel<TEntity>>(this.url(`/api/v1/funnels/${id}/`));
143
+ }
144
+
145
+ /**
146
+ * Create new funnel
147
+ *
148
+ * @param data - Funnel creation data
149
+ * @returns Created funnel
150
+ * @throws ApiError with status 400 if validation fails
151
+ */
152
+ async createFunnel<TEntity = any>(
153
+ data: CreateFunnelInput<TEntity>
154
+ ): Promise<Funnel<TEntity>> {
155
+ return this.adapter.post<Funnel<TEntity>>(this.url('/api/v1/funnels/'), data);
156
+ }
157
+
158
+ /**
159
+ * Update existing funnel
160
+ *
161
+ * @param id - Funnel ID
162
+ * @param data - Funnel update data
163
+ * @returns Updated funnel
164
+ * @throws ApiError with status 404 if not found, 400 if validation fails
165
+ */
166
+ async updateFunnel<TEntity = any>(
167
+ id: string,
168
+ data: Partial<UpdateFunnelInput<TEntity>>
169
+ ): Promise<Funnel<TEntity>> {
170
+ return this.adapter.patch<Funnel<TEntity>>(
171
+ this.url(`/api/v1/funnels/${id}/`),
172
+ data
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Delete funnel
178
+ *
179
+ * @param id - Funnel ID
180
+ * @throws ApiError with status 404 if not found
181
+ */
182
+ async deleteFunnel(id: string): Promise<void> {
183
+ return this.adapter.delete<void>(this.url(`/api/v1/funnels/${id}/`));
184
+ }
185
+
186
+ // ============================================================================
187
+ // Stage CRUD
188
+ // ============================================================================
189
+
190
+ /**
191
+ * Create stage in funnel
192
+ *
193
+ * @param funnelId - Funnel ID
194
+ * @param data - Stage creation data
195
+ * @returns Created stage
196
+ * @throws ApiError with status 404 if funnel not found, 400 if validation fails
197
+ */
198
+ async createStage<TEntity = any>(
199
+ funnelId: string,
200
+ data: CreateStageInput<TEntity>
201
+ ): Promise<FunnelStage<TEntity>> {
202
+ return this.adapter.post<FunnelStage<TEntity>>(
203
+ this.url(`/api/v1/funnels/${funnelId}/stages/`),
204
+ data
205
+ );
206
+ }
207
+
208
+ /**
209
+ * Update stage
210
+ *
211
+ * @param funnelId - Funnel ID
212
+ * @param stageId - Stage ID
213
+ * @param data - Stage update data
214
+ * @returns Updated stage
215
+ * @throws ApiError with status 404 if not found, 400 if validation fails
216
+ */
217
+ async updateStage<TEntity = any>(
218
+ funnelId: string,
219
+ stageId: string,
220
+ data: Partial<UpdateStageInput<TEntity>>
221
+ ): Promise<FunnelStage<TEntity>> {
222
+ return this.adapter.patch<FunnelStage<TEntity>>(
223
+ this.url(`/api/v1/funnels/${funnelId}/stages/${stageId}/`),
224
+ data
225
+ );
226
+ }
227
+
228
+ /**
229
+ * Delete stage
230
+ *
231
+ * @param funnelId - Funnel ID
232
+ * @param stageId - Stage ID
233
+ * @throws ApiError with status 404 if not found
234
+ */
235
+ async deleteStage(funnelId: string, stageId: string): Promise<void> {
236
+ return this.adapter.delete<void>(
237
+ this.url(`/api/v1/funnels/${funnelId}/stages/${stageId}/`)
238
+ );
239
+ }
240
+
241
+ // ============================================================================
242
+ // Run Operations
243
+ // ============================================================================
244
+
245
+ /**
246
+ * Trigger funnel run
247
+ *
248
+ * @param funnelId - Funnel ID
249
+ * @param options - Optional run configuration (trigger_type, metadata, etc)
250
+ * @returns Created funnel run (status: pending or running)
251
+ * @throws ApiError with status 404 if funnel not found, 400 if validation fails
252
+ */
253
+ async runFunnel(
254
+ funnelId: string,
255
+ options?: {
256
+ trigger_type?: 'manual' | 'scheduled' | 'webhook' | 'api';
257
+ metadata?: Record<string, any>;
258
+ }
259
+ ): Promise<FunnelRun> {
260
+ return this.adapter.post<FunnelRun>(
261
+ this.url(`/api/v1/funnels/${funnelId}/run/`),
262
+ options || {}
263
+ );
264
+ }
265
+
266
+ /**
267
+ * Get funnel run history
268
+ *
269
+ * @param funnelId - Funnel ID
270
+ * @param filters - Optional filters (status, pagination, etc)
271
+ * @returns List of funnel runs
272
+ * @throws ApiError with status 404 if funnel not found
273
+ */
274
+ async getFunnelRuns(
275
+ funnelId: string,
276
+ filters?: {
277
+ status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
278
+ page?: number;
279
+ page_size?: number;
280
+ ordering?: string;
281
+ }
282
+ ): Promise<PaginatedResponse<FunnelRun>> {
283
+ return this.adapter.get<PaginatedResponse<FunnelRun>>(
284
+ this.url(`/api/v1/funnels/${funnelId}/runs/`),
285
+ filters
286
+ );
287
+ }
288
+
289
+ /**
290
+ * Get single run detail
291
+ *
292
+ * @param runId - Run ID
293
+ * @returns Funnel run detail
294
+ * @throws ApiError with status 404 if not found
295
+ */
296
+ async getFunnelRun(runId: string): Promise<FunnelRun> {
297
+ return this.adapter.get<FunnelRun>(this.url(`/api/v1/funnel-runs/${runId}/`));
298
+ }
299
+
300
+ /**
301
+ * Get run results (entities that were processed)
302
+ *
303
+ * @param runId - Run ID
304
+ * @param filters - Optional filters (matched, pagination, etc)
305
+ * @returns Paginated list of results
306
+ * @throws ApiError with status 404 if run not found
307
+ */
308
+ async getFunnelResults<TEntity = any>(
309
+ runId: string,
310
+ filters?: {
311
+ matched?: boolean;
312
+ page?: number;
313
+ page_size?: number;
314
+ }
315
+ ): Promise<PaginatedResponse<FunnelResult<TEntity>>> {
316
+ return this.adapter.get<PaginatedResponse<FunnelResult<TEntity>>>(
317
+ this.url(`/api/v1/funnel-runs/${runId}/results/`),
318
+ filters
319
+ );
320
+ }
321
+
322
+ /**
323
+ * Cancel running funnel
324
+ *
325
+ * @param runId - Run ID
326
+ * @returns Updated run with status 'cancelled'
327
+ * @throws ApiError with status 404 if not found, 400 if already completed
328
+ */
329
+ async cancelFunnelRun(runId: string): Promise<FunnelRun> {
330
+ return this.adapter.post<FunnelRun>(
331
+ this.url(`/api/v1/funnel-runs/${runId}/cancel/`),
332
+ {}
333
+ );
334
+ }
335
+
336
+ // ============================================================================
337
+ // Client-Side Preview (Local Evaluation)
338
+ // ============================================================================
339
+
340
+ /**
341
+ * Preview funnel with sample entities (client-side evaluation)
342
+ *
343
+ * Useful for testing funnel logic before running on full dataset.
344
+ * Does NOT hit the server - evaluates locally.
345
+ *
346
+ * Note: This requires the evaluation engine to be available client-side.
347
+ * If not available, this will throw an error.
348
+ *
349
+ * @param funnel - Funnel definition
350
+ * @param sampleEntities - Sample entities to test
351
+ * @returns Preview results showing which entities would match/exclude
352
+ */
353
+ async previewFunnel<TEntity = any>(
354
+ funnel: Funnel<TEntity>,
355
+ sampleEntities: TEntity[]
356
+ ): Promise<PreviewResult<TEntity>> {
357
+ // This is client-side only - would require evaluation engine
358
+ // Throwing for now until we implement the evaluator
359
+ throw new Error(
360
+ 'Client-side preview requires evaluation engine. ' +
361
+ 'Use server-side preview endpoint instead: POST /api/v1/funnels/{id}/preview/'
362
+ );
363
+ }
364
+
365
+ /**
366
+ * Server-side preview (recommended)
367
+ *
368
+ * Send sample entities to server for evaluation.
369
+ * Useful for testing funnel logic before running on full dataset.
370
+ *
371
+ * @param funnelId - Funnel ID
372
+ * @param sampleEntities - Sample entities to test
373
+ * @returns Preview results
374
+ * @throws ApiError with status 404 if funnel not found
375
+ */
376
+ async previewFunnelServer<TEntity = any>(
377
+ funnelId: string,
378
+ sampleEntities: TEntity[]
379
+ ): Promise<PreviewResult<TEntity>> {
380
+ return this.adapter.post<PreviewResult<TEntity>>(
381
+ this.url(`/api/v1/funnels/${funnelId}/preview/`),
382
+ { entities: sampleEntities }
383
+ );
384
+ }
385
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * FetchAdapter - Default fetch-based implementation
3
+ *
4
+ * Simple adapter using standard fetch API.
5
+ * Suitable for apps that don't need custom authentication.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import { ApiAdapter, createApiError } from './adapter';
11
+
12
+ /**
13
+ * Configuration options for FetchAdapter
14
+ */
15
+ export interface FetchAdapterConfig {
16
+ /** Optional headers to include in all requests */
17
+ headers?: Record<string, string>;
18
+
19
+ /** Request timeout in milliseconds (default: 30000) */
20
+ timeout?: number;
21
+
22
+ /**
23
+ * Handle response parsing
24
+ * Default: response.json()
25
+ */
26
+ parseResponse?: (response: Response) => Promise<any>;
27
+
28
+ /**
29
+ * Transform error before throwing
30
+ * Useful for logging, monitoring, etc.
31
+ */
32
+ onError?: (error: Error) => void;
33
+ }
34
+
35
+ /**
36
+ * Default fetch-based adapter
37
+ *
38
+ * Features:
39
+ * - JSON request/response handling
40
+ * - Configurable headers
41
+ * - Request timeout
42
+ * - Error transformation
43
+ *
44
+ * Example:
45
+ * ```ts
46
+ * const adapter = new FetchAdapter({
47
+ * headers: { 'X-API-Key': 'secret' },
48
+ * timeout: 10000
49
+ * });
50
+ * ```
51
+ */
52
+ export class FetchAdapter implements ApiAdapter {
53
+ private config: Required<FetchAdapterConfig>;
54
+
55
+ constructor(config: FetchAdapterConfig = {}) {
56
+ this.config = {
57
+ headers: config.headers || {},
58
+ timeout: config.timeout || 30000,
59
+ parseResponse: config.parseResponse || ((res) => res.json()),
60
+ onError: config.onError || (() => {}),
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Build fetch options
66
+ */
67
+ private buildOptions(
68
+ method: string,
69
+ data?: any,
70
+ params?: Record<string, any>
71
+ ): RequestInit {
72
+ const options: RequestInit = {
73
+ method,
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ ...this.config.headers,
77
+ },
78
+ };
79
+
80
+ if (data !== undefined) {
81
+ options.body = JSON.stringify(data);
82
+ }
83
+
84
+ return options;
85
+ }
86
+
87
+ /**
88
+ * Build URL with query params
89
+ */
90
+ private buildUrl(url: string, params?: Record<string, any>): string {
91
+ if (!params || Object.keys(params).length === 0) {
92
+ return url;
93
+ }
94
+
95
+ const searchParams = new URLSearchParams();
96
+ Object.entries(params).forEach(([key, value]) => {
97
+ if (value !== undefined && value !== null) {
98
+ // Handle arrays
99
+ if (Array.isArray(value)) {
100
+ value.forEach((v) => searchParams.append(key, String(v)));
101
+ } else {
102
+ searchParams.append(key, String(value));
103
+ }
104
+ }
105
+ });
106
+
107
+ const queryString = searchParams.toString();
108
+ return queryString ? `${url}?${queryString}` : url;
109
+ }
110
+
111
+ /**
112
+ * Fetch with timeout
113
+ */
114
+ private async fetchWithTimeout(
115
+ url: string,
116
+ options: RequestInit
117
+ ): Promise<Response> {
118
+ const controller = new AbortController();
119
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
120
+
121
+ try {
122
+ const response = await fetch(url, {
123
+ ...options,
124
+ signal: controller.signal,
125
+ });
126
+ clearTimeout(timeoutId);
127
+ return response;
128
+ } catch (error) {
129
+ clearTimeout(timeoutId);
130
+ if (error instanceof Error && error.name === 'AbortError') {
131
+ throw createApiError(
132
+ `Request timeout after ${this.config.timeout}ms`,
133
+ undefined,
134
+ undefined,
135
+ error
136
+ );
137
+ }
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Handle response
144
+ */
145
+ private async handleResponse<T>(response: Response): Promise<T> {
146
+ // Success
147
+ if (response.ok) {
148
+ // Handle 204 No Content
149
+ if (response.status === 204) {
150
+ return undefined as T;
151
+ }
152
+
153
+ try {
154
+ return await this.config.parseResponse(response);
155
+ } catch (error) {
156
+ throw createApiError(
157
+ 'Failed to parse response',
158
+ response.status,
159
+ undefined,
160
+ error as Error
161
+ );
162
+ }
163
+ }
164
+
165
+ // Error
166
+ let errorBody: any;
167
+ try {
168
+ errorBody = await response.json();
169
+ } catch {
170
+ errorBody = { detail: response.statusText };
171
+ }
172
+
173
+ const error = createApiError(
174
+ errorBody.detail || errorBody.message || `HTTP ${response.status}`,
175
+ response.status,
176
+ errorBody
177
+ );
178
+
179
+ this.config.onError(error);
180
+ throw error;
181
+ }
182
+
183
+ /**
184
+ * Execute request
185
+ */
186
+ private async request<T>(
187
+ method: string,
188
+ url: string,
189
+ data?: any,
190
+ params?: Record<string, any>
191
+ ): Promise<T> {
192
+ const fullUrl = this.buildUrl(url, params);
193
+ const options = this.buildOptions(method, data, params);
194
+
195
+ try {
196
+ const response = await this.fetchWithTimeout(fullUrl, options);
197
+ return await this.handleResponse<T>(response);
198
+ } catch (error) {
199
+ // Already an ApiError
200
+ if ((error as any).name === 'ApiError') {
201
+ throw error;
202
+ }
203
+
204
+ // Network or other error
205
+ const apiError = createApiError(
206
+ 'Network request failed',
207
+ undefined,
208
+ undefined,
209
+ error as Error
210
+ );
211
+ this.config.onError(apiError);
212
+ throw apiError;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * HTTP GET
218
+ */
219
+ async get<T>(url: string, params?: Record<string, any>): Promise<T> {
220
+ return this.request<T>('GET', url, undefined, params);
221
+ }
222
+
223
+ /**
224
+ * HTTP POST
225
+ */
226
+ async post<T>(url: string, data: any): Promise<T> {
227
+ return this.request<T>('POST', url, data);
228
+ }
229
+
230
+ /**
231
+ * HTTP PATCH
232
+ */
233
+ async patch<T>(url: string, data: any): Promise<T> {
234
+ return this.request<T>('PATCH', url, data);
235
+ }
236
+
237
+ /**
238
+ * HTTP DELETE
239
+ */
240
+ async delete<T>(url: string): Promise<T> {
241
+ return this.request<T>('DELETE', url);
242
+ }
243
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @startsimpli/funnels - API Client Module
3
+ *
4
+ * Generic API client for funnel operations.
5
+ * Uses adapter pattern for flexible HTTP integration.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ // Adapter interface
11
+ export type { ApiAdapter, ApiError } from './adapter';
12
+ export { createApiError, isApiError } from './adapter';
13
+
14
+ // Default adapter
15
+ export { FetchAdapter } from './default-adapter';
16
+ export type { FetchAdapterConfig } from './default-adapter';
17
+
18
+ // Client
19
+ export { FunnelApiClient } from './client';
20
+ export type {
21
+ FunnelListFilters,
22
+ PaginatedResponse,
23
+ PreviewResult,
24
+ } from './client';