vasuzex 2.3.11 → 2.3.13

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.
@@ -859,14 +859,22 @@ export class Model extends GuruORMModel {
859
859
  if (this.timestamps && this.updatedAt) {
860
860
  if (!Object.prototype.hasOwnProperty.call(this, '_cachedUpdateFn')) {
861
861
  const modelClass = this;
862
- // query.update is EloquentBuilder.prototype.update a stable reference
863
- // captured once and reused for the lifetime of this model class.
864
- const protoUpdate = query.update;
862
+ // IMPORTANT: Do NOT capture `query.update` here. `query` is a Proxy, and
863
+ // accessing `.update` on a Proxy returns a new wrapper closure that hardcodes
864
+ // the specific builder instance (`target`) from that call. Caching that wrapper
865
+ // means all subsequent `.update()` calls would execute against the ORIGINAL
866
+ // builder's QueryBuilder (from the first call), ignoring the current query's
867
+ // WHERE clauses (e.g. the per-record `WHERE id = ?` clause would be lost).
868
+ //
869
+ // Instead, use `this.query.update(data)` directly. When _cachedUpdateFn is
870
+ // invoked, `this` is the current EloquentBuilder instance (set by the Proxy's
871
+ // get trap via `value.apply(target, args)`), so `this.query` is the correct
872
+ // underlying QueryBuilder with all accumulated WHERE clauses.
865
873
  this._cachedUpdateFn = async function timestampedUpdate(data) {
866
874
  if (data[modelClass.updatedAt] === undefined) {
867
875
  data[modelClass.updatedAt] = new Date();
868
876
  }
869
- return protoUpdate.call(this, data);
877
+ return this.query.update(data);
870
878
  };
871
879
  }
872
880
  query.update = this._cachedUpdateFn;
@@ -169,7 +169,13 @@ export function DataTable(props) {
169
169
  const [statusFilter, setStatusFilter] = React.useState(urlState.statusFilter);
170
170
  const [limit, setLimit] = React.useState(urlState.limit);
171
171
  const [columnSearch, setColumnSearch] = React.useState(urlState.columnSearch);
172
-
172
+ // Debounced version of columnSearch — API is only called once the user pauses typing
173
+ const [debouncedColumnSearch, setDebouncedColumnSearch] = React.useState(urlState.columnSearch);
174
+ const columnSearchDebounceRef = React.useRef(null);
175
+
176
+ // Abort controller for in-flight requests — cancelled whenever new fetch params arrive
177
+ const abortControllerRef = React.useRef(null);
178
+
173
179
  const [data, setData] = React.useState([]);
174
180
  const [loading, setLoading] = React.useState(false);
175
181
  const [totalPages, setTotalPages] = React.useState(1);
@@ -283,6 +289,22 @@ export function DataTable(props) {
283
289
  }
284
290
  }, [location?.search, hasReactRouter, loadStateFromURL]);
285
291
 
292
+ // Debounce columnSearch — user sees immediate input response, but API call waits 400ms
293
+ // Also syncs back if columnSearch is set programmatically (URL restore / browser back)
294
+ React.useEffect(() => {
295
+ if (columnSearchDebounceRef.current) {
296
+ clearTimeout(columnSearchDebounceRef.current);
297
+ }
298
+ columnSearchDebounceRef.current = setTimeout(() => {
299
+ setDebouncedColumnSearch(columnSearch);
300
+ }, 400);
301
+ return () => {
302
+ if (columnSearchDebounceRef.current) {
303
+ clearTimeout(columnSearchDebounceRef.current);
304
+ }
305
+ };
306
+ }, [columnSearch]);
307
+
286
308
  const handleStatusToggle = async (row) => {
287
309
  if (!toggleLink) return;
288
310
  try {
@@ -298,12 +320,19 @@ export function DataTable(props) {
298
320
  }
299
321
  };
300
322
 
301
- // Reset page to 1 when columnSearch changes
323
+ // Reset page to 1 when debouncedColumnSearch changes
302
324
  React.useEffect(() => {
303
325
  setPage(1);
304
- }, [columnSearch]);
326
+ }, [debouncedColumnSearch]);
305
327
 
