@onehat/data 1.23.0 → 1.23.2

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.
@@ -28,6 +28,7 @@ async function beforeEach(that) {
28
28
  }, true);
29
29
  that.repository = that.oneHatData.getRepositoryById('foo');
30
30
  }
31
+
31
32
  function afterEach(that) {
32
33
  that.oneHatData.destroy();
33
34
  }
@@ -267,8 +268,130 @@ describe('OneHatData', function() {
267
268
 
268
269
  const
269
270
  repo1 = that.oneHatData.getRepository('bar'),
270
- repo2 = that.oneHatData.getRepository('bar', true);
271
+ repo2 = await that.oneHatData.getUniqueRepository('bar');
271
272
  expect(repo1 !== repo2).to.be.true;
273
+ expect(repo2.isInitialized).to.be.true;
274
+
275
+ afterEach(that);
276
+ })();
277
+ });
278
+
279
+ it('getRepository(name, true) throws migration error', function() {
280
+ (async () => {
281
+ const that = {};
282
+ await beforeEach(that);
283
+
284
+ expect(() => that.oneHatData.getRepository('bar', true)).to.throw('Use await this.getUniqueRepository(name) instead.');
285
+
286
+ afterEach(that);
287
+ })();
288
+ });
289
+
290
+ it('getUniqueRepository returns initialized unique repository', function() {
291
+ (async () => {
292
+ const that = {};
293
+ await beforeEach(that);
294
+
295
+ const repository = await that.oneHatData.getUniqueRepository('bar');
296
+
297
+ expect(repository).to.be.ok;
298
+ expect(repository.isUnique).to.be.true;
299
+ expect(repository.isInitialized).to.be.true;
300
+
301
+ afterEach(that);
302
+ })();
303
+ });
304
+
305
+ it('getRepository unique keeps filters isolated from bound repository', function() {
306
+ (async () => {
307
+ const that = {};
308
+ await beforeEach(that);
309
+
310
+ const
311
+ boundRepository = that.oneHatData.getRepository('bar'),
312
+ uniqueRepository = await that.oneHatData.getUniqueRepository('bar');
313
+
314
+ boundRepository.filter('key', 'bound-only');
315
+ expect(boundRepository.hasFilterValue('key', 'bound-only')).to.be.true;
316
+ expect(uniqueRepository.hasFilter('key')).to.be.false;
317
+
318
+ uniqueRepository.filter('key', 'unique-only');
319
+ expect(uniqueRepository.hasFilterValue('key', 'unique-only')).to.be.true;
320
+ expect(boundRepository.hasFilterValue('key', 'bound-only')).to.be.true;
321
+ expect(boundRepository.hasFilterValue('key', 'unique-only')).to.be.false;
322
+
323
+ afterEach(that);
324
+ })();
325
+ });
326
+
327
+ it('getUniqueRepository allows setBaseParams for Ajax repositories', function() {
328
+ (async () => {
329
+ const that = {};
330
+ await beforeEach(that);
331
+
332
+ that.oneHatData.createSchema({
333
+ name: 'meters',
334
+ model: {
335
+ idProperty: 'id',
336
+ displayProperty: 'name',
337
+ properties: [
338
+ { name: 'id' },
339
+ { name: 'name' },
340
+ ],
341
+ },
342
+ repository: {
343
+ type: 'ajax',
344
+ api: {
345
+ get: 'meters',
346
+ },
347
+ },
348
+ });
349
+ await that.oneHatData.createRepository('meters', true);
350
+
351
+ const uniqueRepository = await that.oneHatData.getUniqueRepository('meters');
352
+
353
+ expect(() => {
354
+ uniqueRepository.setBaseParams({
355
+ foo: 'bar',
356
+ });
357
+ }).to.not.throw();
358
+
359
+ expect(uniqueRepository.getBaseParam('foo')).to.be.eq('bar');
360
+
361
+ afterEach(that);
362
+ })();
363
+ });
364
+
365
+ it('getOrCreateUniqueRepository reuses existing mapped repository', function() {
366
+ (async () => {
367
+ const that = {};
368
+ await beforeEach(that);
369
+
370
+ const repository1 = await that.oneHatData.getOrCreateUniqueRepository('partsMap', 'bar');
371
+ const repository2 = await that.oneHatData.getOrCreateUniqueRepository('partsMap', 'bar');
372
+
373
+ expect(repository1).to.be.eq(repository2);
374
+ expect(repository1.isUnique).to.be.true;
375
+
376
+ afterEach(that);
377
+ })();
378
+ });
379
+
380
+ it('getOrCreateUniqueRepository recreates repository when mapped id is stale', function() {
381
+ (async () => {
382
+ const that = {};
383
+ await beforeEach(that);
384
+
385
+ const repository1 = await that.oneHatData.getOrCreateUniqueRepository('partsMap', 'bar');
386
+ const originalId = repository1.id;
387
+
388
+ that.oneHatData.deleteRepository(originalId);
389
+
390
+ const repository2 = await that.oneHatData.getOrCreateUniqueRepository('partsMap', 'bar');
391
+
392
+ expect(repository2).to.be.ok;
393
+ expect(repository2.id).to.not.eq(originalId);
394
+ expect(that.oneHatData.uniqueRepositoryIdsMap.partsMap).to.be.eq(repository2.id);
272
395
 
273
396
  afterEach(that);
274
397
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/data",
3
- "version": "1.23.0",
3
+ "version": "1.23.2",
4
4
  "description": "JS data modeling package with adapters for many storage mediums.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -28,12 +28,19 @@ export default function useOneHatData(schemaName, uniqueRepository = false) {
28
28
  useEffect(() => {
29
29
 
30
30
  let repository,
31
+ repositoryIdToDelete = null,
32
+ isMounted = true,
31
33
  onChangeData = () => {};
32
34
  (async () => {
33
35
 
34
36
  if (uniqueRepository) {
35
- repository = await oneHatData.createRepository(schemaName);
36
- repository.isUnique = true;
37
+ if (_.isString(schemaName)) {
38
+ repository = await oneHatData.getUniqueRepository(schemaName);
39
+ } else {
40
+ repository = await oneHatData.createRepository(schemaName);
41
+ repository.isUnique = true;
42
+ }
43
+ repositoryIdToDelete = repository?.id;
37
44
  } else if (_.isObject(schemaName)) {
38
45
  if (schemaName.id) {
39
46
  repository = oneHatData.getRepositoryById(schemaName.id);
@@ -44,6 +51,11 @@ export default function useOneHatData(schemaName, uniqueRepository = false) {
44
51
  } else {
45
52
  repository = oneHatData.getRepository(schemaName); // Get bound Repository for this schema
46
53
  }
54
+
55
+ if (!isMounted || !repository) {
56
+ return;
57
+ }
58
+
47
59
  onChangeData = () => {
48
60
  setEntities(repository.entities); // Set new state in component
49
61
  };
@@ -57,9 +69,12 @@ export default function useOneHatData(schemaName, uniqueRepository = false) {
57
69
  })();
58
70
 
59
71
  return () => {
60
- repository.off('changeData', onChangeData);
61
- if (uniqueRepository) {
62
- oneHatData.deleteRepository(schemaName);
72
+ isMounted = false;
73
+ if (repository) {
74
+ repository.off('changeData', onChangeData);
75
+ }
76
+ if (uniqueRepository && repositoryIdToDelete) {
77
+ oneHatData.deleteRepository(repositoryIdToDelete);
63
78
  }
64
79
  };
65
80
 
package/src/OneHatData.js CHANGED
@@ -479,24 +479,72 @@ export class OneHatData extends EventEmitter {
479
479
  if (this.isDestroyed) {
480
480
  throw new Error('this.getRepository is no longer valid. OneHatData has been destroyed.');
481
481
  }
482
+ if (unique) {
483
+ throw new Error('this.getRepository(name, true) is no longer supported. Use await this.getUniqueRepository(name) instead.');
484
+ }
482
485
  const schema = this.getSchema(name);
483
486
  if (!schema) {
484
487
  return null;
485
488
  }
486
- if (unique) {
487
- const
488
- repoToClone = schema.getBoundRepository(),
489
- clone = _.cloneDeep(repoToClone);
490
-
491
- const id = uuid();
492
- clone.name = clone.name + '-' + id;
493
- clone.id = id;
494
- clone.isUnique = true;
495
- return clone;
496
- }
497
489
  return schema.getBoundRepository();
498
490
  }
499
491
 
492
+ /**
493
+ * Gets a fully initialized unique Repository for the supplied schema.
494
+ * @param {string} name - Name of Schema
495
+ * @return {Promise<Repository>} repository
496
+ */
497
+ getUniqueRepository = async (name) => {
498
+ if (this.isDestroyed) {
499
+ throw new Error('this.getUniqueRepository is no longer valid. OneHatData has been destroyed.');
500
+ }
501
+
502
+ const schema = this.getSchema(name);
503
+ if (!schema) {
504
+ throw new Error('this.getUniqueRepository: Schema not found. Name: ' + name);
505
+ }
506
+
507
+ const boundRepository = schema.getBoundRepository();
508
+ if (!boundRepository) {
509
+ throw new Error('this.getUniqueRepository: Schema does not have a bound Repository. Name: ' + name);
510
+ }
511
+
512
+ const
513
+ id = uuid(),
514
+ schemaRepositoryDef = _.isString(schema.repository) ? { type: schema.repository } : schema.repository,
515
+ safeOverrides = _.omit(boundRepository.originalConfig || {}, [ // Keep behavioral overrides while omitting mutable runtime state and nested repo instances.
516
+ 'id',
517
+ 'name',
518
+ 'isUnique',
519
+ 'local',
520
+ 'remote',
521
+ 'entities',
522
+ 'filters',
523
+ 'sorters',
524
+ 'page',
525
+ 'previousPage',
526
+ 'pageTotal',
527
+ 'pageStart',
528
+ 'pageEnd',
529
+ 'totalPages',
530
+ 'total',
531
+ 'isFiltered',
532
+ 'isInitialized',
533
+ 'isLoaded',
534
+ 'isLoading',
535
+ 'lastLoaded',
536
+ 'hash',
537
+ ]),
538
+ config = _.merge({}, schemaRepositoryDef, this._repositoryGlobals, safeOverrides, {
539
+ schema,
540
+ id,
541
+ name: boundRepository.name + '-' + id,
542
+ isUnique: true,
543
+ });
544
+
545
+ return await this.createRepository(config);
546
+ }
547
+
500
548
  /**
501
549
  * Gets or creates a unique repository with the supplied schemaName and name
502
550
  * @param {string} mapName - Name of unique repository (will be internally mapped to an id)
@@ -505,13 +553,17 @@ export class OneHatData extends EventEmitter {
505
553
  */
506
554
  getOrCreateUniqueRepository = async (mapName, schemaName) => {
507
555
  if (this.isDestroyed) {
508
- throw new Error('this.getUniqueRepository is no longer valid. OneHatData has been destroyed.');
556
+ throw new Error('this.getOrCreateUniqueRepository is no longer valid. OneHatData has been destroyed.');
509
557
  }
510
558
 
511
559
  // Try to get it
512
560
  let id = this.uniqueRepositoryIdsMap[mapName];
513
561
  if (id) {
514
- return this.getRepositoryById(id);
562
+ const existingRepository = this.getRepositoryById(id);
563
+ if (existingRepository) {
564
+ return existingRepository;
565
+ }
566
+ delete this.uniqueRepositoryIdsMap[mapName];
515
567
  }
516
568
 
517
569
  // Try to create it
@@ -519,11 +571,8 @@ export class OneHatData extends EventEmitter {
519
571
  if (!schema) {
520
572
  return null;
521
573
  }
522
-
523
- const repository = await this.createRepository(schemaName);
574
+ const repository = await this.getUniqueRepository(schemaName);
524
575
  id = repository.id;
525
- repository.name += '-' + id;
526
- repository.isUnique = true;
527
576
  this.uniqueRepositoryIdsMap[mapName] = repository.id;
528
577
  return repository;
529
578
  }
@@ -1171,7 +1171,6 @@ class AjaxRepository extends Repository {
1171
1171
  * @private
1172
1172
  */
1173
1173
  _send(method, url, data, options = {}) {
1174
-
1175
1174
  if (!url) {
1176
1175
  this.throwError('No url submitted');
1177
1176
  return;
@@ -160,6 +160,7 @@ export default class Repository extends EventEmitter {
160
160
  * @member {boolean} debugMode - Whether this Repository should output debug messages
161
161
  */
162
162
  debugMode: false,
163
+
163
164
  };
164
165
 
165
166
  _.merge(this, defaults, config);
@@ -235,6 +236,11 @@ export default class Repository extends EventEmitter {
235
236
  */
236
237
  this.isInitialized = false;
237
238
 
239
+ /**
240
+ * @member {boolean} isInitializing - State: whether initialize() is currently running
241
+ */
242
+ this.isInitializing = false;
243
+
238
244
  /**
239
245
  * @member {boolean} isTree - Whether this Repository contains TreeNodes
240
246
  * @readonly
@@ -328,38 +334,43 @@ export default class Repository extends EventEmitter {
328
334
  * This is async because we may need to wait for loading and sorting.
329
335
  */
330
336
  async initialize() {
331
- // Create default sorters if none supplied
332
- if (this.isAutoSort && !this.sorters.length) {
333
- this.sorters = this.getDefaultSorters();
334
- }
335
-
336
- // Assign event handlers
337
- this.on('entity_change', async (entity) => { // Entity changed its value
338
- if (this.isAutoSave && !this.isRemotePhantomMode) {
339
- return await this.save(entity);
337
+ this.isInitializing = true;
338
+ try {
339
+ // Create default sorters if none supplied
340
+ if (this.isAutoSort && !this.sorters.length) {
341
+ this.sorters = this.getDefaultSorters();
340
342
  }
341
- });
343
+
344
+ // Assign event handlers
345
+ this.on('entity_change', async (entity) => { // Entity changed its value
346
+ if (this.isAutoSave && !this.isRemotePhantomMode) {
347
+ return await this.save(entity);
348
+ }
349
+ });
342
350
 
343
- // Auto load & sort
344
- if (this.isAutoLoad && !this.isTree) {
345
- await this.load();
346
- }
347
- if (!this.isSorted && this.isAutoSort && !this.isRemoteSort && !this.isTree) { // load may have sorted, in which case this will be skipped.
348
- await this.sort();
349
- }
351
+ // Auto load & sort
352
+ if (this.isAutoLoad && !this.isTree) {
353
+ await this.load();
354
+ }
355
+ if (!this.isSorted && this.isAutoSort && !this.isRemoteSort && !this.isTree) { // load may have sorted, in which case this will be skipped.
356
+ await this.sort();
357
+ }
350
358
 
351
- this._createMethods();
352
- this._createStatics();
353
- this._createListeners();
359
+ this._createMethods();
360
+ this._createStatics();
361
+ this._createListeners();
354
362
 
355
- const init = this.schema.repository.init || this.originalConfig.init; // The latter is mainly for lfr repositories
356
- if (init) {
357
- await init.call(this);
358
- }
359
- this.rehash();
363
+ const init = this.schema.repository.init || this.originalConfig.init; // The latter is mainly for lfr repositories
364
+ if (init) {
365
+ await init.call(this);
366
+ }
367
+ this.rehash();
360
368
 
361
- this.isInitialized = true;
362
- this.emit('initialize');
369
+ this.isInitialized = true;
370
+ this.emit('initialize');
371
+ } finally {
372
+ this.isInitializing = false;
373
+ }
363
374
  }
364
375
 
365
376
  /**
@@ -1201,7 +1212,6 @@ export default class Repository extends EventEmitter {
1201
1212
  * @return {array} entities - new Entity objects
1202
1213
  */
1203
1214
  async addMultiple(allData, isPersisted = false) {
1204
-
1205
1215
  if (!this.canAdd) {
1206
1216
  this.throwError('Adding has been disabled on this repository.');
1207
1217
  return;
@@ -2275,10 +2285,23 @@ export default class Repository extends EventEmitter {
2275
2285
  * @param {object} data - optional data object to describe the error
2276
2286
  */
2277
2287
  throwError(obj, data = null) {
2288
+ let errorObject = obj;
2289
+ if (!(errorObject instanceof Error)) {
2290
+ // standardize the errorObject to be an Error instance
2291
+ if (_.isString(errorObject)) {
2292
+ errorObject = new Error(errorObject);
2293
+ } else if (errorObject && _.isString(errorObject.message)) {
2294
+ errorObject = new Error(errorObject.message);
2295
+ } else {
2296
+ errorObject = new Error('Unknown repository error');
2297
+ }
2298
+ }
2299
+ errorObject.context = data;
2300
+
2278
2301
  if (this.errorHandler) {
2279
- this.errorHandler(obj, data);
2302
+ this.errorHandler(errorObject, data);
2280
2303
  } else {
2281
- this.emit('error', obj, data);
2304
+ this.emit('error', errorObject, data);
2282
2305
  }
2283
2306
  }
2284
2307
 
@@ -2311,11 +2334,7 @@ export default class Repository extends EventEmitter {
2311
2334
  * @return {string} className
2312
2335
  */
2313
2336
  getClassName() {
2314
- if (this.isDestroyed) {
2315
- this.throwError('this.getClassName is no longer valid. Repository has been destroyed.');
2316
- return;
2317
- }
2318
- return this.__proto__.constructor.className;
2337
+ return this.__proto__.constructor.className || this.constructor.className || 'Repository';
2319
2338
  }
2320
2339
 
2321
2340
  get className() {
@@ -2327,11 +2346,7 @@ export default class Repository extends EventEmitter {
2327
2346
  * @return {string} className
2328
2347
  */
2329
2348
  getType() {
2330
- if (this.isDestroyed) {
2331
- this.throwError('this.getClassName is no longer valid. Repository has been destroyed.');
2332
- return;
2333
- }
2334
- return this.__proto__.constructor.type;
2349
+ return this.__proto__.constructor.type || this.constructor.type || null;
2335
2350
  }
2336
2351
 
2337
2352
  get type() {
@@ -2339,11 +2354,9 @@ export default class Repository extends EventEmitter {
2339
2354
  }
2340
2355
 
2341
2356
  toString() {
2342
- if (this.isDestroyed) {
2343
- this.throwError('this.toString is no longer valid. Repository has been destroyed.');
2344
- return;
2345
- }
2346
- return this.getClassName() + 'Repository {' + this.name + '} - ' + this.id;
2357
+ const name = this.name || 'destroyed';
2358
+ const id = this.id || 'unknown';
2359
+ return this.getClassName() + 'Repository {' + name + '} - ' + id;
2347
2360
  }
2348
2361
 
2349
2362
  };