@web-ts-toolkit/access-router-react 0.5.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/index.mjs ADDED
@@ -0,0 +1,583 @@
1
+ // src/create-model-hook.ts
2
+ import { useState, useEffect as useEffect2, useRef as useRef2, useCallback as useCallback2 } from "react";
3
+
4
+ // src/fetch.ts
5
+ import { useEffect, useRef, useCallback } from "react";
6
+ function isAbortError(err) {
7
+ return err instanceof DOMException && err.name === "AbortError";
8
+ }
9
+ function useAbortManager() {
10
+ const ref = useRef(null);
11
+ const replace = useCallback((next) => {
12
+ ref.current?.abort();
13
+ ref.current = next;
14
+ }, []);
15
+ useEffect(() => {
16
+ return () => {
17
+ ref.current?.abort();
18
+ ref.current = null;
19
+ };
20
+ }, []);
21
+ return { replace };
22
+ }
23
+ function stableStringify(value) {
24
+ if (value == null) return "";
25
+ if (typeof value !== "object") return JSON.stringify(value);
26
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
27
+ const keys = Object.keys(value).sort();
28
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
29
+ }
30
+ function useMountRef() {
31
+ const ref = useRef(true);
32
+ useEffect(() => {
33
+ ref.current = true;
34
+ return () => {
35
+ ref.current = false;
36
+ };
37
+ }, []);
38
+ return ref;
39
+ }
40
+
41
+ // src/create-model-hook.ts
42
+ function useAutoQuery({
43
+ doFetch,
44
+ applyResult,
45
+ shouldFetch,
46
+ deps,
47
+ onSuccess,
48
+ onError,
49
+ onSettled
50
+ }) {
51
+ const [isLoading, setIsLoading] = useState(false);
52
+ const [isFetching, setIsFetching] = useState(false);
53
+ const [error, setError] = useState(null);
54
+ const manager = useAbortManager();
55
+ const mountRef = useMountRef();
56
+ const fetchAndSet = useCallback2(
57
+ async (signal) => {
58
+ setIsFetching(true);
59
+ setError(null);
60
+ try {
61
+ return await doFetch(signal);
62
+ } catch (err) {
63
+ if (!isAbortError(err)) setError(err);
64
+ throw err;
65
+ } finally {
66
+ if (!signal?.aborted) setIsFetching(false);
67
+ }
68
+ },
69
+ [doFetch]
70
+ );
71
+ const doFetchWithCallbacks = useCallback2(
72
+ async (signal) => {
73
+ const res = await fetchAndSet(signal);
74
+ if (!signal?.aborted) {
75
+ applyResult(res);
76
+ onSuccess?.(res);
77
+ onSettled?.(res, null);
78
+ }
79
+ return res;
80
+ },
81
+ [fetchAndSet, applyResult, onSuccess, onSettled]
82
+ );
83
+ useEffect2(() => {
84
+ if (!shouldFetch) return;
85
+ mountRef.current = true;
86
+ setIsLoading(true);
87
+ const controller = new AbortController();
88
+ manager.replace(controller);
89
+ doFetchWithCallbacks(controller.signal).catch((err) => {
90
+ if (!mountRef.current || controller.signal.aborted) return;
91
+ onError?.(err);
92
+ onSettled?.(null, err);
93
+ }).finally(() => {
94
+ if (mountRef.current && !controller.signal.aborted) setIsLoading(false);
95
+ });
96
+ return () => {
97
+ mountRef.current = false;
98
+ controller.abort();
99
+ };
100
+ }, deps);
101
+ const refetch = useCallback2(() => {
102
+ setIsLoading(true);
103
+ const controller = new AbortController();
104
+ manager.replace(controller);
105
+ doFetchWithCallbacks(controller.signal).catch(() => {
106
+ }).finally(() => {
107
+ if (!controller.signal.aborted) setIsLoading(false);
108
+ });
109
+ }, [doFetchWithCallbacks, manager]);
110
+ return { isLoading, isFetching, error, setError, refetch, manager, mountRef };
111
+ }
112
+ function useMutation(execute, options) {
113
+ const [isPending, setIsPending] = useState(false);
114
+ const [error, setError] = useState(null);
115
+ const mountRef = useMountRef();
116
+ const { onSuccess, onSettled } = options ?? {};
117
+ const executeMutate = useCallback2(
118
+ async (...args) => {
119
+ setIsPending(true);
120
+ setError(null);
121
+ try {
122
+ const result = await execute(...args);
123
+ if (mountRef.current) {
124
+ onSuccess?.(result);
125
+ onSettled?.(result, null);
126
+ }
127
+ return result;
128
+ } catch (err) {
129
+ const svcErr = err;
130
+ if (mountRef.current) {
131
+ setError(svcErr);
132
+ onSettled?.(null, svcErr);
133
+ }
134
+ throw svcErr;
135
+ } finally {
136
+ if (mountRef.current) setIsPending(false);
137
+ }
138
+ },
139
+ [execute, mountRef, onSuccess, onSettled]
140
+ );
141
+ const reset = useCallback2(() => setError(null), []);
142
+ return { isPending, error, executeMutate, reset };
143
+ }
144
+ function createModelHooks(config) {
145
+ const { modelService } = config;
146
+ function useRead(options = {}) {
147
+ const {
148
+ id,
149
+ advanced,
150
+ select,
151
+ populate,
152
+ sort,
153
+ include,
154
+ tasks,
155
+ basicOptions,
156
+ advancedOptions,
157
+ enabled = true,
158
+ initialData = null,
159
+ requestConfig,
160
+ onSuccess,
161
+ onError,
162
+ onSettled
163
+ } = options;
164
+ const [data, setData] = useState(initialData);
165
+ const applyResult = useCallback2((res) => {
166
+ setData(res.data);
167
+ }, []);
168
+ const doFetchById = useCallback2(
169
+ async (targetId, signal) => {
170
+ if (advanced) {
171
+ return await modelService.readAdvanced(
172
+ targetId,
173
+ { select, populate, sort, include, tasks },
174
+ advancedOptions,
175
+ { ...requestConfig, signal }
176
+ ).exec();
177
+ }
178
+ return await modelService.read(targetId, basicOptions, { ...requestConfig, signal }).exec();
179
+ },
180
+ [modelService, advanced, select, populate, sort, include, tasks, basicOptions, advancedOptions, requestConfig]
181
+ );
182
+ const doFetch = useCallback2(
183
+ (signal) => {
184
+ if (!id) throw new Error("useRead: id is required");
185
+ return doFetchById(id, signal);
186
+ },
187
+ [doFetchById, id]
188
+ );
189
+ const shouldFetch = Boolean(id && enabled);
190
+ const { isLoading, isFetching, error, refetch } = useAutoQuery({
191
+ doFetch,
192
+ applyResult,
193
+ shouldFetch,
194
+ deps: [id, enabled, advanced, select, populate, sort, include, tasks, basicOptions, advancedOptions],
195
+ onSuccess,
196
+ onError,
197
+ onSettled
198
+ });
199
+ const query = useCallback2(
200
+ async (readId) => {
201
+ const res = await doFetchById(readId);
202
+ applyResult(res);
203
+ onSuccess?.(res);
204
+ onSettled?.(res, null);
205
+ return res;
206
+ },
207
+ [doFetchById, applyResult, onSuccess, onSettled]
208
+ );
209
+ const reset = useCallback2(() => {
210
+ setData(initialData);
211
+ }, [initialData]);
212
+ return { data, isLoading, isFetching, error, query, refetch, reset };
213
+ }
214
+ function useList(options = {}) {
215
+ const {
216
+ listParams,
217
+ filter,
218
+ advanced,
219
+ sort,
220
+ select,
221
+ populate,
222
+ include,
223
+ tasks,
224
+ basicOptions,
225
+ advancedOptions,
226
+ enabled = true,
227
+ keepPreviousData = false,
228
+ initialData,
229
+ requestConfig,
230
+ onSuccess,
231
+ onError,
232
+ onSettled
233
+ } = options;
234
+ const [data, setData] = useState(initialData ?? []);
235
+ const [previousData, setPreviousData] = useState(void 0);
236
+ const [totalCount, setTotalCount] = useState(0);
237
+ const latestDataRef = useRef2(data);
238
+ latestDataRef.current = data;
239
+ const applyResult = useCallback2((res) => {
240
+ setData(res.data);
241
+ setTotalCount(res.totalCount);
242
+ setPreviousData(void 0);
243
+ }, []);
244
+ const baseFetch = useCallback2(
245
+ async (args, signal) => {
246
+ if (keepPreviousData) {
247
+ setPreviousData(latestDataRef.current);
248
+ }
249
+ if (advanced) {
250
+ return await modelService.listAdvanced(
251
+ filter ?? {},
252
+ { sort, select, populate, include, tasks, ...args },
253
+ advancedOptions,
254
+ { ...requestConfig, signal }
255
+ ).exec();
256
+ }
257
+ return await modelService.list(args ?? listParams, basicOptions, { ...requestConfig, signal }).exec();
258
+ },
259
+ [
260
+ modelService,
261
+ advanced,
262
+ filter,
263
+ sort,
264
+ select,
265
+ populate,
266
+ include,
267
+ tasks,
268
+ listParams,
269
+ basicOptions,
270
+ advancedOptions,
271
+ requestConfig,
272
+ keepPreviousData,
273
+ latestDataRef
274
+ ]
275
+ );
276
+ const doFetch = useCallback2((signal) => baseFetch(listParams, signal), [baseFetch, listParams]);
277
+ const listParamsKey = stableStringify(listParams);
278
+ const filterKey = stableStringify(filter);
279
+ const sortKey = stableStringify(sort);
280
+ const shouldFetch = Boolean((listParams || advanced) && enabled);
281
+ const { isLoading, isFetching, error, refetch } = useAutoQuery({
282
+ doFetch,
283
+ applyResult,
284
+ shouldFetch,
285
+ deps: [
286
+ listParamsKey,
287
+ filterKey,
288
+ advanced,
289
+ enabled,
290
+ select,
291
+ populate,
292
+ sortKey,
293
+ include,
294
+ tasks,
295
+ basicOptions,
296
+ advancedOptions
297
+ ],
298
+ onSuccess,
299
+ onError,
300
+ onSettled
301
+ });
302
+ const query = useCallback2(
303
+ async (args) => {
304
+ const res = await baseFetch(args);
305
+ applyResult(res);
306
+ onSuccess?.(res);
307
+ onSettled?.(res, null);
308
+ return res;
309
+ },
310
+ [baseFetch, applyResult, onSuccess, onSettled]
311
+ );
312
+ const reset = useCallback2(() => {
313
+ setData(initialData ?? []);
314
+ setPreviousData(void 0);
315
+ setTotalCount(0);
316
+ }, [initialData]);
317
+ return { data, previousData, totalCount, isLoading, isFetching, error, query, refetch, reset };
318
+ }
319
+ function useCreate(options = {}) {
320
+ const {
321
+ advanced,
322
+ select,
323
+ populate,
324
+ tasks,
325
+ basicOptions,
326
+ advancedOptions,
327
+ requestConfig,
328
+ onSuccess,
329
+ onError,
330
+ onSettled
331
+ } = options;
332
+ const [data, setData] = useState(null);
333
+ const mountRef = useMountRef();
334
+ const execute = useCallback2(
335
+ async (createData) => {
336
+ let res;
337
+ if (advanced) {
338
+ res = await modelService.createAdvanced(
339
+ createData,
340
+ { select, populate, tasks },
341
+ advancedOptions,
342
+ requestConfig
343
+ ).exec();
344
+ } else {
345
+ res = await modelService.create(createData, basicOptions, requestConfig).exec();
346
+ }
347
+ if (mountRef.current) setData(res.data);
348
+ return res;
349
+ },
350
+ [modelService, advanced, select, populate, tasks, basicOptions, advancedOptions, requestConfig, mountRef]
351
+ );
352
+ const { isPending, error, executeMutate, reset: resetError } = useMutation(execute, { onSuccess, onSettled });
353
+ const mutate = useCallback2(
354
+ async (createData) => {
355
+ try {
356
+ return await executeMutate(createData);
357
+ } catch (err) {
358
+ onError?.(err);
359
+ throw err;
360
+ }
361
+ },
362
+ [executeMutate, onError]
363
+ );
364
+ const reset = useCallback2(() => {
365
+ setData(null);
366
+ resetError();
367
+ }, [resetError]);
368
+ return { data, isPending, error, mutate, reset };
369
+ }
370
+ function useUpdate(options = {}) {
371
+ const {
372
+ advanced,
373
+ select,
374
+ populate,
375
+ tasks,
376
+ basicOptions,
377
+ advancedOptions,
378
+ requestConfig,
379
+ onSuccess,
380
+ onError,
381
+ onSettled
382
+ } = options;
383
+ const [data, setData] = useState(null);
384
+ const mountRef = useMountRef();
385
+ const execute = useCallback2(
386
+ async (updateId, updateData) => {
387
+ let res;
388
+ if (advanced) {
389
+ res = await modelService.updateAdvanced(
390
+ updateId,
391
+ updateData,
392
+ { select, populate, tasks },
393
+ advancedOptions,
394
+ requestConfig
395
+ ).exec();
396
+ } else {
397
+ res = await modelService.update(updateId, updateData, basicOptions, requestConfig).exec();
398
+ }
399
+ if (mountRef.current) setData(res.data);
400
+ return res;
401
+ },
402
+ [modelService, advanced, select, populate, tasks, basicOptions, advancedOptions, requestConfig, mountRef]
403
+ );
404
+ const { isPending, error, executeMutate, reset: resetError } = useMutation(execute, { onSuccess, onSettled });
405
+ const mutate = useCallback2(
406
+ async (updateId, updateData) => {
407
+ try {
408
+ return await executeMutate(updateId, updateData);
409
+ } catch (err) {
410
+ onError?.(err);
411
+ throw err;
412
+ }
413
+ },
414
+ [executeMutate, onError]
415
+ );
416
+ const reset = useCallback2(() => {
417
+ setData(null);
418
+ resetError();
419
+ }, [resetError]);
420
+ return { data, isPending, error, mutate, reset };
421
+ }
422
+ function useUpsert(options = {}) {
423
+ const {
424
+ advanced,
425
+ select,
426
+ populate,
427
+ tasks,
428
+ basicOptions,
429
+ advancedOptions,
430
+ requestConfig,
431
+ onSuccess,
432
+ onError,
433
+ onSettled
434
+ } = options;
435
+ const [data, setData] = useState(null);
436
+ const mountRef = useMountRef();
437
+ const execute = useCallback2(
438
+ async (upsertData) => {
439
+ let res;
440
+ if (advanced) {
441
+ res = await modelService.upsertAdvanced(
442
+ upsertData,
443
+ { select, populate, tasks },
444
+ advancedOptions,
445
+ requestConfig
446
+ ).exec();
447
+ } else {
448
+ res = await modelService.upsert(upsertData, basicOptions, requestConfig).exec();
449
+ }
450
+ if (mountRef.current) setData(res.data);
451
+ return res;
452
+ },
453
+ [modelService, advanced, select, populate, tasks, basicOptions, advancedOptions, requestConfig, mountRef]
454
+ );
455
+ const { isPending, error, executeMutate, reset: resetError } = useMutation(execute, { onSuccess, onSettled });
456
+ const mutate = useCallback2(
457
+ async (upsertData) => {
458
+ try {
459
+ return await executeMutate(upsertData);
460
+ } catch (err) {
461
+ onError?.(err);
462
+ throw err;
463
+ }
464
+ },
465
+ [executeMutate, onError]
466
+ );
467
+ const reset = useCallback2(() => {
468
+ setData(null);
469
+ resetError();
470
+ }, [resetError]);
471
+ return { data, isPending, error, mutate, reset };
472
+ }
473
+ function useDelete(options = {}) {
474
+ const { requestConfig, onSuccess, onError, onSettled } = options;
475
+ const execute = useCallback2(
476
+ async (deleteId) => {
477
+ const res = await modelService.delete(deleteId, requestConfig).exec();
478
+ return res;
479
+ },
480
+ [modelService, requestConfig]
481
+ );
482
+ const { isPending, error, executeMutate, reset } = useMutation(execute, { onSuccess, onSettled });
483
+ const mutate = useCallback2(
484
+ async (deleteId) => {
485
+ try {
486
+ return await executeMutate(deleteId);
487
+ } catch (err) {
488
+ onError?.(err);
489
+ throw err;
490
+ }
491
+ },
492
+ [executeMutate, onError]
493
+ );
494
+ return { isPending, error, mutate, reset };
495
+ }
496
+ function useCount(options = {}) {
497
+ const { advanced, filter, enabled = true, requestConfig, onSuccess, onError, onSettled } = options;
498
+ const [data, setData] = useState(null);
499
+ const applyResult = useCallback2((res) => {
500
+ setData(res.data);
501
+ }, []);
502
+ const doFetch = useCallback2(
503
+ async (signal) => {
504
+ if (advanced) {
505
+ return await modelService.countAdvanced(filter ?? {}, void 0, { ...requestConfig, signal }).exec();
506
+ }
507
+ return await modelService.count({ ...requestConfig, signal }).exec();
508
+ },
509
+ [modelService, advanced, filter, requestConfig]
510
+ );
511
+ const filterKey = stableStringify(filter);
512
+ const { isLoading, error, refetch } = useAutoQuery({
513
+ doFetch,
514
+ applyResult,
515
+ shouldFetch: enabled,
516
+ deps: [enabled, advanced, filterKey],
517
+ onSuccess,
518
+ onError,
519
+ onSettled
520
+ });
521
+ const query = useCallback2(async () => {
522
+ const res = await doFetch();
523
+ applyResult(res);
524
+ onSuccess?.(res);
525
+ onSettled?.(res, null);
526
+ return res;
527
+ }, [doFetch, applyResult, onSuccess, onSettled]);
528
+ const reset = useCallback2(() => {
529
+ setData(null);
530
+ }, []);
531
+ return { data, isLoading, error, query, refetch, reset };
532
+ }
533
+ function useDistinct(options) {
534
+ const { field, conditions, enabled = true, requestConfig, onSuccess, onError, onSettled } = options;
535
+ const [data, setData] = useState(null);
536
+ const applyResult = useCallback2((res) => {
537
+ setData(res.data);
538
+ }, []);
539
+ const doFetch = useCallback2(
540
+ async (signal) => {
541
+ if (conditions && Object.keys(conditions).length > 0) {
542
+ return await modelService.distinctAdvanced(field, conditions, { ...requestConfig, signal }).exec();
543
+ }
544
+ return await modelService.distinct(field, { ...requestConfig, signal }).exec();
545
+ },
546
+ [modelService, field, conditions, requestConfig]
547
+ );
548
+ const conditionsKey = stableStringify(conditions);
549
+ const { isLoading, error, refetch } = useAutoQuery({
550
+ doFetch,
551
+ applyResult,
552
+ shouldFetch: enabled,
553
+ deps: [enabled, field, conditionsKey],
554
+ onSuccess,
555
+ onError,
556
+ onSettled
557
+ });
558
+ const query = useCallback2(async () => {
559
+ const res = await doFetch();
560
+ applyResult(res);
561
+ onSuccess?.(res);
562
+ onSettled?.(res, null);
563
+ return res;
564
+ }, [doFetch, applyResult, onSuccess, onSettled]);
565
+ const reset = useCallback2(() => {
566
+ setData(null);
567
+ }, []);
568
+ return { data, isLoading, error, query, refetch, reset };
569
+ }
570
+ return {
571
+ useRead,
572
+ useList,
573
+ useCreate,
574
+ useUpdate,
575
+ useUpsert,
576
+ useDelete,
577
+ useCount,
578
+ useDistinct
579
+ };
580
+ }
581
+ export {
582
+ createModelHooks
583
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@web-ts-toolkit/access-router-react",
3
+ "description": "React hooks for @web-ts-toolkit/access-router-client model services",
4
+ "homepage": "https://web-ts-toolkit.pages.dev/docs/packages/access-router-react",
5
+ "version": "0.5.0",
6
+ "sideEffects": false,
7
+ "keywords": [
8
+ "react",
9
+ "hooks",
10
+ "typescript",
11
+ "crud",
12
+ "access-router",
13
+ "model"
14
+ ],
15
+ "main": "./index.js",
16
+ "module": "./index.mjs",
17
+ "types": "./index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./index.d.ts",
21
+ "import": "./index.mjs",
22
+ "require": "./index.js",
23
+ "default": "./index.js"
24
+ }
25
+ },
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "peerDependencies": {
30
+ "react": "^18.0.0 || ^19.0.0",
31
+ "@web-ts-toolkit/access-router-client": "0.5.0"
32
+ },
33
+ "author": "Junmin Ahn",
34
+ "bugs": {
35
+ "url": "https://github.com/egose/web-ts-toolkit/issues"
36
+ },
37
+ "license": "Apache-2.0",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/egose/web-ts-toolkit.git",
41
+ "directory": "packages/access-router-react"
42
+ }
43
+ }