api-core-lib 12.11.4 → 12.12.100

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/dist/client.js ADDED
@@ -0,0 +1,547 @@
1
+ "use client";
2
+ import {
3
+ buildPaginateQuery,
4
+ callDynamicApi,
5
+ createApiServices,
6
+ generateCacheKey,
7
+ globalStateManager
8
+ } from "./chunk-KPZ7BF52.js";
9
+
10
+ // src/hooks/useApi.ts
11
+ import { useState, useEffect, useCallback, useRef, useMemo } from "react";
12
+ function buildDynamicPath(template, params) {
13
+ if (!params) return template;
14
+ let path = template;
15
+ for (const key in params) {
16
+ path = path.replace(new RegExp(`{${key}}`, "g"), String(params[key]));
17
+ }
18
+ return path;
19
+ }
20
+ function useApi(axiosInstance, config) {
21
+ const {
22
+ endpoint,
23
+ pathParams,
24
+ // [REMOVE] لم نعد بحاجة لهذا الخيار
25
+ // encodeQuery = true,
26
+ initialData,
27
+ initialQuery = {},
28
+ enabled = true,
29
+ refetchAfterChange = true,
30
+ requestConfig,
31
+ onSuccess,
32
+ onError
33
+ } = config;
34
+ const finalEndpoint = useMemo(
35
+ () => buildDynamicPath(endpoint, pathParams),
36
+ [endpoint, pathParams]
37
+ );
38
+ const [state, setState] = useState({
39
+ data: initialData ?? null,
40
+ rawResponse: null,
41
+ loading: enabled,
42
+ error: null,
43
+ success: false,
44
+ message: void 0,
45
+ validationErrors: void 0
46
+ });
47
+ const [queryOptions, setQueryOptions] = useState(initialQuery);
48
+ const apiServices = useMemo(
49
+ () => createApiServices(axiosInstance, finalEndpoint),
50
+ [axiosInstance, finalEndpoint]
51
+ );
52
+ const savedOnSuccess = useRef(onSuccess);
53
+ const savedOnError = useRef(onError);
54
+ useEffect(() => {
55
+ savedOnSuccess.current = onSuccess;
56
+ savedOnError.current = onError;
57
+ }, [onSuccess, onError]);
58
+ const fetchData = useCallback(async (currentQueryOptions) => {
59
+ setState((prev) => ({ ...prev, loading: true, error: null }));
60
+ try {
61
+ const finalQuery = currentQueryOptions || queryOptions;
62
+ const hasQueryParams = Object.keys(finalQuery).length > 0;
63
+ const queryString = buildPaginateQuery(finalQuery);
64
+ const result = hasQueryParams ? await apiServices.getWithQuery(queryString, requestConfig) : await apiServices.get(void 0, requestConfig);
65
+ setState(result);
66
+ if (result.success && savedOnSuccess.current) {
67
+ savedOnSuccess.current(result.message || "Fetch successful!", result.data ?? void 0);
68
+ } else if (!result.success && savedOnError.current) {
69
+ savedOnError.current(result.message || "Fetch failed", result.error ?? void 0);
70
+ }
71
+ } catch (e) {
72
+ const apiError = { status: 500, message: e.message || "An unexpected client-side error occurred." };
73
+ setState((prev) => ({ ...prev, loading: false, error: apiError, success: false }));
74
+ if (savedOnError.current) {
75
+ savedOnError.current(apiError.message, apiError);
76
+ }
77
+ }
78
+ }, [apiServices, queryOptions, requestConfig]);
79
+ useEffect(() => {
80
+ if (enabled) {
81
+ fetchData();
82
+ }
83
+ }, [enabled, fetchData]);
84
+ const handleActionResult = useCallback(async (actionPromise) => {
85
+ setState((prev) => ({ ...prev, loading: true }));
86
+ try {
87
+ const result = await actionPromise;
88
+ if (result.success) {
89
+ if (savedOnSuccess.current) {
90
+ savedOnSuccess.current(result.message || "Action successful!", result.data ?? void 0);
91
+ }
92
+ if (refetchAfterChange) {
93
+ await fetchData(queryOptions);
94
+ } else {
95
+ setState((prev) => ({ ...prev, ...result, loading: false }));
96
+ }
97
+ } else {
98
+ if (savedOnError.current) {
99
+ savedOnError.current(result.message || "Action failed", result.error ?? void 0);
100
+ }
101
+ setState((prev) => ({ ...prev, loading: false, error: result.error, success: false }));
102
+ }
103
+ return result;
104
+ } catch (e) {
105
+ const apiError = { status: 500, message: e.message || "An unexpected error occurred during action." };
106
+ setState((prev) => ({ ...prev, loading: false, error: apiError, success: false }));
107
+ if (savedOnError.current) {
108
+ savedOnError.current(apiError.message, apiError);
109
+ }
110
+ return { data: null, error: apiError, loading: false, success: false, rawResponse: e };
111
+ }
112
+ }, [refetchAfterChange, fetchData, queryOptions]);
113
+ const actions = {
114
+ fetch: fetchData,
115
+ create: (newItem, options) => handleActionResult(apiServices.post(newItem, options)),
116
+ put: (id, item, options) => handleActionResult(apiServices.put(id, item, options)),
117
+ update: (id, updatedItem, options) => handleActionResult(apiServices.patch(id, updatedItem, options)),
118
+ remove: (id, options) => handleActionResult(apiServices.remove(id, options)),
119
+ bulkRemove: (ids, options) => handleActionResult(apiServices.bulkDelete(ids, options))
120
+ };
121
+ const query = {
122
+ options: queryOptions,
123
+ setOptions: setQueryOptions,
124
+ setPage: (page) => setQueryOptions((prev) => ({ ...prev, page })),
125
+ setLimit: (limit) => setQueryOptions((prev) => ({ ...prev, page: 1, limit })),
126
+ setSearchTerm: (search) => setQueryOptions((prev) => ({ ...prev, page: 1, search })),
127
+ setSorting: (sortBy) => setQueryOptions((prev) => ({ ...prev, sortBy })),
128
+ setFilters: (filter) => setQueryOptions((prev) => ({ ...prev, page: 1, filter })),
129
+ setQueryParam: (key, value) => setQueryOptions((prev) => ({ ...prev, [key]: value, page: key !== "page" ? 1 : value })),
130
+ reset: () => setQueryOptions(initialQuery)
131
+ };
132
+ return { state, setState, actions, query };
133
+ }
134
+
135
+ // src/hooks/useApiRecord/index.ts
136
+ import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2, useMemo as useMemo2 } from "react";
137
+ function buildDynamicPath2(template, params) {
138
+ if (!params) return template;
139
+ let path = template;
140
+ for (const key in params) {
141
+ path = path.replace(new RegExp(`{${key}}`, "g"), String(params[key]));
142
+ }
143
+ return path;
144
+ }
145
+ function useApiRecord(axiosInstance, config) {
146
+ const {
147
+ endpoint,
148
+ pathParams,
149
+ recordId,
150
+ initialData,
151
+ enabled = true,
152
+ refetchAfterChange = true,
153
+ requestConfig,
154
+ onSuccess,
155
+ onError
156
+ } = config;
157
+ const finalEndpoint = useMemo2(
158
+ () => buildDynamicPath2(endpoint, pathParams),
159
+ [endpoint, pathParams]
160
+ );
161
+ const initialState = useMemo2(() => ({
162
+ data: initialData ?? null,
163
+ rawResponse: null,
164
+ error: null,
165
+ loading: enabled && !!recordId,
166
+ success: false,
167
+ message: void 0,
168
+ validationErrors: void 0
169
+ }), [initialData, enabled, recordId]);
170
+ const [state, setState] = useState2(initialState);
171
+ const savedOnSuccess = useRef2(onSuccess);
172
+ const savedOnError = useRef2(onError);
173
+ useEffect2(() => {
174
+ savedOnSuccess.current = onSuccess;
175
+ savedOnError.current = onError;
176
+ }, [onSuccess, onError]);
177
+ const apiServices = useMemo2(
178
+ () => createApiServices(axiosInstance, finalEndpoint),
179
+ [axiosInstance, finalEndpoint]
180
+ );
181
+ const fetchRecord = useCallback2(async () => {
182
+ if (!recordId) {
183
+ setState(initialState);
184
+ return;
185
+ }
186
+ setState((prev) => ({ ...prev, loading: true, error: null, success: false }));
187
+ try {
188
+ const result = await apiServices.get(recordId, requestConfig);
189
+ setState(result);
190
+ if (result.success && savedOnSuccess.current) {
191
+ savedOnSuccess.current(result.message || "Record fetched successfully!", result.data ?? void 0);
192
+ } else if (!result.success && savedOnError.current) {
193
+ savedOnError.current(result.message || "Fetch failed", result.error ?? void 0);
194
+ }
195
+ } catch (e) {
196
+ console.error("[useApiRecord Fetch Error]", e);
197
+ const apiError = {
198
+ status: e.response?.status || 500,
199
+ message: e.message || "An unexpected client-side error occurred.",
200
+ code: e.code
201
+ };
202
+ setState((prev) => ({ ...prev, loading: false, success: false, error: apiError }));
203
+ if (savedOnError.current) {
204
+ savedOnError.current(apiError.message, apiError);
205
+ }
206
+ }
207
+ }, [apiServices, recordId, requestConfig, initialState]);
208
+ useEffect2(() => {
209
+ if (enabled) {
210
+ fetchRecord();
211
+ }
212
+ }, [enabled, fetchRecord]);
213
+ const handleActionResult = useCallback2(
214
+ async (actionPromise, options) => {
215
+ setState((prev) => ({ ...prev, loading: true }));
216
+ try {
217
+ const result = await actionPromise;
218
+ if (result.success) {
219
+ if (savedOnSuccess.current) {
220
+ savedOnSuccess.current(result.message || "Action successful!", result.data);
221
+ }
222
+ const shouldRefetch = options?.refetch ?? refetchAfterChange;
223
+ if (shouldRefetch) {
224
+ await fetchRecord();
225
+ } else {
226
+ setState({ ...result, loading: false, data: result.data });
227
+ }
228
+ } else {
229
+ if (savedOnError.current) {
230
+ savedOnError.current(result.message || "Action failed", result.error ?? void 0);
231
+ }
232
+ setState((prev) => ({ ...prev, ...result, loading: false }));
233
+ }
234
+ return result;
235
+ } catch (e) {
236
+ console.error("[useApiRecord Action Error]", e);
237
+ const apiError = {
238
+ status: e.response?.status || 500,
239
+ message: e.message || "An unexpected error occurred during the action.",
240
+ code: e.code
241
+ };
242
+ const errorResponse = { ...initialState, loading: false, success: false, error: apiError, rawResponse: e, data: null };
243
+ setState(errorResponse);
244
+ if (savedOnError.current) {
245
+ savedOnError.current(apiError.message, apiError);
246
+ }
247
+ return errorResponse;
248
+ }
249
+ },
250
+ [refetchAfterChange, fetchRecord, initialState]
251
+ );
252
+ const actions = {
253
+ fetch: fetchRecord,
254
+ update: (updatedItem, options) => handleActionResult(apiServices.patch(recordId, updatedItem, { ...requestConfig, ...options }), options),
255
+ put: (item, options) => handleActionResult(apiServices.put(recordId, item, { ...requestConfig, ...options }), options),
256
+ remove: (options) => handleActionResult(apiServices.remove(recordId, { ...requestConfig, ...options }), options),
257
+ resetState: () => setState(initialState)
258
+ };
259
+ return { state, setState, actions };
260
+ }
261
+
262
+ // src/hooks/useDeepCompareEffect/index.ts
263
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
264
+ import isEqual from "fast-deep-equal";
265
+ function useDeepCompareEffect(callback, dependencies) {
266
+ const currentDependenciesRef = useRef3();
267
+ if (!isEqual(currentDependenciesRef.current, dependencies)) {
268
+ currentDependenciesRef.current = dependencies;
269
+ }
270
+ useEffect3(callback, [currentDependenciesRef.current]);
271
+ }
272
+
273
+ // src/hooks/useApiModule/useApiModule.ts
274
+ import { createContext, useCallback as useCallback3, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState3, useSyncExternalStore } from "react";
275
+ var ApiModuleContext = createContext(null);
276
+ var ApiModuleProvider = ApiModuleContext.Provider;
277
+ var createInitialState = () => ({
278
+ data: null,
279
+ lastSuccessAt: void 0,
280
+ meta: [],
281
+ validationErrors: [],
282
+ error: null,
283
+ loading: false,
284
+ success: false,
285
+ called: false,
286
+ isStale: false,
287
+ rawResponse: null
288
+ });
289
+ function useApiActionState(actionConfig, cacheKey, execute, input, enabled) {
290
+ const getClientSnapshot = () => globalStateManager.getSnapshot(cacheKey);
291
+ const getServerSnapshot = () => globalStateManager.getSnapshot(cacheKey);
292
+ const state = useSyncExternalStore(
293
+ (callback) => globalStateManager.subscribe(cacheKey, callback),
294
+ getClientSnapshot,
295
+ getServerSnapshot
296
+ );
297
+ const inputRef = useRef4(input);
298
+ useEffect4(() => {
299
+ inputRef.current = input;
300
+ }, [input]);
301
+ const refetch = useCallback3(() => {
302
+ execute(inputRef.current);
303
+ }, [execute]);
304
+ const prevCacheKeyRef = useRef4(cacheKey);
305
+ useEffect4(() => {
306
+ if (prevCacheKeyRef.current !== cacheKey && enabled && state.called) {
307
+ console.log(`[Cache Key Changed] from ${prevCacheKeyRef.current} to ${cacheKey}. Refetching...`);
308
+ refetch();
309
+ } else if (enabled && actionConfig.autoFetch && !state.called && !state.loading) {
310
+ console.log(`[Auto Fetch] for ${cacheKey}. Fetching...`);
311
+ refetch();
312
+ } else if (enabled && state.isStale && !state.loading) {
313
+ console.log(`[Stale State] for ${cacheKey}. Refetching...`);
314
+ refetch();
315
+ }
316
+ prevCacheKeyRef.current = cacheKey;
317
+ }, [cacheKey, enabled, state.isStale, state.loading, state.called, actionConfig.autoFetch, refetch]);
318
+ return state;
319
+ }
320
+ function useApiModule(axiosInstance, moduleConfig, options = {}) {
321
+ const {
322
+ refetchOnWindowFocus = true,
323
+ onSuccess,
324
+ onError,
325
+ pathParams: modulePathParams,
326
+ enabled = true,
327
+ hydratedState,
328
+ extraContextData
329
+ } = options;
330
+ const pathParamsString = useMemo3(() => JSON.stringify(modulePathParams || {}), [modulePathParams]);
331
+ const [queryOptions, setQueryOptions] = useState3({});
332
+ const savedCallbacks = useRef4({ onSuccess, onError });
333
+ useEffect4(() => {
334
+ savedCallbacks.current = { onSuccess, onError };
335
+ }, [onSuccess, onError]);
336
+ useMemo3(() => {
337
+ if (hydratedState) {
338
+ globalStateManager.rehydrate(hydratedState);
339
+ }
340
+ }, [hydratedState]);
341
+ useEffect4(() => {
342
+ savedCallbacks.current = { onSuccess, onError };
343
+ }, [onSuccess, onError]);
344
+ useMemo3(() => {
345
+ if (hydratedState) {
346
+ globalStateManager.rehydrate(hydratedState);
347
+ }
348
+ }, [hydratedState]);
349
+ const actions = useMemo3(() => {
350
+ return Object.keys(moduleConfig.actions).reduce((acc, actionName) => {
351
+ const actionConfig = moduleConfig.actions[actionName];
352
+ const shouldCache = actionConfig.cacheResponse !== false;
353
+ const execute = async (input, options2 = {}) => {
354
+ const finalPathParams = { ...JSON.parse(pathParamsString), ...options2.pathParams };
355
+ const cacheKey = shouldCache ? generateCacheKey(moduleConfig.baseEndpoint, actionName, input, { pathParams: finalPathParams }) : "";
356
+ let mutationContext;
357
+ try {
358
+ if (options2.onMutate && shouldCache) {
359
+ mutationContext = await options2.onMutate(input);
360
+ }
361
+ if (shouldCache) {
362
+ globalStateManager.setState(cacheKey, (prev) => ({ ...prev, loading: true, called: true, error: null, isStale: false }));
363
+ }
364
+ const result = await callDynamicApi(axiosInstance, moduleConfig.baseEndpoint, actionConfig, {
365
+ pathParams: finalPathParams,
366
+ body: input,
367
+ config: options2.config
368
+ });
369
+ console.log("[API Result execute]", result);
370
+ if (shouldCache) {
371
+ globalStateManager.setState(cacheKey, (prev) => ({
372
+ ...prev,
373
+ // احتفظ بالخصائص القديمة مثل 'called'
374
+ loading: false,
375
+ success: result.success,
376
+ // تحديث صريح
377
+ error: result.success ? null : result.error || prev.error,
378
+ // تحديث صريح
379
+ data: result.data,
380
+ // تحديث صريح للبيانات
381
+ meta: result.meta,
382
+ // تحديث صريح
383
+ links: result.links,
384
+ // تحديث صريح
385
+ message: result.message,
386
+ validationErrors: result.validationErrors || [],
387
+ rawResponse: result.rawResponse
388
+ // isStale تم تعيينه إلى false في بداية execute، وسيظل كذلك
389
+ }));
390
+ }
391
+ if (result.success) {
392
+ savedCallbacks.current.onSuccess?.(actionName, result.message || "Action successful", result.data);
393
+ options2.onSuccess?.(result.data, mutationContext);
394
+ actionConfig.invalidates?.forEach((keyToInvalidate) => {
395
+ const prefix = `${moduleConfig.baseEndpoint}/${keyToInvalidate}::`;
396
+ console.log(`[Invalidating] by prefix: ${prefix}`);
397
+ globalStateManager.invalidateByPrefix(prefix);
398
+ });
399
+ } else {
400
+ savedCallbacks.current.onError?.(actionName, result.message || "Action failed", result.error ?? void 0);
401
+ options2.onError?.(result.error, mutationContext);
402
+ }
403
+ return result;
404
+ } catch (error) {
405
+ const apiError = error.response?.data || { status: 500, message: error.message };
406
+ if (shouldCache) {
407
+ globalStateManager.setState(cacheKey, (prev) => ({ ...prev, error: apiError, loading: false, success: false }));
408
+ }
409
+ savedCallbacks.current.onError?.(actionName, apiError.message, apiError);
410
+ options2.onError?.(apiError, mutationContext);
411
+ throw error;
412
+ } finally {
413
+ if (options2.onSettled) {
414
+ options2.onSettled();
415
+ }
416
+ }
417
+ };
418
+ const reset = (input, options2 = {}) => {
419
+ if (shouldCache) {
420
+ const finalPathParams = { ...JSON.parse(pathParamsString), ...options2.pathParams };
421
+ const cacheKey = generateCacheKey(moduleConfig.baseEndpoint, actionName, input, { pathParams: finalPathParams });
422
+ globalStateManager.setState(cacheKey, () => createInitialState());
423
+ }
424
+ };
425
+ acc[actionName] = { execute, reset };
426
+ return acc;
427
+ }, {});
428
+ }, [axiosInstance, moduleConfig, pathParamsString]);
429
+ const queries = useMemo3(() => {
430
+ const builtQueries = {};
431
+ for (const actionName in moduleConfig.actions) {
432
+ if (moduleConfig.actions[actionName]?.hasQuery) {
433
+ const setActionQueryOptions = (updater) => {
434
+ setQueryOptions((prev) => ({ ...prev, [actionName]: typeof updater === "function" ? updater(prev[actionName] || {}) : updater }));
435
+ };
436
+ builtQueries[actionName] = {
437
+ options: queryOptions[actionName] || {},
438
+ setOptions: setActionQueryOptions,
439
+ setPage: (page) => setActionQueryOptions((p) => ({ ...p, page })),
440
+ setLimit: (limit) => setActionQueryOptions((p) => ({ ...p, limit, page: 1 })),
441
+ setSearchTerm: (search) => setActionQueryOptions((p) => ({ ...p, search, page: 1 })),
442
+ setFilters: (filter) => setActionQueryOptions((p) => ({ ...p, filter, page: 1 })),
443
+ setSorting: (sortBy) => setActionQueryOptions((p) => ({ ...p, sortBy })),
444
+ setQueryParam: (key, value) => setActionQueryOptions((p) => ({ ...p, [key]: value, page: key !== "page" ? value : p.page })),
445
+ reset: () => setActionQueryOptions({})
446
+ };
447
+ }
448
+ }
449
+ return builtQueries;
450
+ }, [queryOptions, moduleConfig.actions]);
451
+ const states = {};
452
+ function isActionWithQuery(key, actions2) {
453
+ return actions2[key]?.hasQuery === true;
454
+ }
455
+ for (const actionName in moduleConfig.actions) {
456
+ if (Object.prototype.hasOwnProperty.call(moduleConfig.actions, actionName)) {
457
+ const actionConfig = moduleConfig.actions[actionName];
458
+ if (actionConfig.cacheResponse !== false) {
459
+ let queryOptions2;
460
+ if (isActionWithQuery(actionName, moduleConfig.actions)) {
461
+ queryOptions2 = queries[actionName]?.options;
462
+ }
463
+ const input = queryOptions2;
464
+ const pathParams = JSON.parse(pathParamsString);
465
+ const cacheKey = generateCacheKey(
466
+ moduleConfig.baseEndpoint,
467
+ actionName,
468
+ input,
469
+ { pathParams }
470
+ );
471
+ states[actionName] = useApiActionState(
472
+ actionConfig,
473
+ cacheKey,
474
+ actions[actionName].execute,
475
+ input,
476
+ enabled
477
+ );
478
+ } else {
479
+ states[actionName] = createInitialState();
480
+ }
481
+ }
482
+ }
483
+ const lastBlurTimestamp = useRef4(Date.now());
484
+ useEffect4(() => {
485
+ if (!enabled || !refetchOnWindowFocus) return;
486
+ const onFocus = () => {
487
+ if (Date.now() - lastBlurTimestamp.current > 1e4) {
488
+ const actionKeys = Object.keys(moduleConfig.actions);
489
+ for (const actionName of actionKeys) {
490
+ const state = states[actionName];
491
+ if (state && state.called && !state.loading) {
492
+ const prefix = `${moduleConfig.baseEndpoint}/${actionName}::`;
493
+ globalStateManager.invalidateByPrefix(prefix);
494
+ }
495
+ }
496
+ }
497
+ };
498
+ const onBlur = () => {
499
+ lastBlurTimestamp.current = Date.now();
500
+ };
501
+ window.addEventListener("focus", onFocus);
502
+ window.addEventListener("blur", onBlur);
503
+ return () => {
504
+ window.removeEventListener("focus", onFocus);
505
+ window.removeEventListener("blur", onBlur);
506
+ };
507
+ }, [enabled, refetchOnWindowFocus, moduleConfig, states]);
508
+ const dehydrate = useMemo3(() => {
509
+ return () => globalStateManager.dehydrate();
510
+ }, []);
511
+ const baseApiReturn = { actions, states, queries, dehydrate };
512
+ const finalApiReturn = useMemo3(() => ({
513
+ ...baseApiReturn,
514
+ ...extraContextData || {}
515
+ }), [baseApiReturn, extraContextData]);
516
+ return finalApiReturn;
517
+ }
518
+
519
+ // src/hooks/useApiModule/apiModuleContext.ts
520
+ import { createContext as createContext2, useContext as useContext2, createElement } from "react";
521
+ function createApiModuleContext() {
522
+ const Context = createContext2(null);
523
+ Context.displayName = "ApiModuleContext";
524
+ const Provider = ({ value, children }) => {
525
+ return createElement(Context.Provider, { value }, children);
526
+ };
527
+ const useConsumer = () => {
528
+ const context = useContext2(Context);
529
+ if (!context) {
530
+ throw new Error("This component must be used within its corresponding ApiModuleProvider.");
531
+ }
532
+ return context;
533
+ };
534
+ return {
535
+ Context,
536
+ Provider,
537
+ useContext: useConsumer
538
+ };
539
+ }
540
+ export {
541
+ ApiModuleProvider,
542
+ createApiModuleContext,
543
+ useApi,
544
+ useApiModule,
545
+ useApiRecord,
546
+ useDeepCompareEffect
547
+ };