306
328
  const fetchData = React.useCallback(async () => {
329
+ // Cancel any in-flight request before starting a new one
330
+ if (abortControllerRef.current) {
331
+ abortControllerRef.current.abort();
332
+ }
333
+ abortControllerRef.current = new AbortController();
334
+ const signal = abortControllerRef.current.signal;
335
+
307
336
  setLoading(true);
308
337
  try {
309
338
  const params = new URLSearchParams({
@@ -314,14 +343,14 @@ export function DataTable(props) {
314
343
  });
315
344
  if (statusFilter !== "all") params.append("isActive", statusFilter);
316
345
  if (search) params.append("search", search);
317
- // Add column search params
318
- Object.entries(columnSearch).forEach(([field, value]) => {
346
+ // Add debounced column search params
347
+ Object.entries(debouncedColumnSearch).forEach(([field, value]) => {
319
348
  if (value) params.append(`columnSearch[${field}]`, value);
320
349
  });
321
350
 
322
351
  // Properly append params to apiUrl (check if apiUrl already has query params)
323
352
  const separator = apiUrl.includes('?') ? '&' : '?';
324
- const result = await api.get(`${apiUrl}${separator}${params}`);
353
+ const result = await api.get(`${apiUrl}${separator}${params}`, { signal });
325
354
 
326
355
  // Handle nested data structure: result.data.data OR result.data.items
327
356
  const items = Array.isArray(result.data)
@@ -333,13 +362,16 @@ export function DataTable(props) {
333
362
  setTotalPages(pagination?.totalPages || 1);
334
363
  setTotalItems(pagination?.total || 0);
335
364
  } catch (err) {
365
+ // Ignore abort errors — they are expected when a newer request supersedes this one
366
+ if (err && err.name === 'AbortError') return;
367
+ if (err && err.code === 'ERR_CANCELED') return;
336
368
  setData([]);
337
369
  setTotalPages(1);
338
370
  setTotalItems(0);
339
371
  } finally {
340
372
  setLoading(false);
341
373
  }
342
- }, [api, apiUrl, page, sortBy, sortOrder, statusFilter, limit, search, columnSearch]);
374
+ }, [api, apiUrl, page, sortBy, sortOrder, statusFilter, limit, search, debouncedColumnSearch]);
343
375
 
344
376
  // Trigger fetchData for main params
345
377
  React.useEffect(() => {
@@ -360,6 +392,15 @@ export function DataTable(props) {
360
392
  }
361
393
  }, [refreshKey, fetchData]);
362
394
 
395
+ // Abort any in-flight request when the component unmounts
396
+ React.useEffect(() => {
397
+ return () => {
398
+ if (abortControllerRef.current) {
399
+ abortControllerRef.current.abort();
400
+ }
401
+ };
402
+ }, []);
403
+
363
404
  const handleSort = (field) => {
364
405
  if (sortBy === field) {
365
406
  setSortOrder((prev) => (prev === "asc" ? "desc" : "asc"));
package/jsconfig.json CHANGED
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "module": "esnext",
16
16
  "moduleResolution": "node",
17
+ "ignoreDeprecations": "6.0",
17
18
  "target": "es2020",
18
19
  "lib": ["es2020"],
19
20
  "allowSyntheticDefaultImports": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vasuzex",
3
- "version": "2.3.11",
3
+ "version": "2.3.13",
4
4
  "description": "Laravel-inspired framework for Node.js monorepos - V2 with optimized dependencies",
5
5
  "type": "module",
6
6
  "main": "./framework/index.js",
@@ -108,7 +108,7 @@
108
108
  "express-rate-limit": "^8.2.1",
109
109
  "firebase-admin": "^13.6.0",
110
110
  "fs-extra": "^11.3.2",
111
- "guruorm": "^2.1.23",
111
+ "guruorm": "^2.1.24",
112
112
  "helmet": "^8.1.0",
113
113
  "inquirer": "^9.3.8",
114
114
  "ip2location-nodejs": "^9.7.0",