@onehat/data 1.7.5 → 1.7.9

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.
@@ -525,6 +525,7 @@ describe('Entity', function() {
525
525
  entity.bar = 'Test';
526
526
  entity.markSaved();
527
527
  expect(entity.isTempId).to.be.false;
528
+ expect(entity.isStaged).to.be.false;
528
529
 
529
530
  expect(entity.isPersisted).to.be.true;
530
531
  const expected = {
@@ -557,6 +558,16 @@ describe('Entity', function() {
557
558
  this.entity.undelete();
558
559
  expect(this.entity.isDeleted).to.be.false;
559
560
  });
561
+
562
+ it('markStaged, stage', function() {
563
+ expect(this.entity.isStaged).to.be.false;
564
+
565
+ this.entity.stage();
566
+ expect(this.entity.isStaged).to.be.true;
567
+
568
+ this.entity.markStaged(false);
569
+ expect(this.entity.isStaged).to.be.false;
570
+ });
560
571
  });
561
572
 
562
573
  describe('events', function() {
@@ -650,8 +650,12 @@ describe('Repository Base', function() {
650
650
  it('getNonPersisted', async function() {
651
651
  this.repository.setAutoSave(false);
652
652
  const entity = await this.repository.add({ value: 'six' }),
653
- nonPersisted = this.repository.getNonPersisted();
653
+ entities = this.repository.entities;
654
654
 
655
+ let nonPersisted = this.repository.getNonPersisted();
656
+ expect(_.isEqual(entity, nonPersisted[0])).to.be.true;
657
+
658
+ nonPersisted = this.repository.getNonPersisted(entities);
655
659
  expect(_.isEqual(entity, nonPersisted[0])).to.be.true;
656
660
  });
657
661
 
@@ -690,6 +694,15 @@ describe('Repository Base', function() {
690
694
  expect(_.isEqual(entity, deleted[0])).to.be.true;
691
695
  });
692
696
 
697
+ it('getStaged', function() {
698
+ this.repository.setAutoSave(false);
699
+ const entity = this.repository.getByIx(0);
700
+
701
+ entity.isStaged = true;
702
+ const staged = this.repository.getStaged();
703
+ expect(_.isEqual(entity, staged[0])).to.be.true;
704
+ });
705
+
693
706
  it('isInRepository, hasId', function() {
694
707
  this.repository.setAutoSave(false);
695
708
  const id = 1,
@@ -720,6 +733,9 @@ describe('Repository Base', function() {
720
733
  this.repository.add({ value: 'six' });
721
734
 
722
735
  expect(didFire).to.be.true;
736
+
737
+ // TODO: Need comprehensive test of save(), not just event firing
738
+
723
739
  });
724
740
 
725
741
  it('save by entity', async function() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/data",
3
- "version": "1.7.5",
3
+ "version": "1.7.9",
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
@@ -117,6 +117,12 @@ class Entity extends EventEmitter {
117
117
  */
118
118
  this.isDeleted = false;
119
119
 
120
+ /**
121
+ * @member {boolean} isStaged - Whether this object has been marked for saving
122
+ * @private
123
+ */
124
+ this.isStaged = false;
125
+
120
126
  /**
121
127
  * @member {boolean} isDestroyed - Whether this object has been destroyed
122
128
  * @private
@@ -152,6 +158,7 @@ class Entity extends EventEmitter {
152
158
  initialize = () => {
153
159
  this.properties = this._createProperties();
154
160
  this._createMethods();
161
+ this._createStatics();
155
162
  this.reset();
156
163
  this.isInitialized = true;
157
164
  }
@@ -164,14 +171,30 @@ class Entity extends EventEmitter {
164
171
  if (this.isDestroyed) {
165
172
  throw Error('this._createMethods is no longer valid. Entity has been destroyed.');
166
173
  }
167
- const methodDefinitions = this.schema.entity.methods;
168
- if (!_.isEmpty(methodDefinitions)) {
169
- _.each(methodDefinitions, (method, name) => {
174
+ const methodsDefinitions = this.schema.entity.methods;
175
+ if (!_.isEmpty(methodsDefinitions)) {
176
+ _.each(methodsDefinitions, (method, name) => {
170
177
  this[name] = method; // NOTE: Methods must be defined in schema as "function() {}", not as "() => {}" so "this" will be assigned correctly
171
178
  });
172
179
  }
173
180
  }
174
181
 
182
+ /**
183
+ * Creates the static properties for this Entity, based on Schema.
184
+ * @private
185
+ */
186
+ _createStatics = () => {
187
+ if (this.isDestroyed) {
188
+ throw Error('this._createStatics is no longer valid. Entity has been destroyed.');
189
+ }
190
+ const staticsDefinitions = this.schema.entity.statics;
191
+ if (!_.isEmpty(staticsDefinitions)) {
192
+ _.each(staticsDefinitions, (value, key) => {
193
+ this[key] = value;
194
+ });
195
+ }
196
+ }
197
+
175
198
  /**
176
199
  * Generates a new unique id and assigns it to this entity.
177
200
  */
@@ -1057,6 +1080,7 @@ class Entity extends EventEmitter {
1057
1080
  this.getIdProperty().isTempId = false;
1058
1081
  this._originalData = this._getReconstructedOriginalData();
1059
1082
  this._originalDataParsed = this.getParsedValues();
1083
+ this.markStaged(false);
1060
1084
  }
1061
1085
 
1062
1086
  /**
@@ -1102,6 +1126,24 @@ class Entity extends EventEmitter {
1102
1126
  this.emit('undelete', this._proxy);
1103
1127
  }
1104
1128
 
1129
+ /**
1130
+ * Marks an entity as having been staged for saving.
1131
+ * @param {boolean} bool - How it should be marked. Defaults to true.
1132
+ */
1133
+ markStaged = (bool = true) => {
1134
+ if (this.isDestroyed) {
1135
+ throw Error('this.markStaged is no longer valid. Entity has been destroyed.');
1136
+ }
1137
+ this.isStaged = bool;
1138
+ }
1139
+
1140
+ /**
1141
+ * Convenience function.
1142
+ */
1143
+ stage = () => {
1144
+ this.markStaged(true);
1145
+ }
1146
+
1105
1147
  /**
1106
1148
  * Destroy this object.
1107
1149
  * - Removes all circular references to parent objects
@@ -834,6 +834,9 @@ class AjaxRepository extends Repository {
834
834
  * @private
835
835
  */
836
836
  _finalizeSave = (promises) => {
837
+ if (!promises.length) {
838
+ return;
839
+ }
837
840
  return this.axios.all(promises)
838
841
  .then(this.axios.spread((...batchOperationResults) => {
839
842
  // All requests are now complete
@@ -1135,52 +1135,120 @@ export default class Repository extends EventEmitter {
1135
1135
 
1136
1136
  /**
1137
1137
  * Get all phantom (unsaved) Entities
1138
+ * @param {array} entities - Array of entities to filter. Optional. Defaults to this.entities
1138
1139
  * @return {Entity[]} Array of phantom Entities, or []
1139
1140
  */
1140
- getPhantom = () => {
1141
+ getPhantom = (entities = null) => {
1141
1142
  if (this.isDestroyed) {
1142
1143
  throw Error('this.getPhantom is no longer valid. Repository has been destroyed.');
1143
1144
  }
1144
- return _.filter(this.entities, (entity) => entity.isPhantom);
1145
+ if (!entities) {
1146
+ entities = this.entities;
1147
+ }
1148
+ return _.filter(this.entities, entity => entity.isPhantom);
1145
1149
  }
1146
1150
 
1147
1151
  /**
1148
1152
  * Get all Entities not yet persisted to a storage medium
1153
+ * @param {array} entities - Array of entities to filter. Optional. Defaults to this.entities
1149
1154
  * @return {Entity[]} Array of dirty Entities, or []
1150
1155
  */
1151
- getNonPersisted = () => {
1156
+ getNonPersisted = (entities = null) => {
1152
1157
  if (this.isDestroyed) {
1153
1158
  throw Error('this.getDirty is no longer valid. Repository has been destroyed.');
1154
1159
  }
1155
- return _.filter(this.entities, (entity) => {
1156
- return !entity.isPersisted;
1157
- });
1160
+ if (!entities) {
1161
+ entities = this.entities;
1162
+ }
1163
+ return _.filter(this.entities, entity => !entity.isPersisted);
1158
1164
  }
1159
1165
 
1160
1166
  /**
1161
1167
  * Get all dirty (having unsaved changes) Entities
1168
+ * @param {array} entities - Array of entities to filter. Optional. Defaults to this.entities
1162
1169
  * @return {Entity[]} Array of dirty Entities, or []
1163
1170
  */
1164
- getDirty = () => {
1171
+ getDirty = (entities = null) => {
1165
1172
  if (this.isDestroyed) {
1166
1173
  throw Error('this.getDirty is no longer valid. Repository has been destroyed.');
1167
1174
  }
1168
- return _.filter(this.entities, (entity) => {
1169
- return entity.isDirty;
1170
- });
1175
+ if (!entities) {
1176
+ entities = this.entities;
1177
+ }
1178
+ return _.filter(entities, entity => entity.isDirty);
1171
1179
  }
1172
1180
 
1173
1181
  /**
1174
1182
  * Get all deleted Entities
1183
+ * @param {array} entities - Array of entities to filter. Optional. Defaults to this.entities
1175
1184
  * @return {Entity[]} Array of deleted Entities, or []
1176
1185
  */
1177
- getDeleted = () => {
1186
+ getDeleted = (entities = null) => {
1178
1187
  if (this.isDestroyed) {
1179
1188
  throw Error('this.getDeleted is no longer valid. Repository has been destroyed.');
1180
1189
  }
1181
- return _.filter(this.entities, (entity) => {
1182
- return entity.isDeleted;
1183
- });
1190
+ if (!entities) {
1191
+ entities = this.entities;
1192
+ }
1193
+ return _.filter(entities, entity => entity.isDeleted);
1194
+ }
1195
+
1196
+ /**
1197
+ * Get all staged Entities
1198
+ * @param {array} entities - Array of entities to filter. Optional. Defaults to this.entities
1199
+ * @return {Entity[]} Array of deleted Entities, or []
1200
+ */
1201
+ getStaged = (entities = null) => {
1202
+ if (this.isDestroyed) {
1203
+ throw Error('this.getStaged is no longer valid. Repository has been destroyed.');
1204
+ }
1205
+ if (!entities) {
1206
+ entities = this.entities;
1207
+ }
1208
+ return _.filter(entities, entity => entity.isStaged);
1209
+ }
1210
+
1211
+ /**
1212
+ * Gets the Schema object
1213
+ * @return {Schema} schema
1214
+ */
1215
+ getSchema = () => {
1216
+ if (this.isDestroyed) {
1217
+ throw Error('this.getSchema is no longer valid. Entity has been destroyed.');
1218
+ }
1219
+ return this.schema;
1220
+ }
1221
+
1222
+ /**
1223
+ * Gets the associated Repository
1224
+ * @param {string} repositoryName - Name of the Repository to retrieve
1225
+ * @return {boolean} hasProperty
1226
+ */
1227
+ getAssociatedRepository = (repositoryName) => {
1228
+ if (this.isDestroyed) {
1229
+ throw Error('this.getAssociatedRepository is no longer valid. Entity has been destroyed.');
1230
+ }
1231
+
1232
+ const schema = this.getSchema();
1233
+ if (!schema.model.associations.hasOne.includes(repositoryName) &&
1234
+ !schema.model.associations.hasMany.includes(repositoryName) &&
1235
+ !schema.model.associations.belongsTo.includes(repositoryName) &&
1236
+ !schema.model.associations.belongsToMany.includes(repositoryName)
1237
+ ) {
1238
+ throw Error(repositoryName + ' is not associated with this schema');
1239
+ }
1240
+
1241
+ const oneHatData = this.oneHatData;
1242
+ if (!oneHatData) {
1243
+ throw Error('No global oneHatData object');
1244
+ }
1245
+
1246
+ const associatedRepository = oneHatData.getRepository(repositoryName);
1247
+ if (!associatedRepository) {
1248
+ throw Error('Repository ' + repositoryName + ' cannot be found');
1249
+ }
1250
+
1251
+ return associatedRepository;
1184
1252
  }
1185
1253
 
1186
1254
  /**
@@ -1263,6 +1331,13 @@ export default class Repository extends EventEmitter {
1263
1331
  return this.isInRepository(id);
1264
1332
  }
1265
1333
 
1334
+ /**
1335
+ * Convenience function
1336
+ */
1337
+ saveStaged = () => {
1338
+ return this.save(null, true);
1339
+ }
1340
+
1266
1341
  /**
1267
1342
  * Queues up batch operations for saving
1268
1343
  * new, edited, and deleted entities to storage medium.
@@ -1271,8 +1346,9 @@ export default class Repository extends EventEmitter {
1271
1346
  * this.entities until all operations have completed. We leave it to subclasses
1272
1347
  * to implement that.
1273
1348
  * @param {object} entity - Optional single entity to save (instead of doing a batch operation on everything)
1349
+ * @param {boolean} useStaged - Save only the staged items, not all
1274
1350
  */
1275
- save = async (entity = null) => {
1351
+ save = async (entity = null, useStaged = false) => {
1276
1352
  if (this.isDestroyed) {
1277
1353
  throw Error('this.save is no longer valid. Repository has been destroyed.');
1278
1354
  }
@@ -1315,11 +1391,16 @@ export default class Repository extends EventEmitter {
1315
1391
  switch(operation) {
1316
1392
  case 'add':
1317
1393
  entities = this.getNonPersisted();
1394
+ if (useStaged) {
1395
+ entities = this.getStaged(entities);
1396
+ }
1318
1397
  if (_.size(entities) > 0) {
1319
1398
  if (this.combineBatch) {
1320
1399
 
1321
1400
  result = this.batchAsSynchronous ? await this._doBatchAdd(entities) : this._doBatchAdd(entities);
1322
- results.push(result);
1401
+ if (result) {
1402
+ results.push(result);
1403
+ }
1323
1404
 
1324
1405
  } else {
1325
1406
  for (i = 0; i < entities.length; i++) {
@@ -1332,18 +1413,25 @@ export default class Repository extends EventEmitter {
1332
1413
  }
1333
1414
 
1334
1415
  result = this.batchAsSynchronous ? await this._doAdd(entity) : this._doAdd(entity);
1335
- results.push(result);
1416
+ if (result) {
1417
+ results.push(result);
1418
+ }
1336
1419
  }
1337
1420
  }
1338
1421
  }
1339
1422
  break;
1340
1423
  case 'edit':
1341
1424
  entities = this.getDirty();
1425
+ if (useStaged) {
1426
+ entities = this.getStaged(entities);
1427
+ }
1342
1428
  if (_.size(entities) > 0) {
1343
1429
  if (this.combineBatch) {
1344
1430
 
1345
1431
  result = this.batchAsSynchronous ? await this._doBatchEdit(entities) : this._doBatchEdit(entities);
1346
- results.push(result);
1432
+ if (result) {
1433
+ results.push(result);
1434
+ }
1347
1435
 
1348
1436
  } else {
1349
1437
  for (i = 0; i < entities.length; i++) {
@@ -1356,18 +1444,25 @@ export default class Repository extends EventEmitter {
1356
1444
  }
1357
1445
 
1358
1446
  result = this.batchAsSynchronous ? await this._doEdit(entity) : this._doEdit(entity);
1359
- results.push(result);
1447
+ if (result) {
1448
+ results.push(result);
1449
+ }
1360
1450
  }
1361
1451
  }
1362
1452
  }
1363
1453
  break;
1364
1454
  case 'delete':
1365
1455
  entities = this.getDeleted();
1456
+ if (useStaged) {
1457
+ entities = this.getStaged(entities);
1458
+ }
1366
1459
  if (_.size(entities) > 0) {
1367
1460
  if (this.combineBatch) {
1368
1461
 
1369
1462
  result = this.batchAsSynchronous ? await this._doBatchDelete(entities) : this._doBatchDelete(entities);
1370
- results.push(result);
1463
+ if (result) {
1464
+ results.push(result);
1465
+ }
1371
1466
 
1372
1467
  } else {
1373
1468
  for (i = 0; i < entities.length; i++) {
@@ -1378,7 +1473,9 @@ export default class Repository extends EventEmitter {
1378
1473
  } else {
1379
1474
  result = this.batchAsSynchronous ? await this._doDelete(entity) : this._doDelete(entity);
1380
1475
  }
1381
- results.push(result);
1476
+ if (result) {
1477
+ results.push(result);
1478
+ }
1382
1479
  }
1383
1480
  }
1384
1481
  }
@@ -1520,7 +1617,7 @@ export default class Repository extends EventEmitter {
1520
1617
  * Mainly used for phantom Entities
1521
1618
  * Helper for delete()
1522
1619
  */
1523
- removeEntity = async (entity) => {
1620
+ removeEntity = async (entity) => {
1524
1621
  this.entities = _.filter(this.entities, e => e !== entity);
1525
1622
  entity.destroy();
1526
1623
  }
@@ -100,6 +100,7 @@ export default class Schema extends EventEmitter {
100
100
 
101
101
  entity: {
102
102
  methods: {}, // NOTE: Methods must be defined as "function() {}", not as "() => {}" so "this" will be assigned correctly
103
+ statics: {},
103
104
  },
104
105
 
105
106
  /**