@ticatec/restful_service_api 0.6.1 → 0.8.0
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-CN.md +107 -28
- package/README.md +108 -28
- package/dist/FileService.d.ts +4 -4
- package/dist/RestService.d.ts +20 -13
- package/dist/UploadCallback.d.ts +2 -2
- package/package.json +1 -1
package/README-CN.md
CHANGED
|
@@ -18,6 +18,60 @@
|
|
|
18
18
|
- 🌐 **浏览器优先**: 专为前端应用程序设计
|
|
19
19
|
- ✨ **PATCH 支持**: 完整支持 HTTP PATCH 方法进行部分更新
|
|
20
20
|
|
|
21
|
+
## ⚠️ v0.8.0 重大变更
|
|
22
|
+
|
|
23
|
+
**`ErrorHandler` 已变更为返回 `Promise`(支持异步处理)。**
|
|
24
|
+
|
|
25
|
+
### 对您的影响
|
|
26
|
+
|
|
27
|
+
- `ErrorHandler` 函数类型现为 `(ex: Error) => Promise<boolean | void>`。
|
|
28
|
+
- 错误处理器现在应定义为 `async` 异步函数或返回 `Promise`。
|
|
29
|
+
- `UploadCallback.handleError` 统一采用 Promise 异步的 `ErrorHandler` 定义。
|
|
30
|
+
|
|
31
|
+
### 迁移指南
|
|
32
|
+
|
|
33
|
+
**之前(v0.7.x 及更早版本):**
|
|
34
|
+
```typescript
|
|
35
|
+
const errorHandler: ErrorHandler = (error: Error) => {
|
|
36
|
+
console.error('API 错误:', error);
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**之后(v0.8.0+):**
|
|
42
|
+
```typescript
|
|
43
|
+
const errorHandler: ErrorHandler = async (error: Error) => {
|
|
44
|
+
console.error('API 错误:', error);
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## ⚠️ v0.7.0 重大变更
|
|
50
|
+
|
|
51
|
+
**`RestfulOptions.contentType` 已被移除,改为使用通用的 `headers` 字段。**
|
|
52
|
+
|
|
53
|
+
### 对您的影响
|
|
54
|
+
|
|
55
|
+
- `RestfulOptions`(用于 `post`/`put`/`del`/`patch`)不再识别 `contentType` 字段,
|
|
56
|
+
请把 `{ contentType: 'multipart/form-data' }` 改为
|
|
57
|
+
`{ headers: { 'Content-Type': 'multipart/form-data' } }`。
|
|
58
|
+
- `get()` 新增了一个可选的第 4 个参数:`get(url, params?, dataProcessor?, headers?)`。
|
|
59
|
+
已有的按位置调用方式(`get(url)`、`get(url, params)`、`get(url, params, dataProcessor)`)不受影响。
|
|
60
|
+
- 这也让单次请求携带任意自定义请求头成为可能(不仅限于 Content-Type),例如通过
|
|
61
|
+
`If-None-Match` 实现条件 GET —— 参见"自定义请求头(例如条件请求)"一节。
|
|
62
|
+
|
|
63
|
+
### 迁移指南
|
|
64
|
+
|
|
65
|
+
**之前(v0.6.x 及更早版本):**
|
|
66
|
+
```typescript
|
|
67
|
+
await api.post('/upload', formData, { contentType: 'multipart/form-data' });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**之后(v0.7.0+):**
|
|
71
|
+
```typescript
|
|
72
|
+
await api.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } });
|
|
73
|
+
```
|
|
74
|
+
|
|
21
75
|
## ⚠️ v0.5.0 重大变更
|
|
22
76
|
|
|
23
77
|
**从 0.5.0 版本开始,此包已迁移到 ESM (ECMAScript Modules) 格式。**
|
|
@@ -59,7 +113,7 @@ import RestService, { FileService } from '@ticatec/restful_service_api';
|
|
|
59
113
|
|
|
60
114
|
// 实现 RestService 接口
|
|
61
115
|
class MyApiClient implements RestService {
|
|
62
|
-
async get(url: string, params?: any, dataProcessor?: DataProcessor) {
|
|
116
|
+
async get(url: string, params?: any, dataProcessor?: DataProcessor, headers?: Record<string, string>) {
|
|
63
117
|
// 在这里实现
|
|
64
118
|
}
|
|
65
119
|
|
|
@@ -109,7 +163,7 @@ const result = await fileApi.upload('/upload', { userId: 123 }, file);
|
|
|
109
163
|
|
|
110
164
|
```typescript
|
|
111
165
|
interface RestService {
|
|
112
|
-
get(url: string, params?: any, dataProcessor?: DataProcessor): Promise<any>;
|
|
166
|
+
get(url: string, params?: any, dataProcessor?: DataProcessor, headers?: Record<string, string>): Promise<any>;
|
|
113
167
|
post(url: string, data?: any, options?: RestfulOptions): Promise<any>;
|
|
114
168
|
put(url: string, data?: any, options?: RestfulOptions): Promise<any>;
|
|
115
169
|
patch(url: string, data?: any, options?: RestfulOptions): Promise<any>;
|
|
@@ -131,11 +185,12 @@ interface FileService {
|
|
|
131
185
|
|
|
132
186
|
### RestService 方法
|
|
133
187
|
|
|
134
|
-
#### `get(url, params?, dataProcessor?)`
|
|
188
|
+
#### `get(url, params?, dataProcessor?, headers?)`
|
|
135
189
|
执行 HTTP GET 请求获取资源。
|
|
136
190
|
- **url**: 接口端点 URL
|
|
137
191
|
- **params**: 查询参数(可选)
|
|
138
192
|
- **dataProcessor**: 处理响应数据的函数(可选)
|
|
193
|
+
- **headers**: 本次调用的自定义请求头(可选),例如条件请求:`{ 'If-None-Match': contentHash }`
|
|
139
194
|
|
|
140
195
|
#### `post(url, data?, options?)`
|
|
141
196
|
执行 HTTP POST 请求创建新资源。
|
|
@@ -143,7 +198,8 @@ interface FileService {
|
|
|
143
198
|
- **data**: 请求载荷(可选)
|
|
144
199
|
- **options**: 可选的配置对象,包含:
|
|
145
200
|
- **params**: 查询参数(可选)
|
|
146
|
-
- **
|
|
201
|
+
- **headers**: 本次调用的自定义请求头(可选)。可用于覆盖默认的 `Content-Type`
|
|
202
|
+
(`application/json`),例如 `{ 'Content-Type': 'multipart/form-data' }`
|
|
147
203
|
- **dataProcessor**: 处理响应数据的函数(可选)
|
|
148
204
|
|
|
149
205
|
#### `put(url, data?, options?)`
|
|
@@ -152,7 +208,7 @@ interface FileService {
|
|
|
152
208
|
- **data**: 请求载荷(可选)
|
|
153
209
|
- **options**: 可选的配置对象,包含:
|
|
154
210
|
- **params**: 查询参数(可选)
|
|
155
|
-
- **
|
|
211
|
+
- **headers**: 本次调用的自定义请求头(可选)
|
|
156
212
|
- **dataProcessor**: 处理响应数据的函数(可选)
|
|
157
213
|
|
|
158
214
|
#### `patch(url, data?, options?)`
|
|
@@ -161,7 +217,7 @@ interface FileService {
|
|
|
161
217
|
- **data**: 包含要更新字段的请求载荷(可选)
|
|
162
218
|
- **options**: 可选的配置对象,包含:
|
|
163
219
|
- **params**: 查询参数(可选)
|
|
164
|
-
- **
|
|
220
|
+
- **headers**: 本次调用的自定义请求头(可选)
|
|
165
221
|
- **dataProcessor**: 处理响应数据的函数(可选)
|
|
166
222
|
|
|
167
223
|
#### `del(url, data?, options?)`
|
|
@@ -170,7 +226,7 @@ interface FileService {
|
|
|
170
226
|
- **data**: 请求体数据(可选)
|
|
171
227
|
- **options**: 可选的配置对象,包含:
|
|
172
228
|
- **params**: 查询参数(可选)
|
|
173
|
-
- **
|
|
229
|
+
- **headers**: 本次调用的自定义请求头(可选)
|
|
174
230
|
- **dataProcessor**: 处理响应数据的函数(可选)
|
|
175
231
|
|
|
176
232
|
### FileService 方法
|
|
@@ -232,7 +288,7 @@ import {
|
|
|
232
288
|
// RestfulOptions 用于配置请求
|
|
233
289
|
const options: RestfulOptions = {
|
|
234
290
|
params: { page: 1, limit: 10 },
|
|
235
|
-
|
|
291
|
+
headers: { 'Content-Type': 'application/json' },
|
|
236
292
|
dataProcessor: (data: any) => data.results || data
|
|
237
293
|
};
|
|
238
294
|
|
|
@@ -254,7 +310,7 @@ const postInterceptor: PostInterceptor = async (data: any) => {
|
|
|
254
310
|
};
|
|
255
311
|
|
|
256
312
|
// 错误处理器
|
|
257
|
-
const errorHandler: ErrorHandler = (error: Error) => {
|
|
313
|
+
const errorHandler: ErrorHandler = async (error: Error) => {
|
|
258
314
|
console.error('API 错误:', error);
|
|
259
315
|
return true; // 返回 true 表示错误已处理
|
|
260
316
|
};
|
|
@@ -278,8 +334,34 @@ import {
|
|
|
278
334
|
TYPE_TEXT
|
|
279
335
|
} from '@ticatec/restful_service_api';
|
|
280
336
|
|
|
281
|
-
// 使用示例
|
|
282
|
-
await api.post('/upload', data, {
|
|
337
|
+
// 使用示例 - 通过 RestfulOptions.headers 显式设置请求头
|
|
338
|
+
await api.post('/upload', data, { headers: { [CONTENT_TYPE_NAME]: TYPE_JSON } });
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 自定义请求头(例如条件请求)
|
|
342
|
+
|
|
343
|
+
`RestfulOptions.headers`(以及 `get()` 的第 4 个参数 `headers`)允许为单次请求附加自定义请求头。
|
|
344
|
+
一个常见场景是基于本地缓存的 ETag/hash 发起条件 GET 请求:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// 第一次请求:还没有缓存的 hash
|
|
348
|
+
const pack = await api.get('/messages/node1');
|
|
349
|
+
localStorage.setItem('node1-hash', pack.contentHash);
|
|
350
|
+
|
|
351
|
+
// 之后:把缓存的 hash 作为 If-None-Match 发送
|
|
352
|
+
const cachedHash = localStorage.getItem('node1-hash');
|
|
353
|
+
try {
|
|
354
|
+
const updated = await api.get('/messages/node1', undefined, undefined, {
|
|
355
|
+
'If-None-Match': cachedHash ?? ''
|
|
356
|
+
});
|
|
357
|
+
// 服务端返回 200 —— 内容有变化,使用 `updated`
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (error instanceof ApiError && Number(error.status) === 304) {
|
|
360
|
+
// 服务端返回 304 Not Modified —— 本地缓存仍然有效
|
|
361
|
+
} else {
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
283
365
|
```
|
|
284
366
|
|
|
285
367
|
## 实现示例
|
|
@@ -309,48 +391,45 @@ class FetchRestService implements RestService {
|
|
|
309
391
|
this.postInterceptor = postInterceptor;
|
|
310
392
|
}
|
|
311
393
|
|
|
312
|
-
async get(url: string, params?: any, dataProcessor?: DataProcessor): Promise<any> {
|
|
394
|
+
async get(url: string, params?: any, dataProcessor?: DataProcessor, headers?: Record<string, string>): Promise<any> {
|
|
313
395
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
314
|
-
return this.request('GET', url + queryString, null,
|
|
396
|
+
return this.request('GET', url + queryString, null, headers, dataProcessor);
|
|
315
397
|
}
|
|
316
398
|
|
|
317
399
|
async post(url: string, data?: any, options?: RestfulOptions): Promise<any> {
|
|
318
|
-
const contentType = options?.contentType || TYPE_JSON;
|
|
319
400
|
const params = options?.params;
|
|
320
401
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
321
|
-
return this.request('POST', url + queryString, data,
|
|
402
|
+
return this.request('POST', url + queryString, data, options?.headers, options?.dataProcessor);
|
|
322
403
|
}
|
|
323
404
|
|
|
324
405
|
async put(url: string, data?: any, options?: RestfulOptions): Promise<any> {
|
|
325
|
-
const contentType = options?.contentType || TYPE_JSON;
|
|
326
406
|
const params = options?.params;
|
|
327
407
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
328
|
-
return this.request('PUT', url + queryString, data,
|
|
408
|
+
return this.request('PUT', url + queryString, data, options?.headers, options?.dataProcessor);
|
|
329
409
|
}
|
|
330
410
|
|
|
331
411
|
async patch(url: string, data?: any, options?: RestfulOptions): Promise<any> {
|
|
332
|
-
const contentType = options?.contentType || TYPE_JSON;
|
|
333
412
|
const params = options?.params;
|
|
334
413
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
335
|
-
return this.request('PATCH', url + queryString, data,
|
|
414
|
+
return this.request('PATCH', url + queryString, data, options?.headers, options?.dataProcessor);
|
|
336
415
|
}
|
|
337
416
|
|
|
338
417
|
async del(url: string, data?: any, options?: RestfulOptions): Promise<any> {
|
|
339
|
-
const contentType = options?.contentType || TYPE_JSON;
|
|
340
418
|
const params = options?.params;
|
|
341
419
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
342
|
-
return this.request('DELETE', url + queryString, data,
|
|
420
|
+
return this.request('DELETE', url + queryString, data, options?.headers, options?.dataProcessor);
|
|
343
421
|
}
|
|
344
422
|
|
|
345
|
-
private async request(method: string, url: string, body?: any,
|
|
423
|
+
private async request(method: string, url: string, body?: any, customHeaders?: Record<string, string>, dataProcessor?: DataProcessor): Promise<any> {
|
|
346
424
|
const fullUrl = this.baseURL + url;
|
|
347
425
|
|
|
348
426
|
// 应用请求前拦截器
|
|
349
427
|
const interceptorResult = this.preInterceptor?.(method, fullUrl) || { headers: {} };
|
|
350
428
|
|
|
351
429
|
const headers = {
|
|
430
|
+
'Content-Type': TYPE_JSON,
|
|
352
431
|
...interceptorResult.headers,
|
|
353
|
-
...
|
|
432
|
+
...customHeaders
|
|
354
433
|
};
|
|
355
434
|
|
|
356
435
|
try {
|
|
@@ -447,18 +526,18 @@ class FetchFileService implements FileService {
|
|
|
447
526
|
}
|
|
448
527
|
});
|
|
449
528
|
|
|
450
|
-
xhr.addEventListener('load', () => {
|
|
529
|
+
xhr.addEventListener('load', async () => {
|
|
451
530
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
452
531
|
const data = JSON.parse(xhr.responseText);
|
|
453
532
|
callback.onCompleted(data);
|
|
454
533
|
} else if (callback.handleError) {
|
|
455
|
-
callback.handleError(new Error(`上传失败: ${xhr.statusText}`));
|
|
534
|
+
await callback.handleError(new Error(`上传失败: ${xhr.statusText}`));
|
|
456
535
|
}
|
|
457
536
|
});
|
|
458
537
|
|
|
459
|
-
xhr.addEventListener('error', () => {
|
|
538
|
+
xhr.addEventListener('error', async () => {
|
|
460
539
|
if (callback.handleError) {
|
|
461
|
-
callback.handleError(new Error('网络错误'));
|
|
540
|
+
await callback.handleError(new Error('网络错误'));
|
|
462
541
|
}
|
|
463
542
|
});
|
|
464
543
|
|
|
@@ -521,7 +600,7 @@ const progress = await fileApi.asyncUpload('/upload', { userId: 123 }, file, {
|
|
|
521
600
|
method: 'POST',
|
|
522
601
|
progressUpdate: (loaded) => console.log(`已上传: ${loaded} 字节`),
|
|
523
602
|
onCompleted: (data) => console.log('完成!', data),
|
|
524
|
-
handleError: (err) => console.error('错误!', err)
|
|
603
|
+
handleError: async (err) => console.error('错误!', err)
|
|
525
604
|
});
|
|
526
605
|
|
|
527
606
|
// 下载文件
|
package/README.md
CHANGED
|
@@ -18,6 +18,59 @@ A lightweight TypeScript RESTful API client for browsers with comprehensive erro
|
|
|
18
18
|
- 🌐 **Browser-First**: Designed specifically for frontend applications
|
|
19
19
|
- ✨ **PATCH Support**: Full support for HTTP PATCH method for partial updates
|
|
20
20
|
|
|
21
|
+
## ⚠️ Breaking Changes in v0.8.0
|
|
22
|
+
|
|
23
|
+
**`ErrorHandler` has been changed to return a `Promise` (async support).**
|
|
24
|
+
|
|
25
|
+
### What This Means for You
|
|
26
|
+
|
|
27
|
+
- The `ErrorHandler` function type is now `(ex: Error) => Promise<boolean | void>`.
|
|
28
|
+
- Error handler functions should now be `async` functions or return a `Promise`.
|
|
29
|
+
- `UploadCallback.handleError` now adopts the unified Promise-based `ErrorHandler` type.
|
|
30
|
+
|
|
31
|
+
### Migration Guide
|
|
32
|
+
|
|
33
|
+
**Before (v0.7.x and earlier):**
|
|
34
|
+
```typescript
|
|
35
|
+
const errorHandler: ErrorHandler = (error: Error) => {
|
|
36
|
+
console.error('API Error:', error);
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**After (v0.8.0+):**
|
|
42
|
+
```typescript
|
|
43
|
+
const errorHandler: ErrorHandler = async (error: Error) => {
|
|
44
|
+
console.error('API Error:', error);
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## ⚠️ Breaking Changes in v0.7.0
|
|
50
|
+
|
|
51
|
+
**`RestfulOptions.contentType` has been removed in favor of a general-purpose `headers` field.**
|
|
52
|
+
|
|
53
|
+
### What This Means for You
|
|
54
|
+
|
|
55
|
+
- `contentType` is no longer a recognized field on `RestfulOptions` (used by `post`/`put`/`del`/`patch`).
|
|
56
|
+
Replace `{ contentType: 'multipart/form-data' }` with `{ headers: { 'Content-Type': 'multipart/form-data' } }`.
|
|
57
|
+
- `get()` gained a new, optional 4th parameter: `get(url, params?, dataProcessor?, headers?)`. Existing
|
|
58
|
+
positional calls (`get(url)`, `get(url, params)`, `get(url, params, dataProcessor)`) are unaffected.
|
|
59
|
+
- This also enables custom per-call headers in general (not just Content-Type), e.g. a conditional
|
|
60
|
+
GET via `If-None-Match` — see [Custom Headers (e.g. Conditional Requests)](#custom-headers-eg-conditional-requests).
|
|
61
|
+
|
|
62
|
+
### Migration Guide
|
|
63
|
+
|
|
64
|
+
**Before (v0.6.x and earlier):**
|
|
65
|
+
```typescript
|
|
66
|
+
await api.post('/upload', formData, { contentType: 'multipart/form-data' });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**After (v0.7.0+):**
|
|
70
|
+
```typescript
|
|
71
|
+
await api.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } });
|
|
72
|
+
```
|
|
73
|
+
|
|
21
74
|
## ⚠️ Breaking Changes in v0.5.0
|
|
22
75
|
|
|
23
76
|
**Starting from version 0.5.0, this package has migrated to ESM (ECMAScript Modules) format.**
|
|
@@ -58,7 +111,7 @@ import RestService, { FileService } from '@ticatec/restful_service_api';
|
|
|
58
111
|
|
|
59
112
|
// Your implementation of RestService interface
|
|
60
113
|
class MyApiClient implements RestService {
|
|
61
|
-
async get(url: string, params?: any, dataProcessor?: DataProcessor) {
|
|
114
|
+
async get(url: string, params?: any, dataProcessor?: DataProcessor, headers?: Record<string, string>) {
|
|
62
115
|
// Implementation here
|
|
63
116
|
}
|
|
64
117
|
|
|
@@ -108,7 +161,7 @@ The main interface for standard HTTP REST operations:
|
|
|
108
161
|
|
|
109
162
|
```typescript
|
|
110
163
|
interface RestService {
|
|
111
|
-
get(url: string, params?: any, dataProcessor?: DataProcessor): Promise<any>;
|
|
164
|
+
get(url: string, params?: any, dataProcessor?: DataProcessor, headers?: Record<string, string>): Promise<any>;
|
|
112
165
|
post(url: string, data?: any, options?: RestfulOptions): Promise<any>;
|
|
113
166
|
put(url: string, data?: any, options?: RestfulOptions): Promise<any>;
|
|
114
167
|
patch(url: string, data?: any, options?: RestfulOptions): Promise<any>;
|
|
@@ -130,11 +183,13 @@ interface FileService {
|
|
|
130
183
|
|
|
131
184
|
### RestService Methods
|
|
132
185
|
|
|
133
|
-
#### `get(url, params?, dataProcessor?)`
|
|
186
|
+
#### `get(url, params?, dataProcessor?, headers?)`
|
|
134
187
|
Performs HTTP GET request to retrieve a resource.
|
|
135
188
|
- **url**: The endpoint URL
|
|
136
189
|
- **params**: Query parameters (optional)
|
|
137
190
|
- **dataProcessor**: Function to process response data (optional)
|
|
191
|
+
- **headers**: Custom request headers for this single call (optional), e.g. for a conditional
|
|
192
|
+
request: `{ 'If-None-Match': contentHash }`
|
|
138
193
|
|
|
139
194
|
#### `post(url, data?, options?)`
|
|
140
195
|
Performs HTTP POST request to create a new resource.
|
|
@@ -142,7 +197,8 @@ Performs HTTP POST request to create a new resource.
|
|
|
142
197
|
- **data**: Request payload (optional)
|
|
143
198
|
- **options**: Optional configuration object containing:
|
|
144
199
|
- **params**: Query parameters (optional)
|
|
145
|
-
- **
|
|
200
|
+
- **headers**: Custom request headers for this single call (optional). Use this to override
|
|
201
|
+
the default `Content-Type` (`application/json`), e.g. `{ 'Content-Type': 'multipart/form-data' }`
|
|
146
202
|
- **dataProcessor**: Function to process response data (optional)
|
|
147
203
|
|
|
148
204
|
#### `put(url, data?, options?)`
|
|
@@ -151,7 +207,7 @@ Performs HTTP PUT request to update an entire resource.
|
|
|
151
207
|
- **data**: Request payload (optional)
|
|
152
208
|
- **options**: Optional configuration object containing:
|
|
153
209
|
- **params**: Query parameters (optional)
|
|
154
|
-
- **
|
|
210
|
+
- **headers**: Custom request headers for this single call (optional)
|
|
155
211
|
- **dataProcessor**: Function to process response data (optional)
|
|
156
212
|
|
|
157
213
|
#### `patch(url, data?, options?)`
|
|
@@ -160,7 +216,7 @@ Performs HTTP PATCH request to partially update a resource.
|
|
|
160
216
|
- **data**: Request payload with fields to update (optional)
|
|
161
217
|
- **options**: Optional configuration object containing:
|
|
162
218
|
- **params**: Query parameters (optional)
|
|
163
|
-
- **
|
|
219
|
+
- **headers**: Custom request headers for this single call (optional)
|
|
164
220
|
- **dataProcessor**: Function to process response data (optional)
|
|
165
221
|
|
|
166
222
|
#### `del(url, data?, options?)`
|
|
@@ -169,7 +225,7 @@ Performs HTTP DELETE request to delete a resource.
|
|
|
169
225
|
- **data**: Request body data (optional)
|
|
170
226
|
- **options**: Optional configuration object containing:
|
|
171
227
|
- **params**: Query parameters (optional)
|
|
172
|
-
- **
|
|
228
|
+
- **headers**: Custom request headers for this single call (optional)
|
|
173
229
|
- **dataProcessor**: Function to process response data (optional)
|
|
174
230
|
|
|
175
231
|
### FileService Methods
|
|
@@ -231,7 +287,7 @@ import {
|
|
|
231
287
|
// RestfulOptions for configuring requests
|
|
232
288
|
const options: RestfulOptions = {
|
|
233
289
|
params: { page: 1, limit: 10 },
|
|
234
|
-
|
|
290
|
+
headers: { 'Content-Type': 'application/json' },
|
|
235
291
|
dataProcessor: (data: any) => data.results || data
|
|
236
292
|
};
|
|
237
293
|
|
|
@@ -253,7 +309,7 @@ const postInterceptor: PostInterceptor = async (data: any) => {
|
|
|
253
309
|
};
|
|
254
310
|
|
|
255
311
|
// Error handler
|
|
256
|
-
const errorHandler: ErrorHandler = (error: Error) => {
|
|
312
|
+
const errorHandler: ErrorHandler = async (error: Error) => {
|
|
257
313
|
console.error('API Error:', error);
|
|
258
314
|
return true; // Return true if error is handled
|
|
259
315
|
};
|
|
@@ -277,8 +333,35 @@ import {
|
|
|
277
333
|
TYPE_TEXT
|
|
278
334
|
} from '@ticatec/restful_service_api';
|
|
279
335
|
|
|
280
|
-
// Usage
|
|
281
|
-
await api.post('/upload', data, {
|
|
336
|
+
// Usage - set a header explicitly via RestfulOptions.headers
|
|
337
|
+
await api.post('/upload', data, { headers: { [CONTENT_TYPE_NAME]: TYPE_JSON } });
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Custom Headers (e.g. Conditional Requests)
|
|
341
|
+
|
|
342
|
+
`RestfulOptions.headers` (and the 4th `headers` parameter of `get()`) let you attach custom
|
|
343
|
+
request headers to a single call. A common use case is a conditional GET based on a
|
|
344
|
+
previously-cached ETag/hash:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// First request: no cached hash yet
|
|
348
|
+
const pack = await api.get('/messages/node1');
|
|
349
|
+
localStorage.setItem('node1-hash', pack.contentHash);
|
|
350
|
+
|
|
351
|
+
// Later: send the cached hash as If-None-Match
|
|
352
|
+
const cachedHash = localStorage.getItem('node1-hash');
|
|
353
|
+
try {
|
|
354
|
+
const updated = await api.get('/messages/node1', undefined, undefined, {
|
|
355
|
+
'If-None-Match': cachedHash ?? ''
|
|
356
|
+
});
|
|
357
|
+
// Server responded 200 - content changed, use `updated`
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (error instanceof ApiError && Number(error.status) === 304) {
|
|
360
|
+
// Server responded 304 Not Modified - the local cache is still valid
|
|
361
|
+
} else {
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
282
365
|
```
|
|
283
366
|
|
|
284
367
|
## Implementation Example
|
|
@@ -308,48 +391,45 @@ class FetchRestService implements RestService {
|
|
|
308
391
|
this.postInterceptor = postInterceptor;
|
|
309
392
|
}
|
|
310
393
|
|
|
311
|
-
async get(url: string, params?: any, dataProcessor?: DataProcessor): Promise<any> {
|
|
394
|
+
async get(url: string, params?: any, dataProcessor?: DataProcessor, headers?: Record<string, string>): Promise<any> {
|
|
312
395
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
313
|
-
return this.request('GET', url + queryString, null,
|
|
396
|
+
return this.request('GET', url + queryString, null, headers, dataProcessor);
|
|
314
397
|
}
|
|
315
398
|
|
|
316
399
|
async post(url: string, data?: any, options?: RestfulOptions): Promise<any> {
|
|
317
|
-
const contentType = options?.contentType || TYPE_JSON;
|
|
318
400
|
const params = options?.params;
|
|
319
401
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
320
|
-
return this.request('POST', url + queryString, data,
|
|
402
|
+
return this.request('POST', url + queryString, data, options?.headers, options?.dataProcessor);
|
|
321
403
|
}
|
|
322
404
|
|
|
323
405
|
async put(url: string, data?: any, options?: RestfulOptions): Promise<any> {
|
|
324
|
-
const contentType = options?.contentType || TYPE_JSON;
|
|
325
406
|
const params = options?.params;
|
|
326
407
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
327
|
-
return this.request('PUT', url + queryString, data,
|
|
408
|
+
return this.request('PUT', url + queryString, data, options?.headers, options?.dataProcessor);
|
|
328
409
|
}
|
|
329
410
|
|
|
330
411
|
async patch(url: string, data?: any, options?: RestfulOptions): Promise<any> {
|
|
331
|
-
const contentType = options?.contentType || TYPE_JSON;
|
|
332
412
|
const params = options?.params;
|
|
333
413
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
334
|
-
return this.request('PATCH', url + queryString, data,
|
|
414
|
+
return this.request('PATCH', url + queryString, data, options?.headers, options?.dataProcessor);
|
|
335
415
|
}
|
|
336
416
|
|
|
337
417
|
async del(url: string, data?: any, options?: RestfulOptions): Promise<any> {
|
|
338
|
-
const contentType = options?.contentType || TYPE_JSON;
|
|
339
418
|
const params = options?.params;
|
|
340
419
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
341
|
-
return this.request('DELETE', url + queryString, data,
|
|
420
|
+
return this.request('DELETE', url + queryString, data, options?.headers, options?.dataProcessor);
|
|
342
421
|
}
|
|
343
422
|
|
|
344
|
-
private async request(method: string, url: string, body?: any,
|
|
423
|
+
private async request(method: string, url: string, body?: any, customHeaders?: Record<string, string>, dataProcessor?: DataProcessor): Promise<any> {
|
|
345
424
|
const fullUrl = this.baseURL + url;
|
|
346
425
|
|
|
347
426
|
// Apply pre-interceptor
|
|
348
427
|
const interceptorResult = this.preInterceptor?.(method, fullUrl) || { headers: {} };
|
|
349
428
|
|
|
350
429
|
const headers = {
|
|
430
|
+
'Content-Type': TYPE_JSON,
|
|
351
431
|
...interceptorResult.headers,
|
|
352
|
-
...
|
|
432
|
+
...customHeaders
|
|
353
433
|
};
|
|
354
434
|
|
|
355
435
|
try {
|
|
@@ -446,18 +526,18 @@ class FetchFileService implements FileService {
|
|
|
446
526
|
}
|
|
447
527
|
});
|
|
448
528
|
|
|
449
|
-
xhr.addEventListener('load', () => {
|
|
529
|
+
xhr.addEventListener('load', async () => {
|
|
450
530
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
451
531
|
const data = JSON.parse(xhr.responseText);
|
|
452
532
|
callback.onCompleted(data);
|
|
453
533
|
} else if (callback.handleError) {
|
|
454
|
-
callback.handleError(new Error(`Upload failed: ${xhr.statusText}`));
|
|
534
|
+
await callback.handleError(new Error(`Upload failed: ${xhr.statusText}`));
|
|
455
535
|
}
|
|
456
536
|
});
|
|
457
537
|
|
|
458
|
-
xhr.addEventListener('error', () => {
|
|
538
|
+
xhr.addEventListener('error', async () => {
|
|
459
539
|
if (callback.handleError) {
|
|
460
|
-
callback.handleError(new Error('Network error'));
|
|
540
|
+
await callback.handleError(new Error('Network error'));
|
|
461
541
|
}
|
|
462
542
|
});
|
|
463
543
|
|
|
@@ -520,7 +600,7 @@ const progress = await fileApi.asyncUpload('/upload', { userId: 123 }, file, {
|
|
|
520
600
|
method: 'POST',
|
|
521
601
|
progressUpdate: (loaded) => console.log(`Uploaded: ${loaded} bytes`),
|
|
522
602
|
onCompleted: (data) => console.log('Done!', data),
|
|
523
|
-
handleError: (err) => console.error('Error!', err)
|
|
603
|
+
handleError: async (err) => console.error('Error!', err)
|
|
524
604
|
});
|
|
525
605
|
|
|
526
606
|
// Download file
|
package/dist/FileService.d.ts
CHANGED
|
@@ -45,20 +45,20 @@ export default interface FileService {
|
|
|
45
45
|
* { userId: 123 },
|
|
46
46
|
* file,
|
|
47
47
|
* {
|
|
48
|
-
*
|
|
49
|
-
* console.log(`
|
|
48
|
+
* progressUpdate: (uploadBytes) => {
|
|
49
|
+
* console.log(`Uploaded: ${uploadBytes} bytes`);
|
|
50
50
|
* },
|
|
51
51
|
* onCompleted: (data) => {
|
|
52
52
|
* console.log('Upload complete:', data);
|
|
53
53
|
* },
|
|
54
|
-
*
|
|
54
|
+
* handleError: async (error) => {
|
|
55
55
|
* console.error('Upload failed:', error);
|
|
56
56
|
* }
|
|
57
57
|
* }
|
|
58
58
|
* );
|
|
59
59
|
*
|
|
60
60
|
* // Cancel the upload
|
|
61
|
-
* progress.
|
|
61
|
+
* progress.cancel();
|
|
62
62
|
* ```
|
|
63
63
|
*/
|
|
64
64
|
asyncUpload(url: string, params: any, file: File, callback: UploadCallback, fileKey?: string): Promise<UploadProgress>;
|
package/dist/RestService.d.ts
CHANGED
|
@@ -31,17 +31,13 @@ export type PostInterceptor = (data: any) => Promise<any>;
|
|
|
31
31
|
/**
|
|
32
32
|
* Error handler function type for handling request errors
|
|
33
33
|
* @param ex The error object
|
|
34
|
-
* @returns
|
|
34
|
+
* @returns Promise that resolves to boolean or void. Errors always reject the returned Promise.
|
|
35
35
|
*/
|
|
36
|
-
export type ErrorHandler = (ex: Error) => boolean | void
|
|
36
|
+
export type ErrorHandler = (ex: Error) => Promise<boolean | void>;
|
|
37
37
|
/**
|
|
38
38
|
* RESTful API request options
|
|
39
39
|
*/
|
|
40
40
|
export type RestfulOptions<T = any> = {
|
|
41
|
-
/**
|
|
42
|
-
* Optional Content-Type header, defaults to 'application/json'
|
|
43
|
-
*/
|
|
44
|
-
contentType?: string;
|
|
45
41
|
/**
|
|
46
42
|
* Optional data processing function to transform response data before returning
|
|
47
43
|
*/
|
|
@@ -50,6 +46,13 @@ export type RestfulOptions<T = any> = {
|
|
|
50
46
|
* Optional query parameters object that will be converted to URL query string
|
|
51
47
|
*/
|
|
52
48
|
params?: any;
|
|
49
|
+
/**
|
|
50
|
+
* Optional custom request headers for this single call (e.g. { 'If-None-Match': contentHash },
|
|
51
|
+
* or { 'Content-Type': 'multipart/form-data' } to override the default JSON content type).
|
|
52
|
+
* These are merged on top of any headers produced by the PreInterceptor, and take precedence
|
|
53
|
+
* over them when the same header name is used.
|
|
54
|
+
*/
|
|
55
|
+
headers?: Record<string, string>;
|
|
53
56
|
};
|
|
54
57
|
/**
|
|
55
58
|
* HTTP header name for Content-Type
|
|
@@ -76,6 +79,7 @@ export default interface RestService {
|
|
|
76
79
|
* @param url The target URL path for the request
|
|
77
80
|
* @param params Query parameters object that will be converted to URL query string
|
|
78
81
|
* @param dataProcessor Optional data processing function to transform response data before returning
|
|
82
|
+
* @param headers Optional custom request headers for this single call (e.g. conditional GET via If-None-Match)
|
|
79
83
|
* @returns Promise that resolves to the server response data
|
|
80
84
|
*
|
|
81
85
|
* @example
|
|
@@ -88,14 +92,17 @@ export default interface RestService {
|
|
|
88
92
|
*
|
|
89
93
|
* // GET with data processor
|
|
90
94
|
* const users = await restService.get('/api/users', null, (data) => data.items);
|
|
95
|
+
*
|
|
96
|
+
* // GET with a custom header (e.g. conditional GET)
|
|
97
|
+
* const pack = await restService.get('/api/messages/node1', null, undefined, { 'If-None-Match': contentHash });
|
|
91
98
|
* ```
|
|
92
99
|
*/
|
|
93
|
-
get<T = any>(url: string, params?: any, dataProcessor?: DataProcessor<T>): Promise<T>;
|
|
100
|
+
get<T = any>(url: string, params?: any, dataProcessor?: DataProcessor<T>, headers?: Record<string, string>): Promise<T>;
|
|
94
101
|
/**
|
|
95
102
|
* Performs an HTTP POST request to create a new resource
|
|
96
103
|
* @param url The target URL path for the request
|
|
97
104
|
* @param data Request body data to be sent, typically an object or string
|
|
98
|
-
* @param options Optional request configuration options, including
|
|
105
|
+
* @param options Optional request configuration options, including headers and dataProcessor
|
|
99
106
|
* @returns Promise that resolves to the server response data
|
|
100
107
|
*
|
|
101
108
|
* @example
|
|
@@ -103,9 +110,9 @@ export default interface RestService {
|
|
|
103
110
|
* // Simple POST request
|
|
104
111
|
* const newUser = await restService.post('/api/users', { name: 'John', age: 30 });
|
|
105
112
|
*
|
|
106
|
-
* // POST with custom
|
|
113
|
+
* // POST with a custom Content-Type header
|
|
107
114
|
* const result = await restService.post('/api/data', formData, {
|
|
108
|
-
*
|
|
115
|
+
* headers: { 'Content-Type': 'multipart/form-data' }
|
|
109
116
|
* });
|
|
110
117
|
*
|
|
111
118
|
* // POST with data processor
|
|
@@ -119,7 +126,7 @@ export default interface RestService {
|
|
|
119
126
|
* Performs an HTTP PUT request to update an existing resource
|
|
120
127
|
* @param url The target URL path for the request
|
|
121
128
|
* @param data Optional request body data to be sent for updating the resource
|
|
122
|
-
* @param options Optional request configuration options, including
|
|
129
|
+
* @param options Optional request configuration options, including headers and dataProcessor
|
|
123
130
|
* @returns Promise that resolves to the server response data
|
|
124
131
|
*
|
|
125
132
|
* @example
|
|
@@ -136,7 +143,7 @@ export default interface RestService {
|
|
|
136
143
|
* Performs an HTTP DELETE request to delete a resource
|
|
137
144
|
* @param url The target URL path for the request
|
|
138
145
|
* @param data Optional request body data, some DELETE operations may require sending data
|
|
139
|
-
* @param options Optional request configuration options, including
|
|
146
|
+
* @param options Optional request configuration options, including headers and dataProcessor
|
|
140
147
|
* @returns Promise that resolves to the server response data
|
|
141
148
|
*
|
|
142
149
|
* @example
|
|
@@ -153,7 +160,7 @@ export default interface RestService {
|
|
|
153
160
|
* Performs an HTTP PATCH request to partially update a resource
|
|
154
161
|
* @param url The target URL path for the request
|
|
155
162
|
* @param data Optional request body data to be sent for partially updating the resource
|
|
156
|
-
* @param options Optional request configuration options, including
|
|
163
|
+
* @param options Optional request configuration options, including headers and dataProcessor
|
|
157
164
|
* @returns Promise that resolves to the server response data
|
|
158
165
|
*
|
|
159
166
|
* @example
|
package/dist/UploadCallback.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ErrorHandler } from "./RestService.js";
|
|
1
2
|
/**
|
|
2
3
|
* Upload progress update callback function type
|
|
3
4
|
* @param uploadBytes Number of bytes uploaded
|
|
@@ -5,9 +6,8 @@
|
|
|
5
6
|
export type ProgressUpdate = (uploadBytes: number) => void;
|
|
6
7
|
/**
|
|
7
8
|
* Upload error handling callback function type
|
|
8
|
-
* @param e Error object that occurred during upload
|
|
9
9
|
*/
|
|
10
|
-
export type ErrorHandler
|
|
10
|
+
export type { ErrorHandler };
|
|
11
11
|
/**
|
|
12
12
|
* Upload completion callback function type
|
|
13
13
|
* @param data Response data returned from the server
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@ticatec/restful_service_api",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"description": "A lightweight TypeScript RESTful API client for browsers with error handling. (ESM only, v0.6.0+)",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|