@onehat/data 1.21.19 → 1.21.21

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.
@@ -1,6 +1,6 @@
1
1
  /** @module Repository */
2
2
 
3
- import Repository from './Repository.js';
3
+ import Repository from './Repository.js'; // so we can use static methods
4
4
  import ReaderTypes from '../Reader/index.js';
5
5
  import WriterTypes from '../Writer/index.js';
6
6
  import axios from 'axios';
@@ -100,6 +100,11 @@ class AjaxRepository extends Repository {
100
100
  * @private
101
101
  */
102
102
  isOnline: true,
103
+
104
+ /**
105
+ * @member {boolean} disableLimitToOnlyOneLoadRequest - If true, disables automatic cancellation of duplicate load requests
106
+ */
107
+ disableLimitToOnlyOneLoadRequest: false,
103
108
 
104
109
  };
105
110
  _.merge(this, defaults, config);
@@ -119,6 +124,12 @@ class AjaxRepository extends Repository {
119
124
  * @private
120
125
  */
121
126
  this._params = {};
127
+
128
+ /**
129
+ * @member {Map} _activeLoadRequests - Map of active requests keyed by URL
130
+ * @private
131
+ */
132
+ this._activeLoadRequests = new Map();
122
133
 
123
134
  this._operations = {
124
135
  add: false,
@@ -216,18 +227,23 @@ class AjaxRepository extends Repository {
216
227
  * @param {boolean} isBaseParam - Whether param is a base param (to be sent on every request).
217
228
  */
218
229
  setParam(name, value, isBaseParam = false) {
219
- const re = /^([^\[]+)\[([^\]]+)\](.*)$/,
230
+ const
231
+ re = /^([^\[]+)\[([^\]]+)\](.*)$/,
220
232
  matches = name.match(re),
221
233
  paramsToChange = isBaseParam ? this._baseParams : this._params;
222
234
 
223
235
  if (matches) { // name has array notation like 'conditions[username]'
224
- const first = matches[1],
236
+ const
237
+ first = matches[1],
225
238
  second = matches[2];
226
239
  if (paramsToChange && !paramsToChange.hasOwnProperty(first)) {
227
240
  paramsToChange[first] = {};
228
241
  }
229
242
  if (_.isNil(value) && paramsToChange[first] && paramsToChange[first].hasOwnProperty(second)) {
230
243
  delete paramsToChange[first][second];
244
+ if (_.isEmpty(paramsToChange[first])) {
245
+ delete paramsToChange[first];
246
+ }
231
247
  return;
232
248
  }
233
249
  paramsToChange[first][second] = value;
@@ -247,7 +263,8 @@ class AjaxRepository extends Repository {
247
263
  * @param {boolean} isBaseParam - Whether param is a base param (to be sent on every request).
248
264
  */
249
265
  setValuelessParam(name, isBaseParam = false) {
250
- const re = /^([^\[]+)\[([^\]]+)\](.*)$/,
266
+ const
267
+ re = /^([^\[]+)\[([^\]]+)\](.*)$/,
251
268
  matches = name.match(re),
252
269
  paramsToChange = isBaseParam ? this._baseParams : this._params;
253
270
 
@@ -284,7 +301,22 @@ class AjaxRepository extends Repository {
284
301
  * @param {string} name - Param name
285
302
  */
286
303
  hasBaseParam(name) {
287
- return this._baseParams.hasOwnProperty(name);
304
+ if (this._baseParams.hasOwnProperty(name)) {
305
+ return true;
306
+ }
307
+
308
+ // Check for array notation
309
+ const keys = name.split(/[\[\].]+/).filter(Boolean);
310
+ let current = this._baseParams,
311
+ key;
312
+
313
+ for(key of keys) {
314
+ if (!current || !current.hasOwnProperty(key)) {
315
+ return false;
316
+ }
317
+ current = current[key];
318
+ }
319
+ return true;
288
320
  }
289
321
 
290
322
  /**
@@ -295,7 +327,57 @@ class AjaxRepository extends Repository {
295
327
  if (!this.hasBaseParam(name)) {
296
328
  return null;
297
329
  }
298
- return this._baseParams[name];
330
+
331
+ // Handle simple property access
332
+ if (this._baseParams.hasOwnProperty(name)) {
333
+ return this._baseParams[name];
334
+ }
335
+
336
+ // Handle array notation like "conditions[fleets__enterprise_id]"
337
+ const keys = name.split(/[\[\].]+/).filter(Boolean);
338
+ let current = this._baseParams;
339
+
340
+ for(const key of keys) {
341
+ if (!current || !current.hasOwnProperty(key)) {
342
+ return null;
343
+ }
344
+ current = current[key];
345
+ }
346
+ return current;
347
+ }
348
+
349
+ /**
350
+ * Returns current value of base query params
351
+ * @param {object} params - Params to set. Key is parameter name, value is parameter value
352
+ */
353
+ getBaseParams() {
354
+ return this._baseParams;
355
+ }
356
+
357
+ /**
358
+ * Returns current value of any baseParam query conditions
359
+ */
360
+ getBaseParamConditions() {
361
+ const
362
+ existingConditions = this._baseParams.conditions || {},
363
+ convertedConditions = {};
364
+ _.each(existingConditions, (value, key) => {
365
+ convertedConditions['conditions[' + key + ']'] = value;
366
+ });
367
+ return convertedConditions;
368
+ }
369
+
370
+ /**
371
+ * Returns current value of any param query conditions
372
+ */
373
+ getParamConditions() {
374
+ const
375
+ existingConditions = this._params.conditions || {},
376
+ convertedConditions = {};
377
+ _.each(existingConditions, (value, key) => {
378
+ convertedConditions['conditions[' + key + ']'] = value;
379
+ });
380
+ return convertedConditions;
299
381
  }
300
382
 
301
383
  /**
@@ -303,7 +385,22 @@ class AjaxRepository extends Repository {
303
385
  * @param {string} name - Param name
304
386
  */
305
387
  hasParam(name) {
306
- return this._params.hasOwnProperty(name);
388
+ if (this._params.hasOwnProperty(name)) {
389
+ return true;
390
+ }
391
+
392
+ // Check for array notation
393
+ const keys = name.split(/[\[\].]+/).filter(Boolean);
394
+ let current = this._params,
395
+ key;
396
+
397
+ for(key of keys) {
398
+ if (!current || !current.hasOwnProperty(key)) {
399
+ return false;
400
+ }
401
+ current = current[key];
402
+ }
403
+ return true;
307
404
  }
308
405
 
309
406
  /**
@@ -380,7 +477,23 @@ class AjaxRepository extends Repository {
380
477
  this.setBaseParam(this.paramPageSize, this.isPaginated ? this.pageSize : null);
381
478
 
382
479
  if (this.isLoaded && !this.eventsPaused) {
383
- return this.reload();
480
+ const
481
+ currentEntityCount = this.entities.length,
482
+ newPageSize = this.pageSize;
483
+ if (this.previousPage === 1 && this.page === 1 && currentEntityCount >= newPageSize && this.isPaginated) {
484
+ // Optimization: if we're on page 1 and already have enough entities
485
+ // for the new page size, just truncate the existing data instead of reloading
486
+ const entitiesToRemove = this.entities.splice(newPageSize);
487
+ entitiesToRemove.forEach(entity => entity.destroy());
488
+ this._setPaginationVars();
489
+ this.emit('changeData', this.entities);
490
+ if (this.debugMode) {
491
+ console.log(`Truncated entities from ${currentEntityCount} to ${newPageSize}`);
492
+ }
493
+ } else {
494
+ // We need more data or weren't already on page 1, so reload
495
+ return this.reload();
496
+ }
384
497
  }
385
498
  }
386
499
 
@@ -399,7 +512,7 @@ class AjaxRepository extends Repository {
399
512
  * @fires beforeLoad,changeData,load,error
400
513
  */
401
514
  async load(params, callback = null) {
402
- if (this.isTree && this.loadRootNodes) {
515
+ if (this.isTree) {
403
516
  return this.loadRootNodes();
404
517
  }
405
518
  if (this.isDestroyed) {
@@ -410,6 +523,30 @@ class AjaxRepository extends Repository {
410
523
  this.throwError('No "get" api endpoint defined.');
411
524
  return;
412
525
  }
526
+
527
+
528
+
529
+ if (!_.isNil(params) && _.isObject(params)) {
530
+ this.setParams(params);
531
+ }
532
+ const
533
+ url = this.getModel() + '/' + this.api.get,
534
+ data = _.merge({}, this._baseParams, this._params),
535
+ requestKey = this._generateRequestKey(url, data);
536
+
537
+ if (!this.disableLimitToOnlyOneLoadRequest && !this.isUnique) {
538
+ if (this._activeLoadRequests.has(requestKey)) {
539
+ // Identical request already in progress, ignore this one
540
+ if (this.debugMode) {
541
+ console.log('Ignoring duplicate load request for', url, data);
542
+ }
543
+ return;
544
+ }
545
+
546
+ // Cancel any existing request for the same URL but different params
547
+ this._cancelExistingRequestsForUrl(url, requestKey);
548
+ }
549
+
413
550
  this.emit('beforeLoad'); // TODO: canceling beforeLoad will cancel the load operation
414
551
  this.markLoading();
415
552
 
@@ -425,14 +562,9 @@ class AjaxRepository extends Repository {
425
562
  this.resumeEvents();
426
563
  }
427
564
 
428
- if (!_.isNil(params) && _.isObject(params)) {
429
- this.setParams(params);
430
- }
431
-
432
565
  const repository = this;
433
- const data = _.merge({}, this._baseParams, this._params);
434
566
 
435
- return this._send(this.methods.get, this.api.get, data)
567
+ return this._send(this.methods.get, url, data, { isLoadRequest: true, requestKey, })
436
568
  .then(result => {
437
569
  if (this.debugMode) {
438
570
  console.log('Response for ' + this.name, result);
@@ -488,9 +620,69 @@ class AjaxRepository extends Repository {
488
620
  })
489
621
  .finally(() => {
490
622
  this.markLoading(false);
623
+ if (!this.disableLimitToOnlyOneLoadRequest && !this.isUnique && requestKey) {
624
+ this._activeLoadRequests.delete(requestKey);
625
+ }
491
626
  });
492
627
  }
493
628
 
629
+ /**
630
+ * Generates a unique key for a request based on URL and parameters
631
+ * @param {string} url - The request URL
632
+ * @param {object} data - The request parameters
633
+ * @return {string} requestKey - Unique key for this URL+params combination
634
+ * @private
635
+ */
636
+ _generateRequestKey(url, data) {
637
+ const sortedData = this._sortObjectDeep(data);
638
+ return url + '::' + JSON.stringify(sortedData);
639
+ }
640
+
641
+ /**
642
+ * Deep sorts an object by keys to ensure consistent hashing
643
+ * @param {object} obj - Object to sort
644
+ * @return {object} sortedObj - Object with keys sorted recursively
645
+ * @private
646
+ */
647
+ _sortObjectDeep(obj) {
648
+ if (_.isArray(obj)) {
649
+ return obj.map(item => this._sortObjectDeep(item));
650
+ } else if (_.isPlainObject(obj)) {
651
+ const sortedObj = {};
652
+ const sortedKeys = Object.keys(obj).sort();
653
+ for (const key of sortedKeys) {
654
+ sortedObj[key] = this._sortObjectDeep(obj[key]);
655
+ }
656
+ return sortedObj;
657
+ }
658
+ return obj;
659
+ }
660
+
661
+ /**
662
+ * Cancels any existing requests for the same URL but different params
663
+ * @param {string} url - The request URL
664
+ * @param {string} currentRequestKey - The current request key to exclude from cancellation
665
+ * @private
666
+ */
667
+ _cancelExistingRequestsForUrl(url, currentRequestKey) {
668
+ const keysToCancel = [];
669
+
670
+ // Find all requests for the same URL but different params
671
+ for (const [requestKey, requestInfo] of this._activeLoadRequests.entries()) {
672
+ if (requestKey !== currentRequestKey && requestKey.startsWith(url + '::')) {
673
+ keysToCancel.push(requestKey);
674
+ if (requestInfo.controller) {
675
+ requestInfo.controller.abort();
676
+ }
677
+ }
678
+ }
679
+
680
+ // Remove cancelled requests from tracking
681
+ keysToCancel.forEach(key => {
682
+ this._activeLoadRequests.delete(key);
683
+ });
684
+ }
685
+
494
686
  showMore(params = {}, callback) {
495
687
  params.showMore = true;
496
688
  return this.load(params, callback);
@@ -519,8 +711,10 @@ class AjaxRepository extends Repository {
519
711
  if (this.debugMode) {
520
712
  console.log('reloadEntity ' + entity.id, params);
521
713
  }
714
+
715
+ const url = entity.getModel() + '/' + this.api.get;
522
716
 
523
- return this._send(this.methods.get, this.api.get, params)
717
+ return this._send(this.methods.get, url, params)
524
718
  .then(result => {
525
719
  if (this.debugMode) {
526
720
  console.log('reloadEntity response ' + entity.id, result);
@@ -604,7 +798,7 @@ class AjaxRepository extends Repository {
604
798
 
605
799
  const
606
800
  method = this.methods.add,
607
- url = this.api.add,
801
+ url = entity.getModel() + '/' + this.api.add,
608
802
  data = entity.getSubmitValues();
609
803
 
610
804
  return this._send(method, url, data)
@@ -658,7 +852,7 @@ class AjaxRepository extends Repository {
658
852
 
659
853
  const
660
854
  method = this.methods.add,
661
- url = this.api.batchAdd,
855
+ url = this.getModel() + '/' + this.api.batchAdd,
662
856
  data = {
663
857
  entities: _.map(entities, entity => {
664
858
  const values = entity.submitValues;
@@ -723,7 +917,7 @@ class AjaxRepository extends Repository {
723
917
 
724
918
  const
725
919
  method = this.methods.edit,
726
- url = this.api.edit,
920
+ url = entity.getModel() + '/' + this.api.edit,
727
921
  data = entity.getSubmitValues();
728
922
 
729
923
  return this._send(method, url, data)
@@ -777,7 +971,7 @@ class AjaxRepository extends Repository {
777
971
 
778
972
  const
779
973
  method = this.methods.edit,
780
- url = this.api.batchEdit,
974
+ url = this.getModel() + '/' + this.api.batchEdit,
781
975
  data = {
782
976
  entities: _.map(entities, entity => {
783
977
  const values = entity.submitValues;
@@ -846,7 +1040,7 @@ class AjaxRepository extends Repository {
846
1040
 
847
1041
  const
848
1042
  method = this.methods.delete,
849
- url = this.api.delete,
1043
+ url = entity.getModel() + '/' + this.api.delete,
850
1044
  data = {
851
1045
  id: entity.id,
852
1046
  };
@@ -906,7 +1100,7 @@ class AjaxRepository extends Repository {
906
1100
 
907
1101
  const
908
1102
  method = this.methods.delete,
909
- url = this.api.batchDelete,
1103
+ url = this.getModel() + '/' + this.api.batchDelete,
910
1104
  ids = _.map(entities, (entity) => {
911
1105
  entity.isSaving = true;
912
1106
  return entity.id;
@@ -976,7 +1170,7 @@ class AjaxRepository extends Repository {
976
1170
  * Fires off axios request to server
977
1171
  * @private
978
1172
  */
979
- _send(method, url, data) {
1173
+ _send(method, url, data, options = {}) {
980
1174
 
981
1175
  if (!url) {
982
1176
  this.throwError('No url submitted');
@@ -988,12 +1182,19 @@ class AjaxRepository extends Repository {
988
1182
  return;
989
1183
  }
990
1184
 
1185
+ const controller = new AbortController();
1186
+ const { signal } = controller;
1187
+
1188
+ if (options.isLoadRequest && !this.disableLimitToOnlyOneLoadRequest && !this.isUnique && options.requestKey) {
1189
+ this._activeLoadRequests.set(options.requestKey, { controller });
1190
+ }
1191
+
991
1192
  const headers = _.merge({
992
1193
  'Content-Type': 'application/json',
993
1194
  'Accept': 'application/json',
994
- }, this.headers);
1195
+ }, this.headers, options.headers);
995
1196
 
996
- const options = {
1197
+ const axiosOptions = {
997
1198
  url,
998
1199
  method,
999
1200
  baseURL: this.api.baseURL,
@@ -1002,22 +1203,34 @@ class AjaxRepository extends Repository {
1002
1203
  params: method === 'GET' ? data : null,
1003
1204
  data: method !== 'GET' ? qs.stringify(data) : null,
1004
1205
  timeout: this.timeout,
1206
+ signal,
1005
1207
  };
1006
1208
 
1007
1209
  if (this.debugMode) {
1008
- console.log(url, options);
1210
+ console.log(url, axiosOptions);
1009
1211
  }
1010
1212
 
1011
- this.lastSendOptions = options;
1213
+ this.lastSendOptions = axiosOptions;
1012
1214
 
1013
- return this.axios(options)
1215
+ return this.axios(axiosOptions)
1014
1216
  .catch(error => {
1217
+ // Don't log or throw error if request was aborted
1218
+ if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
1219
+ return Promise.reject(new Error('Request cancelled'));
1220
+ }
1221
+
1015
1222
  if (this.debugMode) {
1016
1223
  console.log(url + ' error', error);
1017
1224
  console.log('response:', error.response);
1018
1225
  }
1019
1226
  this.throwError(error);
1020
1227
  return;
1228
+ })
1229
+ .finally(() => {
1230
+ // Clean up tracking for GET requests
1231
+ if (options.isLoadRequest && !this.disableLimitToOnlyOneLoadRequest && !this.isUnique && options.requestKey) {
1232
+ this._activeLoadRequests.delete(options.requestKey);
1233
+ }
1021
1234
  });
1022
1235
  }
1023
1236