kmod-cli 1.8.1 → 1.9.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.
@@ -135,10 +135,30 @@ export interface DataProviderError {
135
135
  }
136
136
 
137
137
  export interface DataProviderOptions {
138
+ /**
139
+ * Time to live for cached data (default: 5 * 60 * 1000) - 5 minutes
140
+ */
138
141
  cacheTime?: number;
142
+ /**
143
+ * Time to live for stale data (default: 5 * 60 * 1000) - 5 minutes
144
+ */
145
+ staleTime?: number;
146
+ /**
147
+ * Retry count (default: 1)
148
+ */
139
149
  retryCount?: number;
150
+ /**
151
+ * Retry delay (ms) (default: 1000) - 1 second
152
+ */
140
153
  retryDelay?: number;
154
+ /**
155
+ * Debug mode (default: false)
156
+ */
141
157
  debug?: boolean;
158
+ /**
159
+ * Persist data in memory, local storage, or session storage (default: memory)
160
+ */
161
+ persist?: "memory" | "local" | "session";
142
162
  }
143
163
 
144
164
  interface CacheItem<T = any> {
@@ -149,6 +169,7 @@ interface CacheItem<T = any> {
149
169
  export interface UseListOptions {
150
170
  refetchInterval?: number;
151
171
  enabled?: boolean;
172
+ cache?: boolean;
152
173
  }
153
174
 
154
175
  export interface UseOneOptions {
@@ -221,6 +242,8 @@ class DataProvider {
221
242
  private cache: Map<string, CacheItem>;
222
243
  private options: Required<DataProviderOptions>;
223
244
 
245
+ private cachePrefix = "__DP_CACHE__:";
246
+
224
247
  constructor(
225
248
  httpClient: AxiosInstance = axios.create(),
226
249
  options: DataProviderOptions = {},
@@ -239,11 +262,31 @@ class DataProvider {
239
262
  this.cache = new Map();
240
263
  this.options = {
241
264
  cacheTime: 5 * 60 * 1000,
265
+ staleTime: 30 * 1000,
266
+ persist: "memory",
242
267
  retryCount: 1,
243
268
  retryDelay: 1000,
244
269
  debug: false,
245
270
  ...options,
246
271
  };
272
+
273
+ this.cache = new Map();
274
+ this.loadPersistCache();
275
+ }
276
+
277
+ private loadPersistCache() {
278
+ if (this.options.persist === "memory") return;
279
+
280
+ const storage =
281
+ this.options.persist === "local" ? localStorage : sessionStorage;
282
+
283
+ Object.keys(storage).forEach((key) => {
284
+ if (!key.startsWith(this.cachePrefix)) return;
285
+ try {
286
+ const value = JSON.parse(storage.getItem(key)!);
287
+ this.cache.set(key.replace(this.cachePrefix, ""), value);
288
+ } catch {}
289
+ });
247
290
  }
248
291
 
249
292
  private log(message: string, data?: any): void {
@@ -257,27 +300,40 @@ class DataProvider {
257
300
  return `${resource}:${JSON.stringify(params || {})}`;
258
301
  }
259
302
 
260
- private getCache<T = any>(key: string): T | null {
303
+ private getCache<T>(key: string): {
304
+ data: T;
305
+ isStale: boolean;
306
+ } | null {
261
307
  const cached = this.cache.get(key);
262
308
  if (!cached) return null;
263
309
 
264
- const now = Date.now();
265
- if (now - cached.timestamp > this.options.cacheTime) {
310
+ const age = Date.now() - cached.timestamp;
311
+
312
+ if (age > this.options.cacheTime) {
266
313
  this.cache.delete(key);
267
- // this.log('Cache expired', key);
268
314
  return null;
269
315
  }
270
316
 
271
- // this.log('Cache hit', key);
272
- return cached.data as T;
317
+ return {
318
+ data: cached.data as T,
319
+ isStale: age > this.options.staleTime,
320
+ };
273
321
  }
274
322
 
275
- private setCache<T = any>(key: string, data: T): void {
276
- this.cache.set(key, {
323
+ private setCache<T>(key: string, data: T) {
324
+ const item = {
277
325
  data,
278
326
  timestamp: Date.now(),
279
- });
280
- // this.log('Cache set', key);
327
+ };
328
+
329
+ this.cache.set(key, item);
330
+
331
+ if (this.options.persist !== "memory") {
332
+ const storage =
333
+ this.options.persist === "local" ? localStorage : sessionStorage;
334
+
335
+ storage.setItem(this.cachePrefix + key, JSON.stringify(item));
336
+ }
281
337
  }
282
338
 
283
339
  public invalidateCache(resource: string, id?: string | number): void {
@@ -347,10 +403,21 @@ class DataProvider {
347
403
  useCache: boolean = true,
348
404
  ): Promise<GetListResponse<T>> {
349
405
  const cacheKey = this.getCacheKey(resource, params);
406
+ const cached = useCache
407
+ ? this.getCache<GetListResponse<T>>(cacheKey)
408
+ : null;
409
+
410
+ if (cached) {
411
+ if (!cached.isStale) {
412
+ return cached.data;
413
+ }
350
414
 
351
- if (useCache) {
352
- const cached = this.getCache<GetListResponse<T>>(cacheKey);
353
- if (cached) return cached;
415
+ // stale → trả data cũ + background refetch
416
+ this.getList(resource, params, false).then((fresh) => {
417
+ this.setCache(cacheKey, fresh);
418
+ });
419
+
420
+ return cached.data;
354
421
  }
355
422
 
356
423
  const { pagination, filters, sorters, meta } = params;
@@ -437,10 +504,16 @@ class DataProvider {
437
504
  ): Promise<GetOneResponse<T>> {
438
505
  const { id, meta } = params;
439
506
  const cacheKey = this.getCacheKey(`${resource}/${id}`, {});
507
+ const cached = useCache ? this.getCache<GetOneResponse<T>>(cacheKey) : null;
508
+
509
+ if (cached) {
510
+ if (!cached.isStale) return cached.data;
511
+
512
+ this.getOne(resource, params, false).then((fresh) => {
513
+ this.setCache(cacheKey, fresh);
514
+ });
440
515
 
441
- if (useCache) {
442
- const cached = this.getCache<GetOneResponse<T>>(cacheKey);
443
- if (cached) return cached;
516
+ return cached.data;
444
517
  }
445
518
 
446
519
  const url = `${this.apiUrl}/${resource}/${id}`;
@@ -743,32 +816,36 @@ export function useList<T = any>(
743
816
 
744
817
  const dataProvider = useDataProvider();
745
818
 
746
- const { refetchInterval, enabled = true } = options;
819
+ const { refetchInterval, enabled = true, cache = true } = options;
747
820
  const paramsStr = JSON.stringify(params);
748
821
 
749
- const refetch = useCallback(async () => {
750
- if (!enabled) {
751
- setLoading(false);
752
- return;
753
- }
822
+ const refetch = useCallback(
823
+ async (force = false) => {
824
+ if (!enabled) {
825
+ setLoading(false);
826
+ return;
827
+ }
754
828
 
755
- try {
756
- setLoading(true);
757
- setError(null);
758
- const result = await dataProvider.getList<T>(
759
- resource,
760
- JSON.parse(paramsStr),
761
- );
762
- setData(result.data || []);
763
- setTotal(result.total || 0);
764
- } catch (err) {
765
- setError(err as DataProviderError);
766
- setData([]);
767
- setTotal(0);
768
- } finally {
769
- setLoading(false);
770
- }
771
- }, [dataProvider, resource, paramsStr, enabled]);
829
+ try {
830
+ setLoading(true);
831
+ setError(null);
832
+ const result = await dataProvider.getList<T>(
833
+ resource,
834
+ JSON.parse(paramsStr),
835
+ !force,
836
+ );
837
+ setData(result.data || []);
838
+ setTotal(result.total || 0);
839
+ } catch (err) {
840
+ setError(err as DataProviderError);
841
+ setData([]);
842
+ setTotal(0);
843
+ } finally {
844
+ setLoading(false);
845
+ }
846
+ },
847
+ [dataProvider, resource, paramsStr, enabled],
848
+ );
772
849
 
773
850
  useEffect(() => {
774
851
  refetch();
@@ -797,24 +874,27 @@ export function useOne<T = any>(
797
874
 
798
875
  const { enabled = true } = options;
799
876
 
800
- const refetch = useCallback(async () => {
801
- if (!enabled || !id) {
802
- setLoading(false);
803
- return;
804
- }
877
+ const refetch = useCallback(
878
+ async (force = false) => {
879
+ if (!enabled || !id) {
880
+ setLoading(false);
881
+ return;
882
+ }
805
883
 
806
- try {
807
- setLoading(true);
808
- setError(null);
809
- const result = await dataProvider.getOne<T>(resource, { id });
810
- setData(result.data);
811
- } catch (err) {
812
- setError(err as DataProviderError);
813
- setData(null);
814
- } finally {
815
- setLoading(false);
816
- }
817
- }, [dataProvider, resource, id, enabled]);
884
+ try {
885
+ setLoading(true);
886
+ setError(null);
887
+ const result = await dataProvider.getOne<T>(resource, { id }, !force);
888
+ setData(result.data);
889
+ } catch (err) {
890
+ setError(err as DataProviderError);
891
+ setData(null);
892
+ } finally {
893
+ setLoading(false);
894
+ }
895
+ },
896
+ [dataProvider, resource, id, enabled],
897
+ );
818
898
 
819
899
  useEffect(() => {
820
900
  refetch();
@@ -1290,78 +1370,158 @@ export const cookiesProvider = {
1290
1370
  },
1291
1371
  };
1292
1372
 
1293
- // =================== Example ===================
1294
-
1295
- // create httpClient - (can create multiple httpClients for different apis)
1373
+ // =================== 1. Create httpClient ===================
1296
1374
 
1297
1375
  // const TOKEN = "token";
1298
1376
 
1299
1377
  // const httpClient = createHttpClient({
1300
- // url: `${process.env.NEXT_PUBLIC_API_URL}`,
1378
+ // url: process.env.NEXT_PUBLIC_API_URL,
1301
1379
  // options: {
1302
1380
  // authorizationType: "Bearer",
1303
1381
  // tokenName: TOKEN,
1304
- // tokenStorage: "cookie",
1305
- // withCredentials: true, --- optionals (default to true if tokenStorage is "http-only")
1382
+ // tokenStorage: "cookie", // or "http-only" | "local" | "session"
1383
+ // withCredentials: true, // optional (auto true if tokenStorage = "http-only")
1306
1384
  // },
1307
1385
  // });
1308
1386
 
1309
- // create dataProvider
1387
+ // =================== 2. Create DataProvider ===================
1310
1388
 
1311
- // const dataProvider = useDataProvider(httpClient);
1389
+ // IMPORTANT:
1390
+ // persist !== "memory" --> to CACHE data and prevent F5 from calling API again
1391
+
1392
+ // const dataProvider = new DataProvider(httpClient, {
1393
+ // persist: "session", // "memory" | "session" | "local"
1394
+ // cacheTime: 5 * 60 * 1000, // 5 minutes
1395
+ // staleTime: 30 * 1000, // 30 seconds
1396
+ // });
1397
+
1398
+ // =================== 3. Auth Provider URLs ===================
1312
1399
 
1313
1400
  // const urls = {
1314
- // loginUrl: "/auth/login", --> api_login
1315
- // logoutUrl: "/auth/logout", --> api_logout
1316
- // meUrl: "/auth/me", --> api_get_me_by_token
1317
- // }
1401
+ // loginUrl: "/auth/login", // POST
1402
+ // logoutUrl: "/auth/logout", // POST
1403
+ // meUrl: "/auth/me", // GET (by token)
1404
+ // };
1318
1405
 
1319
- // const keysRemoveOnLogout = [TOKEN, "refreshToken", "user"];
1406
+ // const keysRemoveOnLogout = [TOKEN, "refreshToken", "user"];
1320
1407
 
1321
- // wrapped all into:
1322
- // <DataProvider dataProvider={dataProvider}>
1408
+ // =================== 4. Wrap Providers ===================
1409
+
1410
+ // <DataProviderContainer dataProvider={dataProvider}>
1323
1411
  // <AuthProvider
1324
- // urls={urls} --> api_login
1325
- // tokenKey={TOKEN}
1326
- // keysCleanUpOnLogout={keysRemoveOnLogout} --> optional (default to ["token"]) - additional keys to clean up on logout
1412
+ // urls={urls}
1413
+ // tokenKey={TOKEN}
1414
+ // keysCleanUpOnLogout={keysRemoveOnLogout}
1327
1415
  // >
1328
1416
  // <App />
1329
1417
  // </AuthProvider>
1330
- // </DataProvider>
1418
+ // </DataProviderContainer>
1419
+
1420
+ // =================== 5. Auth Hooks ===================
1421
+
1422
+ // const { login, logout, getMe, isAuthenticated } = useAuth();
1423
+
1424
+ // =================== 6. useList ===================
1425
+
1426
+ // Auto load on mount
1427
+ // Use cache if exists (F5 will NOT refetch if persist !== memory)
1428
+
1429
+ // const {
1430
+ // data,
1431
+ // total,
1432
+ // loading,
1433
+ // error,
1434
+ // refetch,
1435
+ // } = useList<User>(
1436
+ // "users",
1437
+ // {
1438
+ // meta: {
1439
+ // params: {
1440
+ // role: "admin",
1441
+ // },
1442
+ // },
1443
+ // },
1444
+ // {
1445
+ // cache: true,
1446
+ // },
1447
+ // );
1448
+
1449
+ // Force refetch (bypass cache)
1450
+ // refetch(true);
1451
+
1452
+ // =================== 7. useOne ===================
1453
+
1454
+ // const {
1455
+ // data,
1456
+ // loading,
1457
+ // error,
1458
+ // refetch,
1459
+ // } = useOne<User>(
1460
+ // "users",
1461
+ // userId,
1462
+ // {
1463
+ // enabled: !!userId,
1464
+ // },
1465
+ // );
1331
1466
 
1332
- // use hooks to auth
1467
+ // Force refetch (ignore cache)
1468
+ // refetch(true);
1333
1469
 
1334
- // const { login, logout, refresh } = useAuth();
1470
+ // =================== 8. useCreate ===================
1335
1471
 
1336
- // use hook to call apis
1472
+ // const { mutate, loading, error } = useCreate<User, CreateUserPayload>(
1473
+ // "users",
1474
+ // {
1475
+ // onSuccess: (data) => {
1476
+ // console.log("Created:", data);
1477
+ // },
1478
+ // },
1479
+ // );
1337
1480
 
1338
- // const { data, isLoading, error } = useList<DataResponse>({
1339
- // url: '/route_name',
1481
+ // mutate({
1482
+ // name: "John",
1483
+ // email: "john@mail.com",
1340
1484
  // });
1341
1485
 
1342
- // const { data, isLoading, error } = useOne<DataResponse>({
1343
- // url: '/route_name/:id',
1344
- // id: 1,
1345
- // });
1486
+ // =================== 9. useUpdate ===================
1346
1487
 
1347
- // const { data, isLoading, error } = useCreate<DataResponse>({
1348
- // url: '/route_name',
1349
- // payload: {},
1350
- // });
1488
+ // const { mutate, loading, error } = useUpdate<User, UpdateUserPayload>(
1489
+ // "users",
1490
+ // {
1491
+ // onSuccess: (data) => {
1492
+ // console.log("Updated:", data);
1493
+ // },
1494
+ // },
1495
+ // );
1351
1496
 
1352
- // const { data, isLoading, error } = useUpdate<DataResponse>({
1353
- // url: '/route_name/:id',
1354
- // id: 1,
1355
- // payload: {},
1497
+ // mutate(1, {
1498
+ // name: "New name",
1356
1499
  // });
1357
1500
 
1358
- // const { data, isLoading, error } = useDelete<DataResponse>({
1359
- // url: '/route_name/:id',
1360
- // id: 1,
1361
- // });
1501
+ // =================== 10. useDelete ===================
1362
1502
 
1363
- // const { data, isLoading, error } = useCustom<DataResponse>({
1364
- // url: '/route_name or api_url/route/...',
1365
- // method: 'post',
1366
- // payload: {},
1367
- // });
1503
+ // const { mutate, loading } = useDelete<User>(
1504
+ // "users",
1505
+ // {
1506
+ // onSuccess: () => {
1507
+ // console.log("Deleted");
1508
+ // },
1509
+ // },
1510
+ // );
1511
+
1512
+ // mutate(1);
1513
+
1514
+ // =================== 11. useCustom ===================
1515
+
1516
+ // const { mutate, data, loading, error } = useCustom<CustomResponse>(
1517
+ // "/reports/export",
1518
+ // {
1519
+ // method: "post",
1520
+ // payload: {
1521
+ // from: "2024-01-01",
1522
+ // to: "2024-12-31",
1523
+ // },
1524
+ // },
1525
+ // );
1526
+
1527
+ // mutate();
@@ -96,6 +96,9 @@ export type ValidationRules<T> = Partial<{
96
96
  [K in keyof T]: ValidationRule<T[K]>;
97
97
  }>;
98
98
 
99
+ type SetValuesAction<T> =
100
+ | Partial<T>
101
+ | ((prev: T) => Partial<T>);
99
102
 
100
103
  type ValidationErrors<T> = {
101
104
  [K in keyof T]?: string;
@@ -111,7 +114,7 @@ export const useFormValidator = <T extends Record<string, any>>(
111
114
  const [touched, setTouched] = useState<Touched<T>>({});
112
115
 
113
116
 
114
- const validateField = <K extends keyof T>(
117
+ const validateField = <K extends keyof T>(
115
118
  field: K,
116
119
  value: T[K],
117
120
  isTouched = false,
@@ -185,7 +188,7 @@ const validateField = <K extends keyof T>(
185
188
 
186
189
 
187
190
  // validate array of fields
188
- const validateFields = (fields: readonly (keyof T)[]): boolean => {
191
+ const validateFields = (fields: readonly (keyof T)[]): boolean => {
189
192
  let isValid = true;
190
193
  const newErrors: ValidationErrors<T> = {};
191
194
 
@@ -216,6 +219,66 @@ const validateFields = (fields: readonly (keyof T)[]): boolean => {
216
219
  setErrors({});
217
220
  setTouched({});
218
221
  };
222
+ const setValuesWithTouched = (
223
+ action: SetValuesAction<T>,
224
+ options?: {
225
+ validate?: boolean;
226
+ touchAll?: boolean;
227
+ isSubmit?: boolean;
228
+ }
229
+ ) => {
230
+ const {
231
+ validate = true,
232
+ touchAll = false,
233
+ isSubmit = false,
234
+ } = options || {};
235
+
236
+ setValues((prev) => {
237
+ const nextPartial =
238
+ typeof action === "function"
239
+ ? action(prev)
240
+ : action;
241
+
242
+ const merged = { ...prev, ...nextPartial };
243
+
244
+ const newTouched: Touched<T> = {};
245
+ const newErrors: ValidationErrors<T> = {};
246
+
247
+ const fields = touchAll
248
+ ? (Object.keys(merged) as (keyof T)[])
249
+ : (Object.keys(nextPartial) as (keyof T)[]);
250
+
251
+ fields.forEach((field) => {
252
+ newTouched[field] = true;
253
+
254
+ if (validate) {
255
+ const error = validateField(
256
+ field,
257
+ merged[field],
258
+ true,
259
+ isSubmit
260
+ );
261
+ newErrors[field] = error;
262
+ }
263
+ });
264
+
265
+ setTouched((prevTouched) => ({
266
+ ...prevTouched,
267
+ ...newTouched,
268
+ }));
269
+
270
+ if (validate) {
271
+ setErrors((prevErrors) => ({
272
+ ...prevErrors,
273
+ ...newErrors,
274
+ }));
275
+ }
276
+
277
+ return merged;
278
+ });
279
+ };
280
+
281
+
219
282
 
220
283
 
221
284
  return {
@@ -227,19 +290,21 @@ const validateFields = (fields: readonly (keyof T)[]): boolean => {
227
290
  validateAllFields,
228
291
  setValues,
229
292
  resetForm,
293
+ setValuesWithTouched,
230
294
  };
231
295
  };
232
296
 
233
297
 
298
+
234
299
  // import { useFormValidator, REGEXS } from '@/utils/validate/simple-validate';
235
300
 
236
301
  // const validationRules = {
237
- // username: { required: true, pattern: REGEXS.username, errorMessage: "Username must be 3-20 alphanumeric characters." },
238
- // password: { required: true, pattern: REGEXS.password, errorMessage: "Password must contain letters, numbers, and be at least 8 characters." },
302
+ // email: { required: true, pattern: REGEXS.email, errorMessage: "Please enter a valid email." },
303
+ // password: { required: true, minLength: 6, errorMessage: "Password must be at least 6 characters." },
239
304
  // };
240
305
 
241
306
  // const { values, errors, handleChange, validateAllFields } = useFormValidator(
242
- // { username: '', password: '' },
307
+ // { email: '', password: '' },
243
308
  // validationRules
244
309
  // );
245
310
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kmod-cli",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "Stack components utilities fast setup in projects",
5
5
  "author": "kumo_d",
6
6
  "license": "MIT",