@devpad/api 1.0.1
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/README.md +149 -0
- package/dist/api-client.d.ts +337 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +407 -0
- package/dist/error-handlers.d.ts +55 -0
- package/dist/error-handlers.d.ts.map +1 -0
- package/dist/error-handlers.js +217 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/request.d.ts +41 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +178 -0
- package/dist/result.d.ts +42 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +44 -0
- package/dist/tools.d.ts +84 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +425 -0
- package/package.json +67 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { ApiClient as HttpClient } from "./request";
|
|
2
|
+
import { wrap } from "./result";
|
|
3
|
+
/**
|
|
4
|
+
* API client with Result-wrapped operations for clean error handling
|
|
5
|
+
* All methods return Result<T, name> types with context-aware property names
|
|
6
|
+
*/
|
|
7
|
+
export class ApiClient {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
/**
|
|
10
|
+
* Auth namespace with Result-wrapped operations
|
|
11
|
+
*/
|
|
12
|
+
this.auth = {
|
|
13
|
+
/**
|
|
14
|
+
* Get current session information
|
|
15
|
+
*/
|
|
16
|
+
session: () => wrap(() => this.clients.auth.get("/auth/session"), "session"),
|
|
17
|
+
/**
|
|
18
|
+
* Login (redirect to OAuth)
|
|
19
|
+
*/
|
|
20
|
+
login: () => wrap(() => this.clients.auth.get("/auth/login"), "result"),
|
|
21
|
+
/**
|
|
22
|
+
* Logout
|
|
23
|
+
*/
|
|
24
|
+
logout: () => wrap(() => this.clients.auth.get("/auth/logout"), "result"),
|
|
25
|
+
/**
|
|
26
|
+
* API key management
|
|
27
|
+
*/
|
|
28
|
+
keys: {
|
|
29
|
+
/**
|
|
30
|
+
* List all API keys
|
|
31
|
+
*/
|
|
32
|
+
list: () => wrap(() => this.clients.auth.get("/auth/keys"), "keys"),
|
|
33
|
+
/**
|
|
34
|
+
* Generate a new API key
|
|
35
|
+
*/
|
|
36
|
+
create: (name) => wrap(() => this.clients.auth.post("/auth/keys", { body: name ? { name } : {} }), "key"),
|
|
37
|
+
/**
|
|
38
|
+
* Revoke an API key
|
|
39
|
+
*/
|
|
40
|
+
revoke: (key_id) => wrap(() => this.clients.auth.delete(`/auth/keys/${key_id}`), "result"),
|
|
41
|
+
/**
|
|
42
|
+
* Remove an API key (alias for revoke)
|
|
43
|
+
*/
|
|
44
|
+
remove: (key_id) => wrap(() => this.clients.auth.delete(`/auth/keys/${key_id}`), "result"),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Projects namespace with Result-wrapped operations
|
|
49
|
+
*/
|
|
50
|
+
this.projects = {
|
|
51
|
+
/**
|
|
52
|
+
* List projects with optional filtering
|
|
53
|
+
*/
|
|
54
|
+
list: (filters) => wrap(() => this.clients.projects.get(filters?.private === false ? "/projects/public" : "/projects"), "projects"),
|
|
55
|
+
/**
|
|
56
|
+
* Get project map
|
|
57
|
+
*/
|
|
58
|
+
map: async (filters) => wrap(async () => {
|
|
59
|
+
const { projects, error } = await this.projects.list(filters);
|
|
60
|
+
if (error)
|
|
61
|
+
throw new Error(error.message);
|
|
62
|
+
return projects.reduce((acc, project) => {
|
|
63
|
+
acc[project.project_id] = project;
|
|
64
|
+
return acc;
|
|
65
|
+
}, {});
|
|
66
|
+
}, "project_map"),
|
|
67
|
+
/**
|
|
68
|
+
* Get project by ID
|
|
69
|
+
*/
|
|
70
|
+
find: (id) => wrap(() => this.clients.projects.get("/projects", { query: { id } }), "project"),
|
|
71
|
+
/**
|
|
72
|
+
* Get project by name
|
|
73
|
+
*/
|
|
74
|
+
getByName: (name) => wrap(() => this.clients.projects.get("/projects", { query: { name } }), "project"),
|
|
75
|
+
/**
|
|
76
|
+
* Get project by ID (throws if not found)
|
|
77
|
+
*/
|
|
78
|
+
getById: (id) => wrap(() => this.clients.projects.get("/projects", { query: { id } }), "project"),
|
|
79
|
+
/**
|
|
80
|
+
* Create a new project
|
|
81
|
+
*/
|
|
82
|
+
create: (data) => wrap(() => this.clients.projects.patch("/projects", { body: data }), "project"),
|
|
83
|
+
/**
|
|
84
|
+
* Update an existing project
|
|
85
|
+
*/
|
|
86
|
+
update: async (idOrData, changes) => wrap(async () => {
|
|
87
|
+
// Handle backward compatibility: update(data)
|
|
88
|
+
if (typeof idOrData === "object" && idOrData.id) {
|
|
89
|
+
return this.clients.projects.patch("/projects", { body: idOrData });
|
|
90
|
+
}
|
|
91
|
+
// Handle new clean interface: update(id, changes)
|
|
92
|
+
const id = idOrData;
|
|
93
|
+
if (!changes) {
|
|
94
|
+
throw new Error("Changes parameter required for update");
|
|
95
|
+
}
|
|
96
|
+
// Fetch the existing project to get current values
|
|
97
|
+
const { project, error } = await this.projects.find(id);
|
|
98
|
+
if (error)
|
|
99
|
+
throw new Error(error.message);
|
|
100
|
+
if (!project)
|
|
101
|
+
throw new Error(`Project with id ${id} not found`);
|
|
102
|
+
// Merge changes with existing project data
|
|
103
|
+
const updateData = {
|
|
104
|
+
id: project.id,
|
|
105
|
+
project_id: project.project_id,
|
|
106
|
+
owner_id: project.owner_id,
|
|
107
|
+
name: project.name,
|
|
108
|
+
description: project.description,
|
|
109
|
+
specification: project.specification,
|
|
110
|
+
repo_url: project.repo_url,
|
|
111
|
+
repo_id: project.repo_id,
|
|
112
|
+
icon_url: project.icon_url,
|
|
113
|
+
status: project.status,
|
|
114
|
+
deleted: project.deleted,
|
|
115
|
+
link_url: project.link_url,
|
|
116
|
+
link_text: project.link_text,
|
|
117
|
+
visibility: project.visibility,
|
|
118
|
+
current_version: project.current_version,
|
|
119
|
+
...changes,
|
|
120
|
+
};
|
|
121
|
+
return this.clients.projects.patch("/projects", { body: updateData });
|
|
122
|
+
}, "project"),
|
|
123
|
+
/**
|
|
124
|
+
* Project configuration operations
|
|
125
|
+
*/
|
|
126
|
+
config: {
|
|
127
|
+
/**
|
|
128
|
+
* Get project configuration
|
|
129
|
+
*/
|
|
130
|
+
load: (project_id) => wrap(() => this.clients.projects.get("/projects/config", { query: { project_id } }), "config"),
|
|
131
|
+
/**
|
|
132
|
+
* Save project configuration
|
|
133
|
+
*/
|
|
134
|
+
save: (request) => wrap(() => this.clients.projects.patch("/projects/save_config", { body: request }), "result"),
|
|
135
|
+
},
|
|
136
|
+
/**
|
|
137
|
+
* Scanning operations
|
|
138
|
+
*/
|
|
139
|
+
scan: {
|
|
140
|
+
/**
|
|
141
|
+
* Update scan status
|
|
142
|
+
*/
|
|
143
|
+
updateStatus: (project_id, data) => wrap(() => this.clients.projects.post(`/projects/${project_id}/scan/status`, { body: data }), "result"),
|
|
144
|
+
},
|
|
145
|
+
/**
|
|
146
|
+
* Get project history
|
|
147
|
+
*/
|
|
148
|
+
history: (project_id) => wrap(() => this.clients.projects.get(`/projects/${project_id}/history`), "history"),
|
|
149
|
+
/**
|
|
150
|
+
* Legacy methods (keeping for compatibility)
|
|
151
|
+
*/
|
|
152
|
+
upsert: (data) => wrap(() => this.clients.projects.patch("/projects", { body: data }), "project"),
|
|
153
|
+
/**
|
|
154
|
+
* Fetch project specification from GitHub
|
|
155
|
+
*/
|
|
156
|
+
specification: (project_id) => wrap(() => this.clients.projects.get("/projects/fetch_spec", { query: { project_id } }), "specification"),
|
|
157
|
+
/**
|
|
158
|
+
* Delete project (soft delete)
|
|
159
|
+
*/
|
|
160
|
+
deleteProject: (project) => wrap(() => this.clients.projects.patch("/projects", { body: { ...project, deleted: true } }), "result"),
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Milestones namespace with Result-wrapped operations
|
|
164
|
+
*/
|
|
165
|
+
this.milestones = {
|
|
166
|
+
/**
|
|
167
|
+
* List milestones for authenticated user
|
|
168
|
+
*/
|
|
169
|
+
list: () => wrap(() => this.clients.milestones.get("/milestones"), "milestones"),
|
|
170
|
+
/**
|
|
171
|
+
* Get milestones by project ID
|
|
172
|
+
*/
|
|
173
|
+
getByProject: (project_id) => wrap(() => this.clients.milestones.get(`/projects/${project_id}/milestones`), "milestones"),
|
|
174
|
+
/**
|
|
175
|
+
* Get milestone by ID
|
|
176
|
+
*/
|
|
177
|
+
find: (id) => wrap(async () => {
|
|
178
|
+
try {
|
|
179
|
+
return await this.clients.milestones.get(`/milestones/${id}`);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}, "milestone"),
|
|
185
|
+
/**
|
|
186
|
+
* Create new milestone
|
|
187
|
+
*/
|
|
188
|
+
create: (data) => wrap(() => this.clients.milestones.post("/milestones", { body: data }), "milestone"),
|
|
189
|
+
/**
|
|
190
|
+
* Update milestone
|
|
191
|
+
*/
|
|
192
|
+
update: async (id, data) => wrap(async () => {
|
|
193
|
+
// Fetch the existing milestone to get required fields
|
|
194
|
+
const { milestone, error } = await this.milestones.find(id);
|
|
195
|
+
if (error)
|
|
196
|
+
throw new Error(error.message);
|
|
197
|
+
if (!milestone)
|
|
198
|
+
throw new Error(`Milestone with id ${id} not found`);
|
|
199
|
+
// Merge changes with existing milestone data
|
|
200
|
+
const updateData = {
|
|
201
|
+
id: milestone.id,
|
|
202
|
+
project_id: milestone.project_id,
|
|
203
|
+
name: data.name ?? milestone.name,
|
|
204
|
+
description: data.description ?? milestone.description,
|
|
205
|
+
target_time: data.target_time ?? milestone.target_time,
|
|
206
|
+
target_version: data.target_version ?? milestone.target_version,
|
|
207
|
+
};
|
|
208
|
+
return this.clients.milestones.patch(`/milestones/${id}`, { body: updateData });
|
|
209
|
+
}, "milestone"),
|
|
210
|
+
/**
|
|
211
|
+
* Delete milestone (soft delete)
|
|
212
|
+
*/
|
|
213
|
+
delete: (id) => wrap(() => this.clients.milestones.delete(`/milestones/${id}`), "result"),
|
|
214
|
+
/**
|
|
215
|
+
* Get goals for a milestone
|
|
216
|
+
*/
|
|
217
|
+
goals: (id) => wrap(() => this.clients.milestones.get(`/milestones/${id}/goals`), "goals"),
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Goals namespace with Result-wrapped operations
|
|
221
|
+
*/
|
|
222
|
+
this.goals = {
|
|
223
|
+
/**
|
|
224
|
+
* List goals for authenticated user
|
|
225
|
+
*/
|
|
226
|
+
list: () => wrap(() => this.clients.goals.get("/goals"), "goals"),
|
|
227
|
+
/**
|
|
228
|
+
* Get goal by ID
|
|
229
|
+
*/
|
|
230
|
+
find: (id) => wrap(() => this.clients.goals.get(`/goals/${id}`), "goal"),
|
|
231
|
+
/**
|
|
232
|
+
* Create new goal
|
|
233
|
+
*/
|
|
234
|
+
create: (data) => wrap(() => this.clients.goals.post("/goals", { body: data }), "goal"),
|
|
235
|
+
/**
|
|
236
|
+
* Update goal
|
|
237
|
+
*/
|
|
238
|
+
update: async (id, data) => wrap(async () => {
|
|
239
|
+
// Fetch the existing goal to get required fields
|
|
240
|
+
const { goal, error } = await this.goals.find(id);
|
|
241
|
+
if (error)
|
|
242
|
+
throw new Error(error.message);
|
|
243
|
+
if (!goal)
|
|
244
|
+
throw new Error(`Goal with id ${id} not found`);
|
|
245
|
+
// Merge changes with existing goal data
|
|
246
|
+
const updateData = {
|
|
247
|
+
id: goal.id,
|
|
248
|
+
milestone_id: goal.milestone_id,
|
|
249
|
+
name: data.name ?? goal.name,
|
|
250
|
+
description: data.description ?? goal.description,
|
|
251
|
+
target_time: data.target_time ?? goal.target_time,
|
|
252
|
+
};
|
|
253
|
+
return this.clients.goals.patch(`/goals/${id}`, { body: updateData });
|
|
254
|
+
}, "goal"),
|
|
255
|
+
/**
|
|
256
|
+
* Delete goal (soft delete)
|
|
257
|
+
*/
|
|
258
|
+
delete: (id) => wrap(() => this.clients.goals.delete(`/goals/${id}`), "result"),
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Tasks namespace with Result-wrapped operations
|
|
262
|
+
*/
|
|
263
|
+
this.tasks = {
|
|
264
|
+
/**
|
|
265
|
+
* List tasks with optional filtering
|
|
266
|
+
*/
|
|
267
|
+
list: (filters) => wrap(() => {
|
|
268
|
+
const query = {};
|
|
269
|
+
if (filters?.project_id)
|
|
270
|
+
query.project = filters.project_id;
|
|
271
|
+
if (filters?.tag_id)
|
|
272
|
+
query.tag = filters.tag_id;
|
|
273
|
+
return this.clients.tasks.get("/tasks", Object.keys(query).length > 0 ? { query } : {});
|
|
274
|
+
}, "tasks"),
|
|
275
|
+
/**
|
|
276
|
+
* Get task by ID
|
|
277
|
+
*/
|
|
278
|
+
find: (id) => wrap(() => this.clients.tasks.get("/tasks", { query: { id } }), "task"),
|
|
279
|
+
/**
|
|
280
|
+
* Get tasks by project ID
|
|
281
|
+
*/
|
|
282
|
+
getByProject: (project_id) => wrap(() => this.clients.tasks.get(`/tasks`, { query: { project: project_id } }), "tasks"),
|
|
283
|
+
/**
|
|
284
|
+
* Create a new task
|
|
285
|
+
*/
|
|
286
|
+
create: (data) => wrap(() => this.clients.tasks.patch("/tasks", { body: data }), "task"),
|
|
287
|
+
/**
|
|
288
|
+
* Update an existing task
|
|
289
|
+
*/
|
|
290
|
+
update: async (id, changes) => wrap(async () => {
|
|
291
|
+
// Fetch existing task to merge changes
|
|
292
|
+
const { task, error } = await this.tasks.find(id);
|
|
293
|
+
if (error)
|
|
294
|
+
throw new Error(error.message);
|
|
295
|
+
if (!task)
|
|
296
|
+
throw new Error(`Task with id ${id} not found`);
|
|
297
|
+
const updateData = {
|
|
298
|
+
id,
|
|
299
|
+
title: task.task.title,
|
|
300
|
+
summary: task.task.summary,
|
|
301
|
+
description: task.task.description,
|
|
302
|
+
progress: task.task.progress,
|
|
303
|
+
visibility: task.task.visibility,
|
|
304
|
+
start_time: task.task.start_time,
|
|
305
|
+
end_time: task.task.end_time,
|
|
306
|
+
priority: task.task.priority,
|
|
307
|
+
owner_id: task.task.owner_id,
|
|
308
|
+
project_id: task.task.project_id,
|
|
309
|
+
...changes,
|
|
310
|
+
};
|
|
311
|
+
return this.clients.tasks.patch("/tasks", { body: updateData });
|
|
312
|
+
}, "task"),
|
|
313
|
+
/**
|
|
314
|
+
* Upsert task (create or update)
|
|
315
|
+
*/
|
|
316
|
+
upsert: (data) => wrap(() => this.clients.tasks.patch("/tasks", { body: data }), "task"),
|
|
317
|
+
/**
|
|
318
|
+
* Save tags for tasks
|
|
319
|
+
*/
|
|
320
|
+
saveTags: (data) => wrap(() => this.clients.tasks.post("/tasks/save_tags", { body: data }), "result"),
|
|
321
|
+
/**
|
|
322
|
+
* Delete task (soft delete)
|
|
323
|
+
*/
|
|
324
|
+
deleteTask: (task) => wrap(() => this.clients.tasks.patch("/tasks", { body: { ...task.task, deleted: true } }), "result"),
|
|
325
|
+
/**
|
|
326
|
+
* Task history operations
|
|
327
|
+
*/
|
|
328
|
+
history: {
|
|
329
|
+
/**
|
|
330
|
+
* Get task history by task ID
|
|
331
|
+
*/
|
|
332
|
+
get: (task_id) => wrap(() => this.clients.tasks.get(`/tasks/history/${task_id}`), "history"),
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
/**
|
|
336
|
+
* Tags namespace with Result-wrapped operations
|
|
337
|
+
*/
|
|
338
|
+
this.tags = {
|
|
339
|
+
/**
|
|
340
|
+
* List tags for authenticated user
|
|
341
|
+
*/
|
|
342
|
+
list: () => wrap(() => this.clients.tags.get("/tags"), "tags"),
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* GitHub namespace with Result-wrapped operations
|
|
346
|
+
*/
|
|
347
|
+
this.github = {
|
|
348
|
+
/**
|
|
349
|
+
* List repositories for authenticated user
|
|
350
|
+
*/
|
|
351
|
+
repos: () => wrap(() => this.clients.github.get("/repos"), "repos"),
|
|
352
|
+
/**
|
|
353
|
+
* List branches for a GitHub repository
|
|
354
|
+
*/
|
|
355
|
+
branches: (owner, repo) => wrap(() => this.clients.github.get(`/repos/${owner}/${repo}/branches`), "branches"),
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* User namespace with Result-wrapped operations
|
|
359
|
+
*/
|
|
360
|
+
this.user = {
|
|
361
|
+
/**
|
|
362
|
+
* Get user activity history
|
|
363
|
+
*/
|
|
364
|
+
history: () => wrap(() => this.clients.auth.get("/user/history"), "history"),
|
|
365
|
+
/**
|
|
366
|
+
* Update user preferences
|
|
367
|
+
*/
|
|
368
|
+
preferences: (data) => wrap(() => this.clients.auth.patch("/user/preferences", { body: data }), "result"),
|
|
369
|
+
};
|
|
370
|
+
const v0_base_url = options.base_url || "http://localhost:4321/api/v0";
|
|
371
|
+
this._api_key = options.api_key;
|
|
372
|
+
this._auth_mode = options.auth_mode || (options.api_key.startsWith("jwt:") ? "session" : "key");
|
|
373
|
+
// Create category-specific HTTP clients
|
|
374
|
+
const clientOptions = {
|
|
375
|
+
base_url: v0_base_url,
|
|
376
|
+
api_key: options.api_key,
|
|
377
|
+
max_history_size: options.max_history_size,
|
|
378
|
+
};
|
|
379
|
+
this.clients = {
|
|
380
|
+
auth: new HttpClient({ ...clientOptions, category: "auth" }),
|
|
381
|
+
projects: new HttpClient({ ...clientOptions, category: "projects" }),
|
|
382
|
+
tasks: new HttpClient({ ...clientOptions, category: "tasks" }),
|
|
383
|
+
milestones: new HttpClient({ ...clientOptions, category: "milestones" }),
|
|
384
|
+
goals: new HttpClient({ ...clientOptions, category: "goals" }),
|
|
385
|
+
github: new HttpClient({ ...clientOptions, category: "github" }),
|
|
386
|
+
tags: new HttpClient({ ...clientOptions, category: "tags" }),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Get request history for debugging
|
|
391
|
+
*/
|
|
392
|
+
history() {
|
|
393
|
+
return this.clients.projects.history();
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Get the API key
|
|
397
|
+
*/
|
|
398
|
+
getApiKey() {
|
|
399
|
+
return this._api_key;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Get the authentication mode
|
|
403
|
+
*/
|
|
404
|
+
getAuthMode() {
|
|
405
|
+
return this._auth_mode;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ApiError, AuthenticationError, NetworkError } from "./errors";
|
|
2
|
+
/**
|
|
3
|
+
* Centralized error handling utilities to reduce duplication
|
|
4
|
+
* across API client methods and improve consistency
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Standard HTTP status codes
|
|
8
|
+
*/
|
|
9
|
+
export declare const HTTP_STATUS: {
|
|
10
|
+
readonly OK: 200;
|
|
11
|
+
readonly CREATED: 201;
|
|
12
|
+
readonly NO_CONTENT: 204;
|
|
13
|
+
readonly BAD_REQUEST: 400;
|
|
14
|
+
readonly UNAUTHORIZED: 401;
|
|
15
|
+
readonly FORBIDDEN: 403;
|
|
16
|
+
readonly NOT_FOUND: 404;
|
|
17
|
+
readonly INTERNAL_SERVER_ERROR: 500;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Handle response based on status code with consistent error types
|
|
21
|
+
*/
|
|
22
|
+
export declare function handleHttpResponse(response: Response): void;
|
|
23
|
+
/**
|
|
24
|
+
* Parse and throw appropriate error from response text
|
|
25
|
+
*/
|
|
26
|
+
export declare function handleResponseError(response: Response): Promise<never>;
|
|
27
|
+
/**
|
|
28
|
+
* Handle network errors consistently
|
|
29
|
+
*/
|
|
30
|
+
export declare function handleNetworkError(error: unknown): never;
|
|
31
|
+
/**
|
|
32
|
+
* Type guard to check if error is an API error
|
|
33
|
+
*/
|
|
34
|
+
export declare function isApiError(error: unknown): error is ApiError;
|
|
35
|
+
/**
|
|
36
|
+
* Type guard to check if error is an authentication error
|
|
37
|
+
*/
|
|
38
|
+
export declare function isAuthenticationError(error: unknown): error is AuthenticationError;
|
|
39
|
+
/**
|
|
40
|
+
* Type guard to check if error is a network error
|
|
41
|
+
*/
|
|
42
|
+
export declare function isNetworkError(error: unknown): error is NetworkError;
|
|
43
|
+
/**
|
|
44
|
+
* Parse Zod validation errors into user-friendly messages
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseZodErrors(errorMessage: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Get user-friendly error message from any error
|
|
49
|
+
*/
|
|
50
|
+
export declare function getUserFriendlyErrorMessage(error: unknown): string;
|
|
51
|
+
/**
|
|
52
|
+
* Retry wrapper for API calls with exponential backoff
|
|
53
|
+
*/
|
|
54
|
+
export declare function withRetry<T>(operation: () => Promise<T>, maxRetries?: number, baseDelay?: number): Promise<T>;
|
|
55
|
+
//# sourceMappingURL=error-handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-handlers.d.ts","sourceRoot":"","sources":["../src/error-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,YAAY,EAAmB,MAAM,UAAU,CAAC;AAExF;;;GAGG;AAEH;;GAEG;AACH,eAAO,MAAM,WAAW;;;;;;;;;CASd,CAAC;AAEX;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAc3D;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,CAmB5E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAGxD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAE5D;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,mBAAmB,CAElF;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAEpE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAgF3D;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CA0BlE;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,UAAU,GAAE,MAAU,EAAE,SAAS,GAAE,MAAa,GAAG,OAAO,CAAC,CAAC,CAAC,CA0B5H"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { ApiError, AuthenticationError, NetworkError, ValidationError } from "./errors";
|
|
2
|
+
/**
|
|
3
|
+
* Centralized error handling utilities to reduce duplication
|
|
4
|
+
* across API client methods and improve consistency
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Standard HTTP status codes
|
|
8
|
+
*/
|
|
9
|
+
export const HTTP_STATUS = {
|
|
10
|
+
OK: 200,
|
|
11
|
+
CREATED: 201,
|
|
12
|
+
NO_CONTENT: 204,
|
|
13
|
+
BAD_REQUEST: 400,
|
|
14
|
+
UNAUTHORIZED: 401,
|
|
15
|
+
FORBIDDEN: 403,
|
|
16
|
+
NOT_FOUND: 404,
|
|
17
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Handle response based on status code with consistent error types
|
|
21
|
+
*/
|
|
22
|
+
export function handleHttpResponse(response) {
|
|
23
|
+
switch (response.status) {
|
|
24
|
+
case HTTP_STATUS.UNAUTHORIZED:
|
|
25
|
+
throw new AuthenticationError("Invalid or expired API key");
|
|
26
|
+
case HTTP_STATUS.NOT_FOUND:
|
|
27
|
+
throw new ApiError("Resource not found", { statusCode: HTTP_STATUS.NOT_FOUND });
|
|
28
|
+
case HTTP_STATUS.BAD_REQUEST:
|
|
29
|
+
// Will be handled by specific error text parsing
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new ApiError(`Request failed: ${response.statusText}`, { statusCode: response.status });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse and throw appropriate error from response text
|
|
39
|
+
*/
|
|
40
|
+
export async function handleResponseError(response) {
|
|
41
|
+
const errorText = await response.text();
|
|
42
|
+
const errorMessage = errorText || "Request failed";
|
|
43
|
+
// Try to parse structured error responses
|
|
44
|
+
let parsedError = null;
|
|
45
|
+
try {
|
|
46
|
+
parsedError = JSON.parse(errorText);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Not JSON, use raw text
|
|
50
|
+
}
|
|
51
|
+
if (response.status === HTTP_STATUS.BAD_REQUEST && parsedError?.error?.name === "ZodError") {
|
|
52
|
+
// Enhanced: Create a more detailed ValidationError with the original Zod error info
|
|
53
|
+
const zodErrorDetails = parsedError.error?.issues ? JSON.stringify(parsedError.error) : errorMessage;
|
|
54
|
+
throw new ValidationError(zodErrorDetails);
|
|
55
|
+
}
|
|
56
|
+
throw new ApiError(errorMessage, { statusCode: response.status });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Handle network errors consistently
|
|
60
|
+
*/
|
|
61
|
+
export function handleNetworkError(error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : "Unknown network error";
|
|
63
|
+
throw new NetworkError(message);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Type guard to check if error is an API error
|
|
67
|
+
*/
|
|
68
|
+
export function isApiError(error) {
|
|
69
|
+
return error instanceof ApiError;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Type guard to check if error is an authentication error
|
|
73
|
+
*/
|
|
74
|
+
export function isAuthenticationError(error) {
|
|
75
|
+
return error instanceof AuthenticationError;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Type guard to check if error is a network error
|
|
79
|
+
*/
|
|
80
|
+
export function isNetworkError(error) {
|
|
81
|
+
return error instanceof NetworkError;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parse Zod validation errors into user-friendly messages
|
|
85
|
+
*/
|
|
86
|
+
export function parseZodErrors(errorMessage) {
|
|
87
|
+
try {
|
|
88
|
+
// Try to parse as JSON first to get structured error info
|
|
89
|
+
let parsedError = null;
|
|
90
|
+
try {
|
|
91
|
+
parsedError = JSON.parse(errorMessage);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// If not JSON, try to extract Zod error details from the error message
|
|
95
|
+
const zodErrorMatch = errorMessage.match(/ZodError: (.+)/);
|
|
96
|
+
if (zodErrorMatch && zodErrorMatch[1]) {
|
|
97
|
+
try {
|
|
98
|
+
parsedError = JSON.parse(zodErrorMatch[1]);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// If still not JSON, try to extract from the message
|
|
102
|
+
const issuesMatch = errorMessage.match(/issues:\s*(\[.*\])/s);
|
|
103
|
+
if (issuesMatch && issuesMatch[1]) {
|
|
104
|
+
try {
|
|
105
|
+
parsedError = { issues: JSON.parse(issuesMatch[1]) };
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Fall back to basic parsing
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (parsedError?.issues && Array.isArray(parsedError.issues)) {
|
|
115
|
+
const friendlyMessages = parsedError.issues.map((issue) => {
|
|
116
|
+
const path = issue.path && issue.path.length > 0 ? issue.path.join(".") : "field";
|
|
117
|
+
const message = issue.message || "is invalid";
|
|
118
|
+
// Handle common validation types with friendly messages
|
|
119
|
+
switch (issue.code) {
|
|
120
|
+
case "invalid_type":
|
|
121
|
+
return `${path} must be a ${issue.expected} (received ${issue.received})`;
|
|
122
|
+
case "too_small":
|
|
123
|
+
if (issue.type === "string") {
|
|
124
|
+
return `${path} must be at least ${issue.minimum} characters long`;
|
|
125
|
+
}
|
|
126
|
+
if (issue.type === "number") {
|
|
127
|
+
return `${path} must be at least ${issue.minimum}`;
|
|
128
|
+
}
|
|
129
|
+
return `${path} is too small`;
|
|
130
|
+
case "too_big":
|
|
131
|
+
if (issue.type === "string") {
|
|
132
|
+
return `${path} must be no more than ${issue.maximum} characters long`;
|
|
133
|
+
}
|
|
134
|
+
if (issue.type === "number") {
|
|
135
|
+
return `${path} must be no more than ${issue.maximum}`;
|
|
136
|
+
}
|
|
137
|
+
return `${path} is too large`;
|
|
138
|
+
case "invalid_string":
|
|
139
|
+
if (issue.validation === "email") {
|
|
140
|
+
return `${path} must be a valid email address`;
|
|
141
|
+
}
|
|
142
|
+
if (issue.validation === "url") {
|
|
143
|
+
return `${path} must be a valid URL`;
|
|
144
|
+
}
|
|
145
|
+
if (issue.validation === "uuid") {
|
|
146
|
+
return `${path} must be a valid UUID`;
|
|
147
|
+
}
|
|
148
|
+
return `${path} format is invalid`;
|
|
149
|
+
case "custom":
|
|
150
|
+
return `${path}: ${message}`;
|
|
151
|
+
default:
|
|
152
|
+
return `${path}: ${message}`;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
if (friendlyMessages.length === 1) {
|
|
156
|
+
return friendlyMessages[0];
|
|
157
|
+
}
|
|
158
|
+
return `Validation failed:\n• ${friendlyMessages.join("\n• ")}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
// Fall back to original message if parsing fails
|
|
163
|
+
console.debug("Failed to parse Zod error:", e);
|
|
164
|
+
}
|
|
165
|
+
return errorMessage;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get user-friendly error message from any error
|
|
169
|
+
*/
|
|
170
|
+
export function getUserFriendlyErrorMessage(error) {
|
|
171
|
+
if (isAuthenticationError(error)) {
|
|
172
|
+
return "Please check your API key and try again";
|
|
173
|
+
}
|
|
174
|
+
if (isNetworkError(error)) {
|
|
175
|
+
return "Network connection issue. Please try again";
|
|
176
|
+
}
|
|
177
|
+
if (isApiError(error)) {
|
|
178
|
+
if (error.statusCode === HTTP_STATUS.NOT_FOUND) {
|
|
179
|
+
return "The requested resource was not found";
|
|
180
|
+
}
|
|
181
|
+
if (error.statusCode === HTTP_STATUS.BAD_REQUEST) {
|
|
182
|
+
return "Invalid request. Please check your data";
|
|
183
|
+
}
|
|
184
|
+
// Enhanced: Parse Zod validation errors for ValidationError types
|
|
185
|
+
if (error.code === "VALIDATION_ERROR" && error.message) {
|
|
186
|
+
return parseZodErrors(error.message);
|
|
187
|
+
}
|
|
188
|
+
return error.message || "An error occurred";
|
|
189
|
+
}
|
|
190
|
+
return "An unexpected error occurred";
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Retry wrapper for API calls with exponential backoff
|
|
194
|
+
*/
|
|
195
|
+
export async function withRetry(operation, maxRetries = 3, baseDelay = 1000) {
|
|
196
|
+
let lastError;
|
|
197
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
198
|
+
try {
|
|
199
|
+
return await operation();
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
lastError = error;
|
|
203
|
+
// Don't retry authentication or validation errors
|
|
204
|
+
if (isAuthenticationError(error) || error instanceof ValidationError) {
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
// Don't retry on final attempt
|
|
208
|
+
if (attempt === maxRetries) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
// Exponential backoff
|
|
212
|
+
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
213
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
throw lastError;
|
|
217
|
+
}
|