@onehat/data 1.5.0 → 1.6.1

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.
@@ -44,7 +44,7 @@ describe('Entity', function() {
44
44
  expect(this.entity.id).to.be.eq(1);
45
45
  });
46
46
 
47
- it('createId, isTempId', function() {
47
+ it('createTempId, isTempId', function() {
48
48
  const schema = new Schema({
49
49
  name: 'baz',
50
50
  model: {
@@ -62,33 +62,14 @@ describe('Entity', function() {
62
62
  entity = new Entity(schema, data);
63
63
  entity.initialize();
64
64
  expect(entity.id).to.be.null;
65
+ expect(entity.isTempId).to.be.false;
65
66
 
66
- entity.createId();
67
+ entity.createTempId();
67
68
  expect(entity.id).to.be.not.null;
68
- expect(Entity.isTempId(entity.id)).to.be.true;
69
+ expect(entity.isTempId).to.be.true;
69
70
 
70
-
71
-
72
- const schema2 = new Schema({
73
- name: 'baz',
74
- model: {
75
- idProperty: 'foo',
76
- displayProperty: 'bar',
77
- properties: [
78
- { name: 'foo', type: 'uuid' }, // MOD
79
- { name: 'bar' },
80
- ],
81
- },
82
- }),
83
- entity2 = new Entity(schema2, data);
84
-
85
- entity2.initialize();
86
- expect(entity2.id).to.be.null;
87
-
88
- entity2.createId();
89
- expect(entity2.id).to.be.not.null;
90
- const idProperty = entity2.getIdProperty();
91
- expect(Entity.isTempId(entity2.id)).to.be.false;
71
+ entity.markSaved();
72
+ expect(entity.isTempId).to.be.false;
92
73
  });
93
74
 
94
75
 
@@ -301,9 +282,35 @@ describe('Entity', function() {
301
282
  expect(_.isEqual(reverseMapped2, expected2)).to.be.true;
302
283
  });
303
284
 
304
- it('getReverseMappedRawValues', function() {
305
- const result = this.entity.getReverseMappedRawValues();
306
- expect(_.isEqual(result, this.data)).to.be.true;
285
+ it('getReverseMappedRawValues (check deep matches)', function() {
286
+ const schema = new Schema({
287
+ name: 'baz',
288
+ model: {
289
+ idProperty: 'foo',
290
+ displayProperty: 'bar',
291
+ properties: [
292
+ { name: 'foo', type: 'int' },
293
+ { name: 'bar' },
294
+ { name: 'baz', mapping: 'baz.test.val', type: 'bool', defaultValue: null, },
295
+ { name: 'baz2', mapping: 'baz.test2', type: 'bool', defaultValue: null, },
296
+ ],
297
+ },
298
+ }),
299
+ data = {
300
+ foo: 1,
301
+ bar: 'one',
302
+ baz: {
303
+ test: {
304
+ val: true,
305
+ },
306
+ test2: false,
307
+ },
308
+ },
309
+ entity = new Entity(schema, data);
310
+ entity.initialize();
311
+
312
+ const result = entity.getReverseMappedRawValues();
313
+ expect(_.isEqual(result, data)).to.be.true;
307
314
  });
308
315
 
309
316
  it('build a new entity from an existing one using getDataForNewEntity', function() {
@@ -381,6 +388,12 @@ describe('Entity', function() {
381
388
  const entity = new Entity(this.schema, {});
382
389
  entity.initialize();
383
390
  expect(entity.isPhantom).to.be.true;
391
+
392
+ entity.createTempId();
393
+ expect(entity.isPhantom).to.be.true;
394
+
395
+ entity.markSaved();
396
+ expect(entity.isPhantom).to.be.false;
384
397
  });
385
398
 
386
399
  it('isDirty', function() {
@@ -443,6 +456,15 @@ describe('Entity', function() {
443
456
  expect(this.entity.isDirty).to.be.true;
444
457
  });
445
458
 
459
+ it('setId', function() {
460
+ expect(this.entity.foo).to.be.eq(1);
461
+ expect(this.entity.isTempId).to.be.false;
462
+
463
+ this.entity.setId(2);
464
+ expect(this.entity.foo).to.be.eq(2);
465
+ expect(this.entity.isTempId).to.be.false;
466
+ });
467
+
446
468
  it('_recalculateDependentProperties', function() {
447
469
  const schema = new Schema({
448
470
  name: 'baz',
@@ -482,6 +504,7 @@ describe('Entity', function() {
482
504
 
483
505
  entity.bar = 'Test';
484
506
  entity.markSaved();
507
+ expect(entity.isTempId).to.be.false;
485
508
 
486
509
  expect(entity.isPersisted).to.be.true;
487
510
  const expected = {
@@ -19,6 +19,14 @@ describe('IntegerProperty', function() {
19
19
  expect(className).to.be.eq('Integer');
20
20
  });
21
21
 
22
+ it('newId', function() {
23
+ const id1 = this.property.newId();
24
+ expect(id1.valueOf()).to.be.eq(this.property.idStartsAt);
25
+
26
+ const id2 = this.property.newId();
27
+ expect(id2.valueOf()).to.be.eq(this.property.idStartsAt +1);
28
+ });
29
+
22
30
  // it('default value', function() {
23
31
  // const property = this.property,
24
32
  // rawValue = property.getDefaultValue();
@@ -112,6 +112,20 @@ describe('Property', function() {
112
112
  it('getMapping', function() {
113
113
  expect(this.property.getMapping()).to.be.eq('id');
114
114
  });
115
+
116
+ it('getMapping', function() {
117
+ expect(this.property.getMapping()).to.be.eq('id');
118
+ });
119
+
120
+ it('isTempId', function() {
121
+ const definition = {
122
+ type: 'int',
123
+ isTempId: true,
124
+ },
125
+ Property = PropertyTypes[definition.type];
126
+ const property = new Property(definition);
127
+ expect(property.isTempId).to.be.true;
128
+ });
115
129
 
116
130
  });
117
131
 
@@ -10,9 +10,21 @@ describe('StringProperty', function() {
10
10
  this.property = new Property(definition);
11
11
  });
12
12
 
13
- it('className', function() {
14
- const className = this.property.getClassName();
15
- expect(className).to.be.eq('String');
13
+ describe('general', function() {
14
+
15
+ it('className', function() {
16
+ const className = this.property.getClassName();
17
+ expect(className).to.be.eq('String');
18
+ });
19
+
20
+ it('newId', function() {
21
+ const id1 = this.property.newId();
22
+ expect(id1.valueOf()).to.be.eq('TEMP-1');
23
+
24
+ const id2 = this.property.newId();
25
+ expect(id2.valueOf()).to.be.eq('TEMP-2');
26
+ });
27
+
16
28
  });
17
29
 
18
30
  describe('parse', function() {
@@ -511,6 +511,12 @@ describe('Repository Base', function() {
511
511
  expect(didFireAdd).to.be.true;
512
512
  });
513
513
 
514
+ it('createStandaloneEntity', async function() {
515
+ const entity = await this.repository.createStandaloneEntity({ key: 6, value: 'six' });
516
+ expect(entity.id).to.be.eq(6);
517
+ expect(_.size(this.repository.entities)).to.be.eq(5);
518
+ });
519
+
514
520
  it('addMultiple', async function() {
515
521
  await this.repository.addMultiple([
516
522
  { key: 6, value: 'six' },
@@ -575,6 +581,23 @@ describe('Repository Base', function() {
575
581
  expect(result.value).to.be.eq('three');
576
582
  });
577
583
 
584
+ it('getById', function() {
585
+ const result = this.repository.getById(3);
586
+ expect(result.value).to.be.eq('three');
587
+ });
588
+
589
+ it('getBy', function() {
590
+ const result = this.repository.getBy(entity => entity.id === 2 || entity.id === 3);
591
+ expect(result.length).to.be.eq(2);
592
+ expect(result[0].value).to.be.eq('two');
593
+ expect(result[1].value).to.be.eq('three');
594
+ });
595
+
596
+ it('getFirstBy', function() {
597
+ const result = this.repository.getFirstBy(entity => entity.id === 3);
598
+ expect(result.value).to.be.eq('three');
599
+ });
600
+
578
601
  it('getByRange', function() {
579
602
  const result = this.repository.getByRange(1, 4);
580
603
  expect(_.size(result)).to.be.eq(4);
@@ -616,11 +639,18 @@ describe('Repository Base', function() {
616
639
  expect(_.isEqual(entity, deleted[0])).to.be.true;
617
640
  });
618
641
 
619
- it('isInRepository', function() {
642
+ it('isInRepository, hasId', function() {
620
643
  this.repository.setAutoSave(false);
621
- const entity = this.repository.getByIx(0),
622
- result = this.repository.isInRepository(entity);
623
-
644
+ const id = 1,
645
+ entity = this.repository.getById(id);
646
+
647
+ let result = this.repository.isInRepository(entity);
648
+ expect(result).to.be.true;
649
+
650
+ result = this.repository.isInRepository(id);
651
+ expect(result).to.be.true;
652
+
653
+ result = this.repository.hasId(id);
624
654
  expect(result).to.be.true;
625
655
  });
626
656
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/data",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
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
@@ -4,8 +4,6 @@ import EventEmitter from '@onehat/events';
4
4
  import PropertyTypes from './Property';
5
5
  import _ from 'lodash';
6
6
 
7
- const TEMP_PREFIX = 'TEMP-';
8
-
9
7
  /**
10
8
  * Class represents a single Entity (i.e. a record)
11
9
  * which is a collection of Properties with current values.
@@ -158,31 +156,24 @@ class Entity extends EventEmitter {
158
156
 
159
157
  /**
160
158
  * Generates a new unique id and assigns it to this entity.
161
- * If the idProperty is of type 'uuid', then it generates a new UUID.
162
- * If not, then it generates a temp id.
163
159
  */
164
- createId = () => {
160
+ createTempId = () => {
165
161
  if (this.isDestroyed) {
166
- throw Error('this.generateTempId is no longer valid. Entity has been destroyed.');
162
+ throw Error('this.createTempId is no longer valid. Entity has been destroyed.');
167
163
  }
168
164
  if (!_.isNil(this.id)) {
169
165
  throw new Error('Entity id already exists.');
170
166
  }
171
- const idProperty = this.getIdProperty(),
172
- id = (idProperty.type === 'uuid') ? idProperty.newId() : _.uniqueId(TEMP_PREFIX);
173
- this.setId(id);
174
- }
175
167
 
176
- /**
177
- * Determines whether submitted id is a "temp" id.
178
- * @param {any} id
179
- * @return {boolean} isTempId
180
- */
181
- static isTempId(id) {
182
- if (_.isString(id) && id.match(new RegExp('^' + TEMP_PREFIX))) {
183
- return true;
168
+ const idProperty = this.getIdProperty();
169
+
170
+ if (!idProperty.newId) {
171
+ throw new Error('idProperty.newId() does not exist');
184
172
  }
185
- return false;
173
+
174
+ this.setId(idProperty.newId());
175
+
176
+ idProperty.isTempId = true;
186
177
  }
187
178
 
188
179
  /**
@@ -640,7 +631,7 @@ class Entity extends EventEmitter {
640
631
  let propertyValues = {};
641
632
  _.forOwn(this.properties, (property) => {
642
633
  const reverseMapped = Entity.getReverseMappedRawValue(property);
643
- _.assign(propertyValues, reverseMapped);
634
+ _.merge(propertyValues, reverseMapped);
644
635
  });
645
636
  return propertyValues;
646
637
  }
@@ -739,6 +730,14 @@ class Entity extends EventEmitter {
739
730
  return this.getId();
740
731
  }
741
732
 
733
+ /**
734
+ * Is this Entity's idProperty using a temporary ID?
735
+ * @return {boolean} isTempId
736
+ */
737
+ get isTempId() {
738
+ return this.getIdProperty().isTempId;
739
+ }
740
+
742
741
  /**
743
742
  * Gets the "Display" Property object for this Entity.
744
743
  * This is the Property whose value can easily identify the whole Entity itself.
@@ -793,13 +792,13 @@ class Entity extends EventEmitter {
793
792
  const idProperty = this.getIdProperty(),
794
793
  id = idProperty.getSubmitValue();
795
794
 
796
- // No id
795
+ // No ID
797
796
  if (_.isNil(id)) {
798
797
  return true;
799
798
  }
800
799
 
801
- // Temp id
802
- if (Entity.isTempId(id)) {
800
+ // ID is temporary
801
+ if (idProperty.isTempId) {
803
802
  return true;
804
803
  }
805
804
 
@@ -851,23 +850,25 @@ class Entity extends EventEmitter {
851
850
  */
852
851
  setId = (id, force = false) => {
853
852
  let isChanged = false;
854
- const property = this.getIdProperty();
853
+ const idProperty = this.getIdProperty();
855
854
 
856
- property.pauseEvents(); // We don't need property_change to fire
857
- if (property.setValue(id)) {
855
+ idProperty.pauseEvents(); // We don't need property_change to fire
856
+ if (idProperty.setValue(id)) {
858
857
  isChanged = true;
859
858
  }
860
- property.resumeEvents();
859
+ idProperty.resumeEvents();
861
860
 
862
861
  if (isChanged || force) {
863
862
  // Set this id on the _originalData* objects
864
- if (property.hasMapping) {
865
- _.merge(this._originalData, Entity.getReverseMappedRawValue(property));
863
+ if (idProperty.hasMapping) {
864
+ _.merge(this._originalData, Entity.getReverseMappedRawValue(idProperty));
866
865
  } else {
867
- this._originalData[property.name] = property.getRawValue();
866
+ this._originalData[idProperty.name] = idProperty.getRawValue();
868
867
  }
869
- this._originalDataParsed[property.name] = property.getParsedValue();
868
+ this._originalDataParsed[idProperty.name] = idProperty.getParsedValue();
870
869
  }
870
+
871
+ idProperty.isTempId = false;
871
872
 
872
873
  return isChanged;
873
874
  }
@@ -973,6 +974,7 @@ class Entity extends EventEmitter {
973
974
  throw Error('this.markSaved is no longer valid. Entity has been destroyed.');
974
975
  }
975
976
  this.isPersisted = true;
977
+ this.getIdProperty().isTempId = false;
976
978
  this._originalData = this._getReconstructedOriginalData();
977
979
  this._originalDataParsed = this.getParsedValues();
978
980
  }
@@ -4,6 +4,8 @@ import Property from './Property';
4
4
  import Parsers from '../Util/Parsers';
5
5
  import _ from 'lodash';
6
6
 
7
+ let lastId = 0;
8
+
7
9
  /**
8
10
  * Class represents a Property that stores an integer value.
9
11
  * @extends Property
@@ -15,6 +17,7 @@ export default class IntegerProperty extends Property {
15
17
 
16
18
  const defaults = {
17
19
  // defaultValue: 0,
20
+ idStartsAt: 100 * 1000 * 1000 * 1000, // 100 billion
18
21
  };
19
22
 
20
23
  _.merge(this, defaults, config);
@@ -29,6 +32,32 @@ export default class IntegerProperty extends Property {
29
32
  }
30
33
  return Parsers.ParseInt(value);
31
34
  }
35
+
36
+ /**
37
+ * Generates a new id
38
+ * Mainly for temporary, in-memory usage
39
+ */
40
+ newId = () => {
41
+ let id,
42
+ hasId = false;
43
+
44
+ const entity = this.getEntity(),
45
+ repository = entity && entity.repository;
46
+
47
+ if (lastId < this.idStartsAt) {
48
+ lastId = this.idStartsAt;
49
+ }
50
+
51
+ id = lastId++;
52
+ hasId = repository ? repository.hasId(id) : false;
53
+
54
+ while(hasId) {
55
+ id = lastId++;
56
+ hasId = repository.hasId(id);
57
+ }
58
+ return id;
59
+ }
60
+
32
61
 
33
62
  };
34
63
 
@@ -83,6 +83,12 @@ export default class Property extends EventEmitter {
83
83
  * @member {boolean} isSortable - Whether this property type is sortable
84
84
  */
85
85
  isSortable: true,
86
+
87
+ /**
88
+ * @member {boolean} isTempId - Whether this property's ID is temporary
89
+ */
90
+ isTempId: false,
91
+
86
92
 
87
93
 
88
94
  // OneBuild META attributes, just bring these over wholesale for now
@@ -411,7 +417,7 @@ export default class Property extends EventEmitter {
411
417
  */
412
418
  getMapping = () => {
413
419
  if (this.isDestroyed) {
414
- throw Error('this.getMappedName is no longer valid. Property has been destroyed.');
420
+ throw Error('this.getMapping is no longer valid. Property has been destroyed.');
415
421
  }
416
422
  return this.mapping;
417
423
  }
@@ -4,6 +4,8 @@ import Property from './Property';
4
4
  import Parsers from '../Util/Parsers';
5
5
  import _ from 'lodash';
6
6
 
7
+ const TEMP_PREFIX = 'TEMP-';
8
+
7
9
  /**
8
10
  * Class represents a Property that stores string data.
9
11
  * @extends Property
@@ -29,6 +31,23 @@ export default class StringProperty extends Property {
29
31
  }
30
32
  return Parsers.ParseString(value);
31
33
  }
34
+
35
+ newId = () => {
36
+ let id,
37
+ hasId = false;
38
+
39
+ const entity = this.getEntity(),
40
+ repository = entity && entity.repository;
41
+
42
+ id = _.uniqueId(TEMP_PREFIX);
43
+ hasId = repository ? repository.hasId(id) : false;
44
+
45
+ while(hasId) {
46
+ id = _.uniqueId(TEMP_PREFIX);
47
+ hasId = repository.hasId(id);
48
+ }
49
+ return id;
50
+ }
32
51
  };
33
52
 
34
53
  StringProperty.className = 'String';
@@ -395,7 +395,7 @@ class AjaxRepository extends Repository {
395
395
  });
396
396
 
397
397
  // Set the total records that pass filter
398
- this.total = total;
398
+ this.total = total;
399
399
  this._setPaginationVars();
400
400
 
401
401
  this.isLoading = false;
@@ -134,7 +134,7 @@ class MemoryRepository extends Repository {
134
134
  // Add to internal store
135
135
  _.each(entities, (entity) => {
136
136
  if (entity.isPhantom) {
137
- entity.createId();
137
+ entity.createTempId();
138
138
  }
139
139
  this._keyedEntities[entity.id] = entity;
140
140
  });
@@ -865,8 +865,8 @@ export default class Repository extends EventEmitter {
865
865
  this.entities.push(entity);
866
866
 
867
867
  // Create id if needed
868
- if (entity.isPhantom) {
869
- entity.createId(); // either a UUID or a temp id
868
+ if (entity.isPhantom) { // i.e. idProperty has no value
869
+ entity.createTempId();
870
870
  }
871
871
 
872
872
  this.emit('add', entity);
@@ -878,6 +878,27 @@ export default class Repository extends EventEmitter {
878
878
  return entity;
879
879
  }
880
880
 
881
+ /**
882
+ * Creates a new static Entity that does NOT persist in storage medium.
883
+ * @param {object} data - Either raw data object or Entity. If raw data, keys are Property names, Values are Property values.
884
+ * @param {boolean} isPersisted - Whether the new entity should be marked as already being persisted in storage medium.
885
+ * @param {boolean} originalIsMapped - Has data already been mapped according to schema?
886
+ * @return {object} entity - new Entity object
887
+ */
888
+ createStandaloneEntity = async (data, isPersisted = false, originalIsMapped = false) => {
889
+ if (this.isDestroyed) {
890
+ throw Error('this.createStandaloneEntity is no longer valid. Repository has been destroyed.');
891
+ }
892
+
893
+ const entity = Repository._createEntity(this.schema, data, this, isPersisted, originalIsMapped);
894
+
895
+ if (entity.isPhantom) {
896
+ entity.createTempId();
897
+ }
898
+
899
+ return entity;
900
+ }
901
+
881
902
  /**
882
903
  * Convenience function to add entity with mapped data.
883
904
  */
@@ -1043,20 +1064,19 @@ export default class Repository extends EventEmitter {
1043
1064
  /**
1044
1065
  * Get a single Entity by its id
1045
1066
  * @param {integer} id - id of record to retrieve
1046
- * @return {Entity} The Entity with matching id
1067
+ * @return {Entity} The Entity with matching id, or undefined
1047
1068
  */
1048
1069
  getById = (id) => {
1049
1070
  if (this.isDestroyed) {
1050
1071
  throw Error('this.getById is no longer valid. Repository has been destroyed.');
1051
1072
  }
1052
- const result = this.getBy((entity) => entity.id === id);
1053
- return result.length > 0 ? result[0] : null;
1073
+ return this.getFirstBy(entity => entity.id === id);
1054
1074
  }
1055
1075
 
1056
1076
  /**
1057
1077
  * Get an array of Entities by supplied filter function
1058
1078
  * @param {function} filter - Filter function to apply to all entities
1059
- * @return {Entity[]} Entities that passed through filter
1079
+ * @return {Entity[]} Entities that passed through filter, or []
1060
1080
  */
1061
1081
  getBy = (filter) => {
1062
1082
  if (this.isDestroyed) {
@@ -1072,7 +1092,7 @@ export default class Repository extends EventEmitter {
1072
1092
  * filters into account. Defaults to false.
1073
1093
  *
1074
1094
  * @param {function} filter - Filter function to search by
1075
- * @return {Entity} First Entity found
1095
+ * @return {Entity} First Entity found, or undefined
1076
1096
  */
1077
1097
  getFirstBy = (filter) => {
1078
1098
  if (this.isDestroyed) {
@@ -1083,7 +1103,7 @@ export default class Repository extends EventEmitter {
1083
1103
 
1084
1104
  /**
1085
1105
  * Get all phantom (unsaved) Entities
1086
- * @return {Entity[]} Array of phantom Entities
1106
+ * @return {Entity[]} Array of phantom Entities, or []
1087
1107
  */
1088
1108
  getPhantom = () => {
1089
1109
  if (this.isDestroyed) {
@@ -1094,7 +1114,7 @@ export default class Repository extends EventEmitter {
1094
1114
 
1095
1115
  /**
1096
1116
  * Get all Entities not yet persisted to a storage medium
1097
- * @return {Entity[]} Array of dirty Entities
1117
+ * @return {Entity[]} Array of dirty Entities, or []
1098
1118
  */
1099
1119
  getNonPersisted = () => {
1100
1120
  if (this.isDestroyed) {
@@ -1107,7 +1127,7 @@ export default class Repository extends EventEmitter {
1107
1127
 
1108
1128
  /**
1109
1129
  * Get all dirty (having unsaved changes) Entities
1110
- * @return {Entity[]} Array of dirty Entities
1130
+ * @return {Entity[]} Array of dirty Entities, or []
1111
1131
  */
1112
1132
  getDirty = () => {
1113
1133
  if (this.isDestroyed) {
@@ -1120,7 +1140,7 @@ export default class Repository extends EventEmitter {
1120
1140
 
1121
1141
  /**
1122
1142
  * Get all deleted Entities
1123
- * @return {Entity[]} Array of deleted Entities
1143
+ * @return {Entity[]} Array of deleted Entities, or []
1124
1144
  */
1125
1145
  getDeleted = () => {
1126
1146
  if (this.isDestroyed) {
@@ -1147,6 +1167,15 @@ export default class Repository extends EventEmitter {
1147
1167
  return !_.isNil(this.getById(idOrEntity));
1148
1168
  }
1149
1169
 
1170
+ /**
1171
+ * Convenience function
1172
+ * Alias for isInRepository
1173
+ * NOTE: It only searches in memory. Doesn't query server
1174
+ */
1175
+ hasId = (id) => {
1176
+ return this.isInRepository(id);
1177
+ }
1178
+
1150
1179
  /**
1151
1180
  * Queues up batch operations for saving
1152
1181
  * new, edited, and deleted entities to storage medium.