@onehat/data 1.7.14 → 1.8.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.
@@ -476,15 +476,40 @@ describe('Entity', function() {
476
476
  it('setValues', function() {
477
477
  expect(this.entity.foo).to.be.eq(1);
478
478
  expect(this.entity.bar).to.be.eq('one');
479
+ expect(this.entity.baz).to.be.eq(true);
479
480
  expect(this.entity.isDirty).to.be.false;
480
481
 
481
482
  this.entity.setValues({
482
483
  foo: 2,
483
484
  bar: 'two',
485
+ baz: false,
484
486
  });
485
487
 
486
488
  expect(this.entity.foo).to.be.eq(2);
487
489
  expect(this.entity.bar).to.be.eq('two');
490
+ expect(this.entity.baz).to.be.eq(false);
491
+ expect(this.entity.isDirty).to.be.true;
492
+ });
493
+
494
+ it('setRawValues', function() {
495
+ expect(this.entity.foo).to.be.eq(1);
496
+ expect(this.entity.bar).to.be.eq('one');
497
+ expect(this.entity.baz).to.be.eq(true);
498
+ expect(this.entity.isDirty).to.be.false;
499
+
500
+ this.entity.setRawValues({
501
+ foo: 2,
502
+ bar: 'two',
503
+ baz: {
504
+ test: {
505
+ val: false,
506
+ },
507
+ },
508
+ });
509
+
510
+ expect(this.entity.foo).to.be.eq(2);
511
+ expect(this.entity.bar).to.be.eq('two');
512
+ expect(this.entity.baz).to.be.eq(false);
488
513
  expect(this.entity.isDirty).to.be.true;
489
514
  });
490
515
 
@@ -580,6 +605,21 @@ describe('Entity', function() {
580
605
  this.entity.markStaged(false);
581
606
  expect(this.entity.isStaged).to.be.false;
582
607
  });
608
+
609
+ it('setValue changed lastModified', function() {
610
+ let earlyLastModified,
611
+ lateLastModified;
612
+
613
+ earlyLastModified = this.entity.lastModified;
614
+ this.entity.setValue('foo', '125');
615
+ lateLastModified = this.entity.lastModified;
616
+ expect(earlyLastModified < lateLastModified).to.be.true;
617
+
618
+ earlyLastModified = this.entity.lastModified;
619
+ this.entity.foo = '126';
620
+ lateLastModified = this.entity.lastModified;
621
+ expect(earlyLastModified < lateLastModified).to.be.true;
622
+ });
583
623
  });
584
624
 
