kmod-cli 1.0.11 → 1.1.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.
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useState,
|
|
5
|
+
} from 'react';
|
|
6
|
+
|
|
7
|
+
import axios, {
|
|
8
|
+
AxiosError,
|
|
9
|
+
AxiosInstance,
|
|
10
|
+
AxiosRequestConfig,
|
|
11
|
+
} from 'axios';
|
|
12
|
+
|
|
13
|
+
// ============ TYPES ============
|
|
14
|
+
|
|
15
|
+
export interface Pagination {
|
|
16
|
+
current?: number;
|
|
17
|
+
pageSize?: number;
|
|
18
|
+
mode?: 'server' | 'client';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Sorter {
|
|
22
|
+
field: string;
|
|
23
|
+
order: 'asc' | 'desc';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type FilterOperator =
|
|
27
|
+
| 'eq'
|
|
28
|
+
| 'ne'
|
|
29
|
+
| 'lt'
|
|
30
|
+
| 'lte'
|
|
31
|
+
| 'gt'
|
|
32
|
+
| 'gte'
|
|
33
|
+
| 'in'
|
|
34
|
+
| 'contains';
|
|
35
|
+
|
|
36
|
+
export interface Filter {
|
|
37
|
+
field: string;
|
|
38
|
+
operator: FilterOperator;
|
|
39
|
+
value: any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface GetListParams {
|
|
43
|
+
pagination?: Pagination;
|
|
44
|
+
sorters?: Sorter[];
|
|
45
|
+
filters?: Filter[];
|
|
46
|
+
meta?: AxiosRequestConfig;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GetOneParams {
|
|
50
|
+
id: string | number;
|
|
51
|
+
meta?: AxiosRequestConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface GetManyParams {
|
|
55
|
+
ids: (string | number)[];
|
|
56
|
+
meta?: AxiosRequestConfig;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CreateParams<T = any> {
|
|
60
|
+
variables: T;
|
|
61
|
+
meta?: AxiosRequestConfig;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CreateManyParams<T = any> {
|
|
65
|
+
variables: T[];
|
|
66
|
+
meta?: AxiosRequestConfig;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface UpdateParams<T = any> {
|
|
70
|
+
id: string | number;
|
|
71
|
+
variables: Partial<T>;
|
|
72
|
+
meta?: AxiosRequestConfig;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface UpdateManyParams<T = any> {
|
|
76
|
+
ids: (string | number)[];
|
|
77
|
+
variables: Partial<T>;
|
|
78
|
+
meta?: AxiosRequestConfig;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface DeleteOneParams {
|
|
82
|
+
id: string | number;
|
|
83
|
+
meta?: AxiosRequestConfig;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface DeleteManyParams {
|
|
87
|
+
ids: (string | number)[];
|
|
88
|
+
meta?: AxiosRequestConfig;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface CustomParams {
|
|
92
|
+
url: string;
|
|
93
|
+
method?: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
94
|
+
payload?: any;
|
|
95
|
+
query?: Record<string, any>;
|
|
96
|
+
headers?: Record<string, string>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface GetListResponse<T = any> {
|
|
100
|
+
data: T[];
|
|
101
|
+
total: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface GetOneResponse<T = any> {
|
|
105
|
+
data: T;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface GetManyResponse<T = any> {
|
|
109
|
+
data: T[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface CreateResponse<T = any> {
|
|
113
|
+
data: T;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface UpdateResponse<T = any> {
|
|
117
|
+
data: T;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface DeleteResponse<T = any> {
|
|
121
|
+
data: T;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface CustomResponse<T = any> {
|
|
125
|
+
data: T;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface DataProviderError {
|
|
129
|
+
message: string;
|
|
130
|
+
statusCode: number;
|
|
131
|
+
errors?: any;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface DataProviderOptions {
|
|
135
|
+
cacheTime?: number;
|
|
136
|
+
retryCount?: number;
|
|
137
|
+
retryDelay?: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface CacheItem<T = any> {
|
|
141
|
+
data: T;
|
|
142
|
+
timestamp: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface UseListOptions {
|
|
146
|
+
refetchInterval?: number;
|
|
147
|
+
enabled?: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface UseOneOptions {
|
|
151
|
+
enabled?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface UseMutationOptions<TData = any> {
|
|
155
|
+
onSuccess?: (data: TData) => void;
|
|
156
|
+
onError?: (error: DataProviderError) => void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============ DATA PROVIDER CLASS ============
|
|
160
|
+
|
|
161
|
+
class DataProvider {
|
|
162
|
+
private apiUrl: string;
|
|
163
|
+
private httpClient: AxiosInstance;
|
|
164
|
+
private cache: Map<string, CacheItem>;
|
|
165
|
+
private options: Required<DataProviderOptions>;
|
|
166
|
+
|
|
167
|
+
constructor(
|
|
168
|
+
apiUrl: string,
|
|
169
|
+
httpClient: AxiosInstance = axios,
|
|
170
|
+
options: DataProviderOptions = {}
|
|
171
|
+
) {
|
|
172
|
+
this.apiUrl = apiUrl;
|
|
173
|
+
this.httpClient = httpClient;
|
|
174
|
+
this.cache = new Map();
|
|
175
|
+
this.options = {
|
|
176
|
+
cacheTime: 5 * 60 * 1000,
|
|
177
|
+
retryCount: 3,
|
|
178
|
+
retryDelay: 1000,
|
|
179
|
+
...options
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Cache helpers
|
|
184
|
+
private getCacheKey(resource: string, params?: any): string {
|
|
185
|
+
return `${resource}:${JSON.stringify(params || {})}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private getCache<T = any>(key: string): T | null {
|
|
189
|
+
const cached = this.cache.get(key);
|
|
190
|
+
if (!cached) return null;
|
|
191
|
+
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
if (now - cached.timestamp > this.options.cacheTime) {
|
|
194
|
+
this.cache.delete(key);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return cached.data as T;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private setCache<T = any>(key: string, data: T): void {
|
|
202
|
+
this.cache.set(key, {
|
|
203
|
+
data,
|
|
204
|
+
timestamp: Date.now()
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public invalidateCache(resource: string): void {
|
|
209
|
+
const keys = Array.from(this.cache.keys());
|
|
210
|
+
keys.forEach(key => {
|
|
211
|
+
if (key.startsWith(`${resource}:`)) {
|
|
212
|
+
this.cache.delete(key);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public clearAllCache(): void {
|
|
218
|
+
this.cache.clear();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Retry logic
|
|
222
|
+
private async retryRequest<T>(
|
|
223
|
+
fn: () => Promise<T>,
|
|
224
|
+
retries: number = this.options.retryCount
|
|
225
|
+
): Promise<T> {
|
|
226
|
+
try {
|
|
227
|
+
return await fn();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (retries <= 0) throw error;
|
|
230
|
+
|
|
231
|
+
// Không retry với lỗi 4xx (client errors)
|
|
232
|
+
const axiosError = error as AxiosError;
|
|
233
|
+
if (
|
|
234
|
+
axiosError.response &&
|
|
235
|
+
axiosError.response.status >= 400 &&
|
|
236
|
+
axiosError.response.status < 500
|
|
237
|
+
) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await new Promise(resolve =>
|
|
242
|
+
setTimeout(resolve, this.options.retryDelay)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return this.retryRequest(fn, retries - 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// CRUD Methods
|
|
250
|
+
async getList<T = any>(
|
|
251
|
+
resource: string,
|
|
252
|
+
params: GetListParams = {},
|
|
253
|
+
useCache: boolean = true
|
|
254
|
+
): Promise<GetListResponse<T>> {
|
|
255
|
+
const cacheKey = this.getCacheKey(resource, params);
|
|
256
|
+
|
|
257
|
+
if (useCache) {
|
|
258
|
+
const cached = this.getCache<GetListResponse<T>>(cacheKey);
|
|
259
|
+
if (cached) return cached;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const { pagination, filters, sorters, meta } = params;
|
|
263
|
+
const url = `${this.apiUrl}/${resource}`;
|
|
264
|
+
const query: Record<string, any> = {};
|
|
265
|
+
|
|
266
|
+
if (pagination) {
|
|
267
|
+
const { current = 1, pageSize = 10 } = pagination;
|
|
268
|
+
query._start = (current - 1) * pageSize;
|
|
269
|
+
query._limit = pageSize;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (sorters && sorters.length > 0) {
|
|
273
|
+
query._sort = sorters.map(s => s.field).join(',');
|
|
274
|
+
query._order = sorters.map(s => s.order).join(',');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (filters && filters.length > 0) {
|
|
278
|
+
filters.forEach(filter => {
|
|
279
|
+
const { field, operator, value } = filter;
|
|
280
|
+
switch (operator) {
|
|
281
|
+
case 'eq':
|
|
282
|
+
query[field] = value;
|
|
283
|
+
break;
|
|
284
|
+
case 'ne':
|
|
285
|
+
query[`${field}_ne`] = value;
|
|
286
|
+
break;
|
|
287
|
+
case 'lt':
|
|
288
|
+
query[`${field}_lt`] = value;
|
|
289
|
+
break;
|
|
290
|
+
case 'lte':
|
|
291
|
+
query[`${field}_lte`] = value;
|
|
292
|
+
break;
|
|
293
|
+
case 'gt':
|
|
294
|
+
query[`${field}_gt`] = value;
|
|
295
|
+
break;
|
|
296
|
+
case 'gte':
|
|
297
|
+
query[`${field}_gte`] = value;
|
|
298
|
+
break;
|
|
299
|
+
case 'in':
|
|
300
|
+
query[`${field}_in`] = Array.isArray(value) ? value.join(',') : value;
|
|
301
|
+
break;
|
|
302
|
+
case 'contains':
|
|
303
|
+
query[`${field}_like`] = value;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const result = await this.retryRequest(async () => {
|
|
311
|
+
const response = await this.httpClient.get<T[] | { data: T[], total: number }>(
|
|
312
|
+
url,
|
|
313
|
+
{
|
|
314
|
+
params: query,
|
|
315
|
+
...meta
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const isArrayResponse = Array.isArray(response.data);
|
|
320
|
+
const data = isArrayResponse ? response.data : (response.data as any).data || [];
|
|
321
|
+
|
|
322
|
+
const total = response.headers['x-total-count']
|
|
323
|
+
? parseInt(response.headers['x-total-count'] as string)
|
|
324
|
+
: (isArrayResponse ? data.length : (response.data as any).total || 0);
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
data: Array.isArray(data) ? data : [],
|
|
328
|
+
total
|
|
329
|
+
};
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (useCache) {
|
|
333
|
+
this.setCache(cacheKey, result);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return result;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
throw this.handleError(error);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async getOne<T = any>(
|
|
343
|
+
resource: string,
|
|
344
|
+
params: GetOneParams,
|
|
345
|
+
useCache: boolean = true
|
|
346
|
+
): Promise<GetOneResponse<T>> {
|
|
347
|
+
const { id, meta } = params;
|
|
348
|
+
const cacheKey = this.getCacheKey(`${resource}/${id}`, {});
|
|
349
|
+
|
|
350
|
+
if (useCache) {
|
|
351
|
+
const cached = this.getCache<GetOneResponse<T>>(cacheKey);
|
|
352
|
+
if (cached) return cached;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const url = `${this.apiUrl}/${resource}/${id}`;
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const result = await this.retryRequest(async () => {
|
|
359
|
+
const response = await this.httpClient.get<T>(url, meta);
|
|
360
|
+
return { data: response.data };
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
if (useCache) {
|
|
364
|
+
this.setCache(cacheKey, result);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return result;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
throw this.handleError(error);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async getMany<T = any>(
|
|
374
|
+
resource: string,
|
|
375
|
+
params: GetManyParams,
|
|
376
|
+
useCache: boolean = true
|
|
377
|
+
): Promise<GetManyResponse<T>> {
|
|
378
|
+
const { ids, meta } = params;
|
|
379
|
+
const url = `${this.apiUrl}/${resource}`;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const result = await this.retryRequest(async () => {
|
|
383
|
+
const response = await this.httpClient.get<T[]>(url, {
|
|
384
|
+
params: { id: ids },
|
|
385
|
+
...meta
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
data: Array.isArray(response.data) ? response.data : []
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
return result;
|
|
393
|
+
} catch (error) {
|
|
394
|
+
throw this.handleError(error);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async create<T = any, V = any>(
|
|
399
|
+
resource: string,
|
|
400
|
+
params: CreateParams<V>
|
|
401
|
+
): Promise<CreateResponse<T>> {
|
|
402
|
+
const { variables, meta } = params;
|
|
403
|
+
const url = `${this.apiUrl}/${resource}`;
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const result = await this.retryRequest(async () => {
|
|
407
|
+
const response = await this.httpClient.post<T>(url, variables, meta);
|
|
408
|
+
return { data: response.data };
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
this.invalidateCache(resource);
|
|
412
|
+
return result;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
throw this.handleError(error);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async createMany<T = any, V = any>(
|
|
419
|
+
resource: string,
|
|
420
|
+
params: CreateManyParams<V>
|
|
421
|
+
): Promise<GetManyResponse<T>> {
|
|
422
|
+
const { variables, meta } = params;
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const result = await this.retryRequest(async () => {
|
|
426
|
+
const responses = await Promise.all(
|
|
427
|
+
variables.map(variable =>
|
|
428
|
+
this.httpClient.post<T>(`${this.apiUrl}/${resource}`, variable, meta)
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
return { data: responses.map(r => r.data) };
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
this.invalidateCache(resource);
|
|
435
|
+
return result;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
throw this.handleError(error);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async update<T = any, V = any>(
|
|
442
|
+
resource: string,
|
|
443
|
+
params: UpdateParams<V>
|
|
444
|
+
): Promise<UpdateResponse<T>> {
|
|
445
|
+
const { id, variables, meta } = params;
|
|
446
|
+
const url = `${this.apiUrl}/${resource}/${id}`;
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const result = await this.retryRequest(async () => {
|
|
450
|
+
const response = await this.httpClient.patch<T>(url, variables, meta);
|
|
451
|
+
return { data: response.data };
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
this.invalidateCache(resource);
|
|
455
|
+
return result;
|
|
456
|
+
} catch (error) {
|
|
457
|
+
throw this.handleError(error);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async updateMany<T = any, V = any>(
|
|
462
|
+
resource: string,
|
|
463
|
+
params: UpdateManyParams<V>
|
|
464
|
+
): Promise<GetManyResponse<T>> {
|
|
465
|
+
const { ids, variables, meta } = params;
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const result = await this.retryRequest(async () => {
|
|
469
|
+
const responses = await Promise.all(
|
|
470
|
+
ids.map(id =>
|
|
471
|
+
this.httpClient.patch<T>(
|
|
472
|
+
`${this.apiUrl}/${resource}/${id}`,
|
|
473
|
+
variables,
|
|
474
|
+
meta
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
return { data: responses.map(r => r.data) };
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
this.invalidateCache(resource);
|
|
482
|
+
return result;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
throw this.handleError(error);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async deleteOne<T = any>(
|
|
489
|
+
resource: string,
|
|
490
|
+
params: DeleteOneParams
|
|
491
|
+
): Promise<DeleteResponse<T>> {
|
|
492
|
+
const { id, meta } = params;
|
|
493
|
+
const url = `${this.apiUrl}/${resource}/${id}`;
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
const result = await this.retryRequest(async () => {
|
|
497
|
+
const response = await this.httpClient.delete<T>(url, meta);
|
|
498
|
+
return { data: response.data };
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
this.invalidateCache(resource);
|
|
502
|
+
return result;
|
|
503
|
+
} catch (error) {
|
|
504
|
+
throw this.handleError(error);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async deleteMany<T = any>(
|
|
509
|
+
resource: string,
|
|
510
|
+
params: DeleteManyParams
|
|
511
|
+
): Promise<GetManyResponse<T>> {
|
|
512
|
+
const { ids, meta } = params;
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
const result = await this.retryRequest(async () => {
|
|
516
|
+
const responses = await Promise.all(
|
|
517
|
+
ids.map(id =>
|
|
518
|
+
this.httpClient.delete<T>(`${this.apiUrl}/${resource}/${id}`, meta)
|
|
519
|
+
)
|
|
520
|
+
);
|
|
521
|
+
return { data: responses.map(r => r.data) };
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
this.invalidateCache(resource);
|
|
525
|
+
return result;
|
|
526
|
+
} catch (error) {
|
|
527
|
+
throw this.handleError(error);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async custom<T = any>(params: CustomParams): Promise<CustomResponse<T>> {
|
|
532
|
+
const { url, method = 'get', payload, query, headers } = params;
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
return await this.retryRequest(async () => {
|
|
536
|
+
const response = await this.httpClient<T>({
|
|
537
|
+
url: `${this.apiUrl}${url}`,
|
|
538
|
+
method,
|
|
539
|
+
data: payload,
|
|
540
|
+
params: query,
|
|
541
|
+
headers,
|
|
542
|
+
});
|
|
543
|
+
return { data: response.data };
|
|
544
|
+
});
|
|
545
|
+
} catch (error) {
|
|
546
|
+
throw this.handleError(error);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private handleError(error: unknown): DataProviderError {
|
|
551
|
+
const axiosError = error as AxiosError<any>;
|
|
552
|
+
|
|
553
|
+
if (axiosError.response) {
|
|
554
|
+
return {
|
|
555
|
+
message: axiosError.response.data?.message || axiosError.message || 'Request failed',
|
|
556
|
+
statusCode: axiosError.response.status,
|
|
557
|
+
errors: axiosError.response.data?.errors,
|
|
558
|
+
};
|
|
559
|
+
} else if (axiosError.request) {
|
|
560
|
+
return {
|
|
561
|
+
message: 'Network error',
|
|
562
|
+
statusCode: 0,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
message: (error as Error).message || 'Unknown error',
|
|
568
|
+
statusCode: 0,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ============ REACT HOOKS ============
|
|
574
|
+
|
|
575
|
+
export function useList<T = any>(
|
|
576
|
+
dataProvider: DataProvider,
|
|
577
|
+
resource: string,
|
|
578
|
+
params: GetListParams = {},
|
|
579
|
+
options: UseListOptions = {}
|
|
580
|
+
) {
|
|
581
|
+
const [data, setData] = useState<T[]>([]);
|
|
582
|
+
const [total, setTotal] = useState<number>(0);
|
|
583
|
+
const [loading, setLoading] = useState<boolean>(true);
|
|
584
|
+
const [error, setError] = useState<DataProviderError | null>(null);
|
|
585
|
+
|
|
586
|
+
const { refetchInterval, enabled = true } = options;
|
|
587
|
+
const paramsStr = JSON.stringify(params);
|
|
588
|
+
|
|
589
|
+
const refetch = useCallback(async () => {
|
|
590
|
+
if (!enabled) return;
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
setLoading(true);
|
|
594
|
+
setError(null);
|
|
595
|
+
const result = await dataProvider.getList<T>(resource, JSON.parse(paramsStr));
|
|
596
|
+
setData(result.data || []);
|
|
597
|
+
setTotal(result.total || 0);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
setError(err as DataProviderError);
|
|
600
|
+
setData([]);
|
|
601
|
+
setTotal(0);
|
|
602
|
+
} finally {
|
|
603
|
+
setLoading(false);
|
|
604
|
+
}
|
|
605
|
+
}, [dataProvider, resource, paramsStr, enabled]);
|
|
606
|
+
|
|
607
|
+
useEffect(() => {
|
|
608
|
+
refetch();
|
|
609
|
+
}, [refetch]);
|
|
610
|
+
|
|
611
|
+
useEffect(() => {
|
|
612
|
+
if (refetchInterval && enabled) {
|
|
613
|
+
const interval = setInterval(refetch, refetchInterval);
|
|
614
|
+
return () => clearInterval(interval);
|
|
615
|
+
}
|
|
616
|
+
}, [refetchInterval, refetch, enabled]);
|
|
617
|
+
|
|
618
|
+
return { data, total, loading, error, refetch };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function useOne<T = any>(
|
|
622
|
+
dataProvider: DataProvider,
|
|
623
|
+
resource: string,
|
|
624
|
+
id: string | number | null | undefined,
|
|
625
|
+
options: UseOneOptions = {}
|
|
626
|
+
) {
|
|
627
|
+
const [data, setData] = useState<T | null>(null);
|
|
628
|
+
const [loading, setLoading] = useState<boolean>(true);
|
|
629
|
+
const [error, setError] = useState<DataProviderError | null>(null);
|
|
630
|
+
|
|
631
|
+
const { enabled = true } = options;
|
|
632
|
+
|
|
633
|
+
const refetch = useCallback(async () => {
|
|
634
|
+
if (!enabled || !id) {
|
|
635
|
+
setLoading(false);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
setLoading(true);
|
|
641
|
+
setError(null);
|
|
642
|
+
const result = await dataProvider.getOne<T>(resource, { id });
|
|
643
|
+
setData(result.data);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
setError(err as DataProviderError);
|
|
646
|
+
setData(null);
|
|
647
|
+
} finally {
|
|
648
|
+
setLoading(false);
|
|
649
|
+
}
|
|
650
|
+
}, [dataProvider, resource, id, enabled]);
|
|
651
|
+
|
|
652
|
+
useEffect(() => {
|
|
653
|
+
refetch();
|
|
654
|
+
}, [refetch]);
|
|
655
|
+
|
|
656
|
+
return { data, loading, error, refetch };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export function useCreate<T = any, V = any>(
|
|
660
|
+
dataProvider: DataProvider,
|
|
661
|
+
resource: string,
|
|
662
|
+
options: UseMutationOptions<T> = {}
|
|
663
|
+
) {
|
|
664
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
665
|
+
const [error, setError] = useState<DataProviderError | null>(null);
|
|
666
|
+
|
|
667
|
+
const { onSuccess, onError } = options;
|
|
668
|
+
|
|
669
|
+
const mutate = useCallback(async (variables: V): Promise<T> => {
|
|
670
|
+
setLoading(true);
|
|
671
|
+
setError(null);
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
const result = await dataProvider.create<T, V>(resource, { variables });
|
|
675
|
+
if (onSuccess) {
|
|
676
|
+
onSuccess(result.data);
|
|
677
|
+
}
|
|
678
|
+
return result.data;
|
|
679
|
+
} catch (err) {
|
|
680
|
+
const errorObj = err as DataProviderError;
|
|
681
|
+
setError(errorObj);
|
|
682
|
+
if (onError) {
|
|
683
|
+
onError(errorObj);
|
|
684
|
+
}
|
|
685
|
+
throw errorObj;
|
|
686
|
+
} finally {
|
|
687
|
+
setLoading(false);
|
|
688
|
+
}
|
|
689
|
+
}, [dataProvider, resource, onSuccess, onError]);
|
|
690
|
+
|
|
691
|
+
return { mutate, loading, error };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export function useUpdate<T = any, V = any>(
|
|
695
|
+
dataProvider: DataProvider,
|
|
696
|
+
resource: string,
|
|
697
|
+
options: UseMutationOptions<T> = {}
|
|
698
|
+
) {
|
|
699
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
700
|
+
const [error, setError] = useState<DataProviderError | null>(null);
|
|
701
|
+
|
|
702
|
+
const { onSuccess, onError } = options;
|
|
703
|
+
|
|
704
|
+
const mutate = useCallback(
|
|
705
|
+
async (id: string | number, variables: Partial<V>): Promise<T> => {
|
|
706
|
+
setLoading(true);
|
|
707
|
+
setError(null);
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
const result = await dataProvider.update<T, V>(resource, { id, variables });
|
|
711
|
+
if (onSuccess) {
|
|
712
|
+
onSuccess(result.data);
|
|
713
|
+
}
|
|
714
|
+
return result.data;
|
|
715
|
+
} catch (err) {
|
|
716
|
+
const errorObj = err as DataProviderError;
|
|
717
|
+
setError(errorObj);
|
|
718
|
+
if (onError) {
|
|
719
|
+
onError(errorObj);
|
|
720
|
+
}
|
|
721
|
+
throw errorObj;
|
|
722
|
+
} finally {
|
|
723
|
+
setLoading(false);
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
[dataProvider, resource, onSuccess, onError]
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
return { mutate, loading, error };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function useDelete<T = any>(
|
|
733
|
+
dataProvider: DataProvider,
|
|
734
|
+
resource: string,
|
|
735
|
+
options: UseMutationOptions<T> = {}
|
|
736
|
+
) {
|
|
737
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
738
|
+
const [error, setError] = useState<DataProviderError | null>(null);
|
|
739
|
+
|
|
740
|
+
const { onSuccess, onError } = options;
|
|
741
|
+
|
|
742
|
+
const mutate = useCallback(
|
|
743
|
+
async (id: string | number): Promise<T> => {
|
|
744
|
+
setLoading(true);
|
|
745
|
+
setError(null);
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
const result = await dataProvider.deleteOne<T>(resource, { id });
|
|
749
|
+
if (onSuccess) {
|
|
750
|
+
onSuccess(result.data);
|
|
751
|
+
}
|
|
752
|
+
return result.data;
|
|
753
|
+
} catch (err) {
|
|
754
|
+
const errorObj = err as DataProviderError;
|
|
755
|
+
setError(errorObj);
|
|
756
|
+
if (onError) {
|
|
757
|
+
onError(errorObj);
|
|
758
|
+
}
|
|
759
|
+
throw errorObj;
|
|
760
|
+
} finally {
|
|
761
|
+
setLoading(false);
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
[dataProvider, resource, onSuccess, onError]
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
return { mutate, loading, error };
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export default DataProvider;
|
|
771
|
+
|
|
772
|
+
/* ============ VÍ DỤ SỬ DỤNG ============
|
|
773
|
+
|
|
774
|
+
// 1. Setup DataProvider
|
|
775
|
+
import DataProvider, { useList, useOne, useCreate, useUpdate, useDelete } from './DataProvider';
|
|
776
|
+
import axios from 'axios';
|
|
777
|
+
|
|
778
|
+
interface User {
|
|
779
|
+
id: number;
|
|
780
|
+
name: string;
|
|
781
|
+
email: string;
|
|
782
|
+
status: 'active' | 'inactive';
|
|
783
|
+
createdAt: string;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
interface CreateUserInput {
|
|
787
|
+
name: string;
|
|
788
|
+
email: string;
|
|
789
|
+
status?: 'active' | 'inactive';
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const axiosInstance = axios.create({
|
|
793
|
+
baseURL: 'https://api.example.com',
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
axiosInstance.interceptors.request.use(config => {
|
|
797
|
+
const token = localStorage.getItem('token');
|
|
798
|
+
if (token) {
|
|
799
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
800
|
+
}
|
|
801
|
+
return config;
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
const dataProvider = new DataProvider('', axiosInstance, {
|
|
805
|
+
cacheTime: 5 * 60 * 1000,
|
|
806
|
+
retryCount: 3,
|
|
807
|
+
retryDelay: 1000
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// 2. Sử dụng trong component
|
|
811
|
+
function UsersList() {
|
|
812
|
+
const [page, setPage] = useState(1);
|
|
813
|
+
|
|
814
|
+
const { data, total, loading, error, refetch } = useList<User>(
|
|
815
|
+
dataProvider,
|
|
816
|
+
'users',
|
|
817
|
+
{
|
|
818
|
+
pagination: { current: page, pageSize: 10 },
|
|
819
|
+
sorters: [{ field: 'createdAt', order: 'desc' }],
|
|
820
|
+
filters: [{ field: 'status', operator: 'eq', value: 'active' }]
|
|
821
|
+
},
|
|
822
|
+
{ refetchInterval: 30000 }
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
const { mutate: createUser, loading: creating } = useCreate<User, CreateUserInput>(
|
|
826
|
+
dataProvider,
|
|
827
|
+
'users',
|
|
828
|
+
{
|
|
829
|
+
onSuccess: (data) => {
|
|
830
|
+
console.log('Created:', data);
|
|
831
|
+
refetch();
|
|
832
|
+
},
|
|
833
|
+
onError: (err) => {
|
|
834
|
+
console.error('Error:', err);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
const { mutate: updateUser } = useUpdate<User, Partial<User>>(
|
|
840
|
+
dataProvider,
|
|
841
|
+
'users',
|
|
842
|
+
{
|
|
843
|
+
onSuccess: () => refetch()
|
|
844
|
+
}
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
const { mutate: deleteUser } = useDelete<User>(
|
|
848
|
+
dataProvider,
|
|
849
|
+
'users',
|
|
850
|
+
{
|
|
851
|
+
onSuccess: () => refetch()
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
const handleCreate = async () => {
|
|
856
|
+
try {
|
|
857
|
+
await createUser({
|
|
858
|
+
name: 'John Doe',
|
|
859
|
+
email: 'john@example.com',
|
|
860
|
+
status: 'active'
|
|
861
|
+
});
|
|
862
|
+
} catch (err) {
|
|
863
|
+
// Error handled
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
const handleUpdate = async (id: number) => {
|
|
868
|
+
try {
|
|
869
|
+
await updateUser(id, { name: 'Jane Doe' });
|
|
870
|
+
} catch (err) {
|
|
871
|
+
// Error handled
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
const handleDelete = async (id: number) => {
|
|
876
|
+
try {
|
|
877
|
+
await deleteUser(id);
|
|
878
|
+
} catch (err) {
|
|
879
|
+
// Error handled
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
if (loading) return <div>Loading...</div>;
|
|
884
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
885
|
+
|
|
886
|
+
return (
|
|
887
|
+
<div>
|
|
888
|
+
<button onClick={handleCreate} disabled={creating}>
|
|
889
|
+
Create User
|
|
890
|
+
</button>
|
|
891
|
+
|
|
892
|
+
{data.map(user => (
|
|
893
|
+
<div key={user.id}>
|
|
894
|
+
<span>{user.name}</span>
|
|
895
|
+
<button onClick={() => handleUpdate(user.id)}>Edit</button>
|
|
896
|
+
<button onClick={() => handleDelete(user.id)}>Delete</button>
|
|
897
|
+
</div>
|
|
898
|
+
))}
|
|
899
|
+
|
|
900
|
+
<button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
|
|
901
|
+
Previous
|
|
902
|
+
</button>
|
|
903
|
+
<button onClick={() => setPage(p => p + 1)}>Next</button>
|
|
904
|
+
|
|
905
|
+
<div>Total: {total}</div>
|
|
906
|
+
</div>
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// 3. Chi tiết user
|
|
911
|
+
function UserDetail({ userId }: { userId: number }) {
|
|
912
|
+
const { data, loading, error, refetch } = useOne<User>(
|
|
913
|
+
dataProvider,
|
|
914
|
+
'users',
|
|
915
|
+
userId
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
if (loading) return <div>Loading...</div>;
|
|
919
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
920
|
+
if (!data) return <div>No data</div>;
|
|
921
|
+
|
|
922
|
+
return (
|
|
923
|
+
<div>
|
|
924
|
+
<h1>{data.name}</h1>
|
|
925
|
+
<p>{data.email}</p>
|
|
926
|
+
<button onClick={refetch}>Refresh</button>
|
|
927
|
+
</div>
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
*/
|