@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.
- package/package.json +9 -31
- package/src/api/README.md +507 -0
- package/src/api/adapter.ts +106 -0
- package/src/api/client.test.ts +640 -0
- package/src/api/client.ts +385 -0
- package/src/api/default-adapter.ts +243 -0
- package/src/api/index.ts +24 -0
- package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
- package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
- package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
- package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
- package/src/components/FilterRuleEditor/README.md +291 -0
- package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
- package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
- package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
- package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
- package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
- package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
- package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
- package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
- package/src/components/FilterRuleEditor/constants.ts +64 -0
- package/src/components/FilterRuleEditor/index.ts +14 -0
- package/src/components/FunnelCard/DESIGN.md +447 -0
- package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
- package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
- package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
- package/src/components/FunnelCard/FunnelCard.tsx +204 -0
- package/src/components/FunnelCard/FunnelStats.tsx +68 -0
- package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
- package/src/components/FunnelCard/INSTALLATION.md +304 -0
- package/src/components/FunnelCard/MatchBar.tsx +49 -0
- package/src/components/FunnelCard/README.md +294 -0
- package/src/components/FunnelCard/StageIndicator.tsx +62 -0
- package/src/components/FunnelCard/StatusBadge.tsx +52 -0
- package/src/components/FunnelCard/index.ts +14 -0
- package/src/components/FunnelPreview/EntityCard.tsx +72 -0
- package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
- package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
- package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
- package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
- package/src/components/FunnelPreview/README.md +337 -0
- package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
- package/src/components/FunnelPreview/example.tsx +286 -0
- package/src/components/FunnelPreview/index.ts +14 -0
- package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
- package/src/components/FunnelRunHistory/README.md +325 -0
- package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
- package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
- package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
- package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
- package/src/components/FunnelRunHistory/index.ts +51 -0
- package/src/components/FunnelRunHistory/types.ts +40 -0
- package/src/components/FunnelRunHistory/utils.test.ts +126 -0
- package/src/components/FunnelRunHistory/utils.ts +100 -0
- package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
- package/src/components/FunnelStageBuilder/README.md +341 -0
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
- package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
- package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
- package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
- package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
- package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
- package/src/components/FunnelStageBuilder/index.ts +21 -0
- package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
- package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
- package/src/components/FunnelVisualFlow/README.md +323 -0
- package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
- package/src/components/FunnelVisualFlow/example.tsx +227 -0
- package/src/components/FunnelVisualFlow/index.ts +10 -0
- package/src/components/index.ts +102 -0
- package/src/core/README.md +307 -0
- package/src/core/engine.test.ts +1087 -0
- package/src/core/engine.ts +329 -0
- package/src/core/evaluator.example.ts +353 -0
- package/src/core/evaluator.test.ts +639 -0
- package/src/core/evaluator.ts +261 -0
- package/src/core/field-resolver.example.ts +175 -0
- package/src/core/field-resolver.test.ts +541 -0
- package/src/core/field-resolver.ts +247 -0
- package/src/core/index.ts +34 -0
- package/src/core/operators.test.ts +539 -0
- package/src/core/operators.ts +241 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useDebouncedValue.ts +28 -0
- package/src/index.ts +155 -0
- package/src/store/README.md +342 -0
- package/src/store/create-funnel-store.test.ts +686 -0
- package/src/store/create-funnel-store.ts +538 -0
- package/src/store/index.ts +9 -0
- package/src/store/types.ts +294 -0
- package/src/stories/CrossDomain.stories.tsx +149 -0
- package/src/stories/Welcome.stories.tsx +81 -0
- package/src/stories/demo-data/index.ts +3 -0
- package/src/stories/demo-data/investors.ts +216 -0
- package/src/stories/demo-data/leads.ts +223 -0
- package/src/stories/demo-data/recipes.ts +217 -0
- package/src/test/setup.ts +5 -0
- package/src/types/index.ts +843 -0
- package/dist/client-3ESO2NHy.d.ts +0 -310
- package/dist/client-CZu03ACp.d.cts +0 -310
- package/dist/components/index.cjs +0 -3241
- package/dist/components/index.cjs.map +0 -1
- package/dist/components/index.css.map +0 -1
- package/dist/components/index.d.cts +0 -726
- package/dist/components/index.d.ts +0 -726
- package/dist/components/index.js +0 -3194
- package/dist/components/index.js.map +0 -1
- package/dist/core/index.cjs +0 -500
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -359
- package/dist/core/index.d.ts +0 -359
- package/dist/core/index.js +0 -486
- package/dist/core/index.js.map +0 -1
- package/dist/hooks/index.cjs +0 -20
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.d.cts +0 -11
- package/dist/hooks/index.d.ts +0 -11
- package/dist/hooks/index.js +0 -18
- package/dist/hooks/index.js.map +0 -1
- package/dist/index-BGDEXbuz.d.cts +0 -434
- package/dist/index-BGDEXbuz.d.ts +0 -434
- package/dist/index.cjs +0 -4499
- package/dist/index.cjs.map +0 -1
- package/dist/index.css +0 -198
- package/dist/index.css.map +0 -1
- package/dist/index.d.cts +0 -99
- package/dist/index.d.ts +0 -99
- package/dist/index.js +0 -4421
- package/dist/index.js.map +0 -1
- package/dist/store/index.cjs +0 -389
- package/dist/store/index.cjs.map +0 -1
- package/dist/store/index.d.cts +0 -225
- package/dist/store/index.d.ts +0 -225
- package/dist/store/index.js +0 -386
- package/dist/store/index.js.map +0 -1
|
@@ -0,0 +1,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
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -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';
|