585
625
  describe('events', function() {
@@ -553,6 +553,30 @@ describe('Repository Base', function() {
553
553
  expect(didFireAdd).to.be.true;
554
554
  });
555
555
 
556
+ it('add already existing', async function() {
557
+ const value = 'another one';
558
+ await this.repository.add({ key: 6, value: 'six' });
559
+ await this.repository.add({ key: 6, value });
560
+ expect(_.size(this.repository.entities)).to.be.eq(6);
561
+
562
+ const entity = this.repository.getById(6);
563
+ expect(entity.value).to.be.eq(value);
564
+ })
565
+
566
+ it('add with an existing id', async function() {
567
+ this.repository.autoSave = false;
568
+
569
+ // ID suppied; should not be temp ID or phantom
570
+ const entity = await this.repository.add({ key: 6, value: 'six' });
571
+ expect(entity.isTempId).to.be.false;
572
+ expect(entity.isPhantom).to.be.false;
573
+
574
+ // No ID suppled. Make it phantom and temp ID
575
+ const entity2 = await this.repository.add({ value: 'seven' });
576
+ expect(entity2.isTempId).to.be.true;
577
+ expect(entity2.isPhantom).to.be.true;
578
+ });
579
+
556
580
  it('createStandaloneEntity', async function() {
557
581
  const entity = await this.repository.createStandaloneEntity({ key: 6, value: 'six' });
558
582
  expect(entity.id).to.be.eq(6);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/data",
3
- "version": "1.7.14",
3
+ "version": "1.8.0",
4
4
  "description": "JS data modeling package with adapters for many storage mediums.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/Entity.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import EventEmitter from '@onehat/events';
4
4
  import PropertyTypes from './Property';
5
+ import moment from 'moment';
5
6
  import _ from 'lodash';
6
7
 
7
8
  /**
@@ -129,6 +130,11 @@ class Entity extends EventEmitter {
129
130
  */
130
131
  this.isDestroyed = false;
131
132
 
133
+ /**
134
+ * @member {string} lastModified - Last time this entity was modified
135
+ */
136
+ this.lastModified = null;
137
+
132
138
  // This ES6 Proxy allows us to create magic getters and setters for all property values.
133
139
  // However, these getters and setters are *not* available within the Entity itself.
134
140
  this._proxy = new Proxy(this, {
@@ -325,6 +331,7 @@ class Entity extends EventEmitter {
325
331
  if (this.isDeleted) {
326
332
  this.undelete();
327
333
  }
334
+ this.setLastModified();
328
335
 
329
336
  this.emit('reset', this._proxy);
330
337
  }
@@ -399,6 +406,10 @@ class Entity extends EventEmitter {
399
406
  }
400
407
  return value;
401
408
  }
409
+
410
+ setLastModified = () => {
411
+ this.lastModified = moment(new Date()).format('YYYY-MM-DD HH:mm:ss.SSSS');
412
+ }
402
413
 
403
414
 
404
415
  // ______ __ __
@@ -990,6 +1001,7 @@ class Entity extends EventEmitter {
990
1001
  }
991
1002
 
992
1003
  idProperty.isTempId = false;
1004
+ this.setLastModified();
993
1005
 
994
1006
  return isChanged;
995
1007
  }
@@ -1010,23 +1022,58 @@ class Entity extends EventEmitter {
1010
1022
  propertyValues[propertyName] = rawValue;
1011
1023
  return this.setValues(propertyValues);
1012
1024
  }
1025
+
1026
+ /**
1027
+ * Sets Property values
1028
+ * @param {object} rawData - Raw data object. These are prior to mapping,
1029
+ * similar to what you'd use to create a brand new Entity. Make sure *all*
1030
+ * values are here, not just a few.
1031
+ * @return {boolean} isChanged - Whether any values were actually changed
1032
+ */
1033
+ setRawValues = (rawData) => {
1034
+ if (this.isDestroyed) {
1035
+ throw Error('this.setRawValues is no longer valid. Entity has been destroyed.');
1036
+ }
1037
+
1038
+ const [dependentProperties, nonDependentProperties] = _.partition(this.properties, (property) => {
1039
+ return property.hasDepends;
1040
+ });
1041
+ const mappedData = {};
1042
+ function setMappedValue(property) {
1043
+ let rawValue;
1044
+ if (property.hasMapping) {
1045
+ rawValue = Entity.getMappedValue(property.mapping, rawData);
1046
+ } else {
1047
+ rawValue = rawData[property.name];
1048
+ }
1049
+ if (_.isNil(rawValue)) {
1050
+ rawValue = property.getDefaultValue();
1051
+ }
1052
+ mappedData[property.name] = rawValue;
1053
+ }
1054
+
1055
+ _.each(nonDependentProperties, setMappedValue);
1056
+ _.each(dependentProperties, setMappedValue);
1057
+
1058
+ return this.setValues(mappedData);
1059
+ }
1013
1060
 
1014
1061
  /**
1015
1062
  * Sets Property values
1016
- * @param {object} rawData - Raw data object. Keys are Property names, Values are Property values.
1063
+ * @param {object} data - Raw data object. Keys are Property names, Values are Property values.
1017
1064
  * @return {boolean} isChanged - Whether any values were actually changed
1018
1065
  * @fires change
1019
1066
  */
1020
- setValues = (rawData) => {
1067
+ setValues = (data) => {
1021
1068
  if (this.isDestroyed) {
1022
1069
  throw Error('this.setValues is no longer valid. Entity has been destroyed.');
1023
1070
  }
1024
- if (_.indexOf(rawData, this.getIdProperty().name) !== -1) {
1071
+ if (_.indexOf(data, this.getIdProperty().name) !== -1) {
1025
1072
  throw new Error('Cannot change id via entity.setValues(). Must use entity.setId() first.');
1026
1073
  }
1027
1074
 
1028
1075
  let isChanged = false;
1029
- _.each(rawData, (value, propertyName) => {
1076
+ _.each(data, (value, propertyName) => {
1030
1077
  const property = this.getProperty(propertyName);
1031
1078
  property.pauseEvents(); // We don't need property_change to fire
1032
1079
  if (property.setValue(value)) {
@@ -1034,6 +1081,7 @@ class Entity extends EventEmitter {
1034
1081
  }
1035
1082
  property.resumeEvents();
1036
1083
  });
1084
+ this.setLastModified();
1037
1085
 
1038
1086
  if (isChanged) {
1039
1087
  this._recalculateDependentProperties();
@@ -362,7 +362,7 @@ class AjaxRepository extends Repository {
362
362
  throw new Error('No "get" api endpoint defined.');
363
363
  }
364
364
  this.emit('beforeLoad'); // TODO: canceling beforeLoad will cancel the load operation
365
- this.isLoading = true;
365
+ this.markLoading();
366
366
 
367
367
 
368
368
  if (!_.isNil(params) && _.isObject(params)) {
@@ -407,9 +407,9 @@ class AjaxRepository extends Repository {
407
407
  // Set the total records that pass filter
408
408
  this.total = total;
409
409
  this._setPaginationVars();
410
-
411
- this.isLoading = false;
412
- this.isLoaded = true;
410
+
411
+ this.markLoaded();
412
+
413
413
  this.emit('changeData', this.entities);
414
414
  this.emit('load', this);
415
415
  })
@@ -432,7 +432,7 @@ class AjaxRepository extends Repository {
432
432
  throw new Error('No "get" api endpoint defined.');
433
433
  }
434
434
  this.emit('beforeLoad'); // TODO: canceling beforeLoad will cancel the load operation
435
- this.isLoading = true;
435
+ this.markLoading();
436
436
 
437
437
  const params = this._getReloadEntityParams(entity);
438
438
  if (this.debugMode) {
@@ -460,8 +460,8 @@ class AjaxRepository extends Repository {
460
460
  entity.loadOriginalData(updatedData, true);
461
461
  entity.emit('reload', entity);
462
462
 
463
- this.isLoading = false;
464
- this.isLoaded = true;
463
+ this.markLoaded();
464
+
465
465
  this.emit('changeData', this.entities);
466
466
  this.emit('load', this);
467
467
  this.emit('reloadEntity', entity);
@@ -95,7 +95,7 @@ class MemoryRepository extends Repository {
95
95
  }
96
96
 
97
97
  this.emit('beforeLoad');
98
- this.isLoading = true;
98
+ this.markLoading();
99
99
 
100
100
 
101
101
  const isDirectLoad = !_.isNil(data);
@@ -143,8 +143,7 @@ class MemoryRepository extends Repository {
143
143
  await this._saveToStorage(entities);
144
144
  }
145
145
 
146
- this.isLoading = false;
147
- this.isLoaded = true;
146
+ this.markLoaded();
148
147
  this._recalculate(); // fires changeData if needed
149
148
  this.emit('load', entities, this);
150
149
  return entities;
@@ -444,6 +443,21 @@ class MemoryRepository extends Repository {
444
443
  })
445
444
  }
446
445
  /* */
446
+
447
+ removeEntity(entity) { // standard function notation
448
+ const id = entity.id;
449
+
450
+ super.removeEntity(entity);
451
+
452
+ if (this.hasSorters) {
453
+ this._applySorters();
454
+ }
455
+ if (this.hasFilters) {
456
+ this._applyFilters();
457
+ }
458
+ delete this._keyedEntities[id];
459
+ }
460
+
447
461
 
448
462
 
449
463
 
@@ -46,7 +46,7 @@ class NullRepository extends Repository {
46
46
  throw Error('this.load is no longer valid. Repository has been destroyed.');
47
47
  }
48
48
  this.emit('beforeLoad');
49
- this.isLoading = true;
49
+ this.markLoading();
50
50
 
51
51
  if (data) {
52
52
  let entities = data;
@@ -65,8 +65,7 @@ class NullRepository extends Repository {
65
65
 
66
66
  this._updateSize();
67
67
 
68
- this.isLoading = false;
69
- this.isLoaded = true;
68
+ this.markLoaded();
70
69
  this.emit('changeData', this.entities);
71
70
  this.emit('load', this);
72
71
  }
@@ -5,6 +5,7 @@ import Entity from '../Entity';
5
5
  import {
6
6
  v4 as uuid,
7
7
  } from 'uuid';
8
+ import moment from 'moment';
8
9
  import _ from 'lodash';
9
10
 
10
11
  /**
@@ -194,6 +195,11 @@ export default class Repository extends EventEmitter {
194
195
  */
195
196
  this.isLoading = false;
196
197
 
198
+ /**
199
+ * @member {string} lastLoaded - Last time this repository was loaded
200
+ */
201
+ this.lastLoaded = null;
202
+
197
203
  /**
198
204
  * @member {Boolean} isSaving - State: whether or not entities are currently being saved
199
205
  */
@@ -268,8 +274,9 @@ export default class Repository extends EventEmitter {
268
274
  this._createMethods();
269
275
  this._createStatics();
270
276
 
271
- if (this.schema.repository.init) {
272
- await this.schema.repository.init.call(this);
277
+ const init = this.schema.repository.init || this.originalConfig.init; // The latter is mainly for lfr repositories
278
+ if (init) {
279
+ await init.call(this);
273
280
  }
274
281
 
275
282
  this.isInitialized = true;
@@ -284,7 +291,7 @@ export default class Repository extends EventEmitter {
284
291
  if (this.isDestroyed) {
285
292
  throw Error('this._createMethods is no longer valid. Repository has been destroyed.');
286
293
  }
287
- const methodDefinitions = this.schema.repository.methods;
294
+ const methodDefinitions = this.schema.repository.methods || this.originalConfig.methods; // The latter is mainly for lfr repositories
288
295
  if (!_.isEmpty(methodDefinitions)) {
289
296
  _.each(methodDefinitions, (method, name) => {
290
297
  this[name] = method; // NOTE: Methods must be defined in schema as "function() {}", not as "() => {}" so "this" will be assigned correctly
@@ -300,7 +307,7 @@ export default class Repository extends EventEmitter {
300
307
  if (this.isDestroyed) {
301
308
  throw Error('this._createStatics is no longer valid. Entity has been destroyed.');
302
309
  }
303
- const staticsDefinitions = this.schema.repository.statics;
310
+ const staticsDefinitions = this.schema.repository.statics || this.originalConfig.statics; // The latter is mainly for lfr repositories
304
311
  if (!_.isEmpty(staticsDefinitions)) {
305
312
  _.each(staticsDefinitions, (value, key) => {
306
313
  this[key] = value;
@@ -323,6 +330,22 @@ export default class Repository extends EventEmitter {
323
330
  throw new Error('load must be implemented by Repository subclass');
324
331
  }
325
332
 
333
+ /**
334
+ * Marks this repository as loading
335
+ */
336
+ markLoading = () => {
337
+ this.isLoading = true;
338
+ }
339
+
340
+ /**
341
+ * Marks this repository as loaded
342
+ */
343
+ markLoaded = () => {
344
+ this.isLoading = false;
345
+ this.isLoaded = true;
346
+ this.lastLoaded = moment(new Date()).format('YYYY-MM-DD HH:mm:ss.SSSS');
347
+ }
348
+
326
349
  /**
327
350
  * Reload data from storage medium, using previous settings.
328
351
  * Subclasses may override this to provide additional
@@ -900,6 +923,19 @@ export default class Repository extends EventEmitter {
900
923
  throw Error('this.add is no longer valid. Repository has been destroyed.');
901
924
  }
902
925
 
926
+ // Does it already exist? If so, edit the existing
927
+ const idProperty = this.getSchema().model.idProperty;
928
+ if (data.hasOwnProperty(idProperty)) {
929
+ if (this.isInRepository(data[idProperty])) {
930
+ const existing = this.getById(data[idProperty]);
931
+ existing.setRawValues(data);
932
+ if (this.autoSave && !existing.isPersisted) {
933
+ await this.save(existing);
934
+ }
935
+ return existing;
936
+ }
937
+ }
938
+
903
939
  let entity = data;
904
940
  if (!(data instanceof Entity)) {
905
941
  // Create the new entity
@@ -1595,7 +1631,7 @@ export default class Repository extends EventEmitter {
1595
1631
  * Mainly used for phantom Entities
1596
1632
  * Helper for delete()
1597
1633
  */
1598
- removeEntity = async (entity) => {
1634
+ removeEntity(entity) { // standard function notation so it can be called by child class
1599
1635
  this.entities = _.filter(this.entities, e => e !== entity);
1600
1636
  entity.destroy();
1601
1637
  }