@onehat/data 1.23.0 → 1.23.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.
@@ -28,6 +28,17 @@ async function beforeEach(that) {
28
28
  }, true);
29
29
  that.repository = that.oneHatData.getRepositoryById('foo');
30
30
  }
31
+
32
+ async function waitForRepositoryInitialization(repository, timeout = 2000) {
33
+ const start = Date.now();
34
+ while (!repository.isInitialized) {
35
+ if (Date.now() - start > timeout) {
36
+ throw new Error('Repository did not initialize in time: ' + repository.name);
37
+ }
38
+ await new Promise((resolve) => setTimeout(resolve, 5));
39
+ }
40
+ }
41
+
31
42
  function afterEach(that) {
32
43
  that.oneHatData.destroy();
33
44
  }
@@ -274,6 +285,97 @@ describe('OneHatData', function() {
274
285
  })();
275
286
  });
276
287
 
288
+ it('getRepositoryAsync returns initialized unique repository', function() {
289
+ (async () => {
290
+ const that = {};
291
+ await beforeEach(that);
292
+
293
+ const repository = await that.oneHatData.getRepositoryAsync('bar', true);
294
+
295
+ expect(repository).to.be.ok;
296
+ expect(repository.isUnique).to.be.true;
297
+ expect(repository.isInitialized).to.be.true;
298
+
299
+ afterEach(that);
300
+ })();
301
+ });
302
+
303
+ it('getRepositoryAsync returns initialized bound repository', function() {
304
+ (async () => {
305
+ const that = {};
306
+ await beforeEach(that);
307
+
308
+ const repository = await that.oneHatData.getRepositoryAsync('bar');
309
+
310
+ expect(repository).to.be.eq(that.repository);
311
+ expect(repository.isInitialized).to.be.true;
312
+
313
+ afterEach(that);
314
+ })();
315
+ });
316
+
317
+ it('getRepository unique keeps filters isolated from bound repository', function() {
318
+ (async () => {
319
+ const that = {};
320
+ await beforeEach(that);
321
+
322
+ const
323
+ boundRepository = that.oneHatData.getRepository('bar'),
324
+ uniqueRepository = that.oneHatData.getRepository('bar', true);
325
+
326
+ await waitForRepositoryInitialization(uniqueRepository);
327
+
328
+ boundRepository.filter('key', 'bound-only');
329
+ expect(boundRepository.hasFilterValue('key', 'bound-only')).to.be.true;
330
+ expect(uniqueRepository.hasFilter('key')).to.be.false;
331
+
332
+ uniqueRepository.filter('key', 'unique-only');
333
+ expect(uniqueRepository.hasFilterValue('key', 'unique-only')).to.be.true;
334
+ expect(boundRepository.hasFilterValue('key', 'bound-only')).to.be.true;
335
+ expect(boundRepository.hasFilterValue('key', 'unique-only')).to.be.false;
336
+
337
+ afterEach(that);
338
+ })();
339
+ });
340
+
341
+ it('getRepository unique allows pre-init setBaseParams for Ajax repositories', function() {
342
+ (async () => {
343
+ const that = {};
344
+ await beforeEach(that);
345
+
346
+ that.oneHatData.createSchema({
347
+ name: 'meters',
348
+ model: {
349
+ idProperty: 'id',
350
+ displayProperty: 'name',
351
+ properties: [
352
+ { name: 'id' },
353
+ { name: 'name' },
354
+ ],
355
+ },
356
+ repository: {
357
+ type: 'ajax',
358
+ api: {
359
+ get: 'meters',
360
+ },
361
+ },
362
+ });
363
+ await that.oneHatData.createRepository('meters', true);
364
+
365
+ const uniqueRepository = that.oneHatData.getRepository('meters', true);
366
+
367
+ expect(() => {
368
+ uniqueRepository.setBaseParams({
369
+ foo: 'bar',
370
+ });
371
+ }).to.not.throw();
372
+
373
+ expect(uniqueRepository.getBaseParam('foo')).to.be.eq('bar');
374
+
375
+ afterEach(that);
376
+ })();
377
+ });
378
+
277
379
  it('getRepositoriesBy', function() {
278
380
  (async function() {
279
381
  await beforeEach();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/data",
3
- "version": "1.23.0",
3
+ "version": "1.23.1",
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.getRepositoryAsync(schemaName, true);
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);
@@ -42,8 +49,13 @@ export default function useOneHatData(schemaName, uniqueRepository = false) {
42
49
  repository = await oneHatData.createRepository(schemaName)
43
50
  }
44
51
  } else {
45
- repository = oneHatData.getRepository(schemaName); // Get bound Repository for this schema
52
+ repository = await oneHatData.getRepositoryAsync(schemaName); // Get initialized bound Repository for this schema
53
+ }
54
+
55
+ if (!isMounted || !repository) {
56
+ return;
46
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
@@ -484,19 +484,244 @@ export class OneHatData extends EventEmitter {
484
484
  return null;
485
485
  }
486
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;
487
+ return this._createUniqueRepositorySync(name);
496
488
  }
497
489
  return schema.getBoundRepository();
498
490
  }
499
491
 
492
+ /**
493
+ * Async variant of getRepository.
494
+ * Useful when unique repositories are needed in frameworks that expect
495
+ * fully initialized objects before first use.
496
+ * @param {string} name - Name of Schema
497
+ * @param {boolean} unique - Whether to create a unique repository
498
+ * @param {number} timeout - Max ms to wait for initialization
499
+ * @return {Promise<Repository>} repository
500
+ */
501
+ getRepositoryAsync = async (name, unique = false, timeout = 10000) => {
502
+ if (this.isDestroyed) {
503
+ throw new Error('this.getRepositoryAsync is no longer valid. OneHatData has been destroyed.');
504
+ }
505
+
506
+ const repository = this.getRepository(name, unique);
507
+ if (!repository) {
508
+ return null;
509
+ }
510
+
511
+ await this._waitForRepositoryInitialization(repository, timeout);
512
+ return repository;
513
+ }
514
+
515
+ /**
516
+ * Creates an unbound unique Repository synchronously.
517
+ * It returns the instance immediately and initializes in background.
518
+ * @param {string} name - Name of Schema
519
+ * @return {Repository} repository
520
+ */
521
+ _createUniqueRepositorySync = (name) => {
522
+ if (this.isDestroyed) {
523
+ throw new Error('this._createUniqueRepositorySync is no longer valid. OneHatData has been destroyed.');
524
+ }
525
+
526
+ const schema = this.getSchema(name);
527
+ if (!schema) {
528
+ return null;
529
+ }
530
+
531
+ const boundRepository = schema.getBoundRepository();
532
+ if (!boundRepository) {
533
+ return null;
534
+ }
535
+
536
+ const id = uuid();
537
+ const schemaRepositoryDef = _.isString(schema.repository) ? { type: schema.repository } : schema.repository;
538
+
539
+ // Keep behavioral overrides while omitting mutable runtime state and nested repo instances.
540
+ const safeOverrides = _.omit(boundRepository.originalConfig || {}, [
541
+ 'id',
542
+ 'name',
543
+ 'isUnique',
544
+ 'local',
545
+ 'remote',
546
+ 'entities',
547
+ 'filters',
548
+ 'sorters',
549
+ 'page',
550
+ 'previousPage',
551
+ 'pageTotal',
552
+ 'pageStart',
553
+ 'pageEnd',
554
+ 'totalPages',
555
+ 'total',
556
+ 'isFiltered',
557
+ 'isInitialized',
558
+ 'isLoaded',
559
+ 'isLoading',
560
+ 'lastLoaded',
561
+ 'hash',
562
+ ]);
563
+
564
+ const config = _.merge({}, schemaRepositoryDef, this._repositoryGlobals, safeOverrides, {
565
+ schema,
566
+ id,
567
+ name: boundRepository.name + '-' + id,
568
+ isUnique: true,
569
+ });
570
+
571
+ const repository = this._createRepositorySync(config);
572
+
573
+ this.repositories[repository.id] = repository;
574
+ this._initializeRepositoryInBackground(repository);
575
+
576
+ if (repository.isRegisteredEvent('logout')) { // OneBuild repository emits this
577
+ this.relayEventsFrom(repository, ['logout']);
578
+ }
579
+ if (repository.isRegisteredEvent(CROSS_TAB_EVENT_NAME)) {
580
+ this.relayEventsFrom(repository, [CROSS_TAB_EVENT_NAME]);
581
+ }
582
+
583
+ this.emit('createRepository', repository);
584
+ return repository;
585
+ }
586
+
587
+ /**
588
+ * Creates a Repository instance synchronously.
589
+ * Used when we must return a repository instance immediately.
590
+ * @param {object} config - Repository config object
591
+ * @return {Repository} repository
592
+ * @private
593
+ */
594
+ _createRepositorySync = (config) => {
595
+ if (this.isDestroyed) {
596
+ throw new Error('this._createRepositorySync is no longer valid. OneHatData has been destroyed.');
597
+ }
598
+
599
+ const workingConfig = _.merge({}, config);
600
+
601
+ if (workingConfig.type === 'lfr') {
602
+ const generalConfig = _.omit(workingConfig, ['type', 'local', 'remote']);
603
+
604
+ let localConfig = workingConfig.local;
605
+ let remoteConfig = workingConfig.remote;
606
+
607
+ if (_.isString(localConfig)) {
608
+ localConfig = {
609
+ type: localConfig,
610
+ };
611
+ }
612
+ if (_.isString(remoteConfig)) {
613
+ remoteConfig = {
614
+ type: remoteConfig,
615
+ };
616
+ }
617
+
618
+ if (workingConfig.mode === MODE_COMMAND_QUEUE) {
619
+ generalConfig.isPaginated = false;
620
+ remoteConfig.type = 'command';
621
+ }
622
+
623
+ localConfig = _.merge({}, generalConfig, localConfig);
624
+ remoteConfig = _.merge({}, generalConfig, remoteConfig);
625
+
626
+ const localRepository = this._createRepositorySync(localConfig);
627
+ const remoteRepository = this._createRepositorySync(remoteConfig);
628
+
629
+ workingConfig.local = localRepository;
630
+ workingConfig.remote = remoteRepository;
631
+ }
632
+
633
+ const RepositoryType = this._repositoryTypes[workingConfig.type];
634
+ if (!RepositoryType) {
635
+ throw new Error('Repository type does not exist');
636
+ }
637
+
638
+ return new RepositoryType(workingConfig, this);
639
+ }
640
+
641
+ /**
642
+ * Initializes repositories in the proper dependency order.
643
+ * @param {Repository} repository - Repository instance
644
+ * @private
645
+ */
646
+ _initializeRepositoryInBackground = (repository) => {
647
+ repository._initializationError = null;
648
+ repository._initializationPromise = Promise.resolve()
649
+ .then(async () => {
650
+ if (repository && repository.type === 'lfr') {
651
+ if (repository.local && _.isFunction(repository.local.initialize)) {
652
+ await repository.local.initialize();
653
+ }
654
+ if (repository.remote && _.isFunction(repository.remote.initialize)) {
655
+ await repository.remote.initialize();
656
+ }
657
+ }
658
+ await repository.initialize();
659
+ })
660
+ .catch((error) => {
661
+ repository._initializationError = error;
662
+ repository.emit('error', error);
663
+ });
664
+ return repository._initializationPromise;
665
+ }
666
+
667
+ /**
668
+ * Waits for repository initialization if currently pending.
669
+ * @param {Repository} repository - Repository instance
670
+ * @param {number} timeout - Max ms to wait for initialization
671
+ * @return {Promise<Repository>} repository
672
+ * @private
673
+ */
674
+ _waitForRepositoryInitialization = async (repository, timeout = 10000) => {
675
+ if (!repository) {
676
+ return null;
677
+ }
678
+
679
+ if (repository._initializationPromise) {
680
+ await repository._initializationPromise;
681
+ }
682
+
683
+ if (repository._initializationError) {
684
+ throw repository._initializationError;
685
+ }
686
+
687
+ if (repository.isInitialized === true) {
688
+ return repository;
689
+ }
690
+
691
+ if (repository.isInitializing && _.isFunction(repository.on) && _.isFunction(repository.off)) {
692
+ await new Promise((resolve, reject) => {
693
+ const timeoutId = setTimeout(() => {
694
+ repository.off('initialize', handleInitialize);
695
+ repository.off('error', handleError);
696
+ reject(new Error('Timed out waiting for repository initialization: ' + repository.name));
697
+ }, timeout);
698
+
699
+ const handleInitialize = () => {
700
+ clearTimeout(timeoutId);
701
+ repository.off('initialize', handleInitialize);
702
+ repository.off('error', handleError);
703
+ resolve();
704
+ };
705
+
706
+ const handleError = (error) => {
707
+ clearTimeout(timeoutId);
708
+ repository.off('initialize', handleInitialize);
709
+ repository.off('error', handleError);
710
+ reject(error || new Error('Repository initialization failed: ' + repository.name));
711
+ };
712
+
713
+ repository.on('initialize', handleInitialize);
714
+ repository.on('error', handleError);
715
+ });
716
+ }
717
+
718
+ if (repository._initializationError) {
719
+ throw repository._initializationError;
720
+ }
721
+
722
+ return repository;
723
+ }
724
+
500
725
  /**
501
726
  * Gets or creates a unique repository with the supplied schemaName and name
502
727
  * @param {string} mapName - Name of unique repository (will be internally mapped to an id)
@@ -141,51 +141,58 @@ class AjaxRepository extends Repository {
141
141
  }
142
142
 
143
143
  async initialize() {
144
+ this.isInitializing = true;
145
+ try {
144
146
 
145
- this.registerEvents([
146
- 'beforeLoad',
147
- ]);
147
+ this.registerEvents([
148
+ 'beforeLoad',
149
+ ]);
148
150
 
149
- // Respond to Repository events
150
- this.on('beforeSave', this._onBeforeSave);
151
+ // Respond to Repository events
152
+ this.on('beforeSave', this._onBeforeSave);
151
153
 
152
154
 
153
- // Create Reader
154
- let readerConfig;
155
- if (this.reader && this.reader.type) {
156
- readerConfig = this.reader;
157
- } else if (_.isString(this.reader)) {
158
- readerConfig = {
159
- type: this.reader,
160
- };
161
- }
162
- if (readerConfig && ReaderTypes[readerConfig.type]) {
163
- const Reader = ReaderTypes[readerConfig.type];
164
- this.reader = new Reader(readerConfig);
165
- } else {
166
- this.reader = null;
167
- }
155
+ // Create Reader
156
+ let readerConfig;
157
+ if (this.reader && this.reader.type) {
158
+ readerConfig = this.reader;
159
+ } else if (_.isString(this.reader)) {
160
+ readerConfig = {
161
+ type: this.reader,
162
+ };
163
+ }
164
+ if (readerConfig && ReaderTypes[readerConfig.type]) {
165
+ const Reader = ReaderTypes[readerConfig.type];
166
+ this.reader = new Reader(readerConfig);
167
+ } else {
168
+ this.reader = null;
169
+ }
168
170
 
169
- // Create Writer
170
- let writerConfig;
171
- if (this.writer && this.writer.type) {
172
- writerConfig = this.writer;
173
- } else if (_.isString(this.writer)) {
174
- writerConfig = {
175
- type: this.writer,
176
- };
177
- }
178
- if (writerConfig && WriterTypes[writerConfig.type]) {
179
- const Writer = WriterTypes[writerConfig.type];
180
- this.writer = new Writer(writerConfig);
181
- } else {
182
- this.writer = null;
183
- }
171
+ // Create Writer
172
+ let writerConfig;
173
+ if (this.writer && this.writer.type) {
174
+ writerConfig = this.writer;
175
+ } else if (_.isString(this.writer)) {
176
+ writerConfig = {
177
+ type: this.writer,
178
+ };
179
+ }
180
+ if (writerConfig && WriterTypes[writerConfig.type]) {
181
+ const Writer = WriterTypes[writerConfig.type];
182
+ this.writer = new Writer(writerConfig);
183
+ } else {
184
+ this.writer = null;
185
+ }
184
186
 
185
- // Initialize query params
186
- this._setInitialQueryParams();
187
+ // Initialize query params
188
+ this._setInitialQueryParams();
187
189
 
188
- await super.initialize();
190
+ await super.initialize();
191
+ } finally {
192
+ if (!this.isInitialized) {
193
+ this.isInitializing = false;
194
+ }
195
+ }
189
196
  }
190
197
 
191
198
  /**
@@ -227,6 +234,7 @@ class AjaxRepository extends Repository {
227
234
  * @param {boolean} isBaseParam - Whether param is a base param (to be sent on every request).
228
235
  */
229
236
  setParam(name, value, isBaseParam = false) {
237
+ this._assertInitialized('setParam');
230
238
  const
231
239
  re = /^([^\[]+)\[([^\]]+)\](.*)$/,
232
240
  matches = name.match(re),
@@ -263,6 +271,7 @@ class AjaxRepository extends Repository {
263
271
  * @param {boolean} isBaseParam - Whether param is a base param (to be sent on every request).
264
272
  */
265
273
  setValuelessParam(name, isBaseParam = false) {
274
+ this._assertInitialized('setValuelessParam');
266
275
  const
267
276
  re = /^([^\[]+)\[([^\]]+)\](.*)$/,
268
277
  matches = name.match(re),
@@ -290,6 +299,7 @@ class AjaxRepository extends Repository {
290
299
  * @param {object} params - Params to set. Key is parameter name, value is parameter value
291
300
  */
292
301
  setParams(params) {
302
+ this._assertInitialized('setParams');
293
303
  const oThis = this;
294
304
  _.each(params, (value, name) => {
295
305
  oThis.setParam(name, value);
@@ -301,6 +311,7 @@ class AjaxRepository extends Repository {
301
311
  * @param {string} name - Param name
302
312
  */
303
313
  hasBaseParam(name) {
314
+ this._assertInitialized('hasBaseParam');
304
315
  if (this._baseParams.hasOwnProperty(name)) {
305
316
  return true;
306
317
  }
@@ -324,6 +335,7 @@ class AjaxRepository extends Repository {
324
335
  * @param {string} name - Param name
325
336
  */
326
337
  getBaseParam(name) {
338
+ this._assertInitialized('getBaseParam');
327
339
  if (!this.hasBaseParam(name)) {
328
340
  return null;
329
341
  }
@@ -351,6 +363,7 @@ class AjaxRepository extends Repository {
351
363
  * @param {object} params - Params to set. Key is parameter name, value is parameter value
352
364
  */
353
365
  getBaseParams() {
366
+ this._assertInitialized('getBaseParams');
354
367
  return this._baseParams;
355
368
  }
356
369
 
@@ -358,6 +371,7 @@ class AjaxRepository extends Repository {
358
371
  * Returns current value of any baseParam query conditions
359
372
  */
360
373
  getBaseParamConditions() {
374
+ this._assertInitialized('getBaseParamConditions');
361
375
  const
362
376
  existingConditions = this._baseParams.conditions || {},
363
377
  convertedConditions = {};
@@ -371,6 +385,7 @@ class AjaxRepository extends Repository {
371
385
  * Returns current value of any param query conditions
372
386
  */
373
387
  getParamConditions() {
388
+ this._assertInitialized('getParamConditions');
374
389
  const
375
390
  existingConditions = this._params.conditions || {},
376
391
  convertedConditions = {};
@@ -385,6 +400,7 @@ class AjaxRepository extends Repository {
385
400
  * @param {string} name - Param name
386
401
  */
387
402
  hasParam(name) {
403
+ this._assertInitialized('hasParam');
388
404
  if (this._params.hasOwnProperty(name)) {
389
405
  return true;
390
406
  }
@@ -409,6 +425,7 @@ class AjaxRepository extends Repository {
409
425
  * @param {any} value - Param value to set.
410
426
  */
411
427
  setBaseParam(name, value) {
428
+ this._assertInitialized('setBaseParam');
412
429
  this.setParam(name, value, true);
413
430
  }
414
431
 
@@ -417,6 +434,7 @@ class AjaxRepository extends Repository {
417
434
  * @param {object} params - Base params to set. Key is parameter name, value is parameter value
418
435
  */
419
436
  setBaseParams(params) {
437
+ this._assertInitialized('setBaseParams');
420
438
  const oThis = this;
421
439
  _.each(params, (value, name) => {
422
440
  oThis.setBaseParam(name, value);
@@ -429,6 +447,7 @@ class AjaxRepository extends Repository {
429
447
  * @param {boolean} reload - Whether to reload repository. Defaults to false.
430
448
  */
431
449
  clearParams(reload = false, clearBase = false) {
450
+ this._assertInitialized('clearParams');
432
451
  this._params = {};
433
452
  if (clearBase) {
434
453
  this._baseParams = {};
@@ -444,6 +463,7 @@ class AjaxRepository extends Repository {
444
463
  * Refreshes entities.
445
464
  */
446
465
  _onChangeSorters() {
466
+ this._assertInitialized('_onChangeSorters');
447
467
  const sorter = this.sorters[0];
448
468
  this.setBaseParam(this.paramSort, sorter.name);
449
469
  this.setBaseParam(this.paramDirection, sorter.direction);
@@ -458,6 +478,7 @@ class AjaxRepository extends Repository {
458
478
  * Refreshes entities.
459
479
  */
460
480
  _onChangeFilters() {
481
+ this._assertInitialized('_onChangeFilters');
461
482
  const oThis = this;
462
483
  _.each(this.filters, (value, name) => {
463
484
  oThis.setParam(name, value);
@@ -473,6 +494,7 @@ class AjaxRepository extends Repository {
473
494
  * Refreshes entities.
474
495
  */
475
496
  _onChangePagination() {
497
+ this._assertInitialized('_onChangePagination');
476
498
  this.setBaseParam(this.paramPageNum, this.isPaginated ? this.page : null);
477
499
  this.setBaseParam(this.paramPageSize, this.isPaginated ? this.pageSize : null);
478
500
 
@@ -512,6 +534,7 @@ class AjaxRepository extends Repository {
512
534
  * @fires beforeLoad,changeData,load,error
513
535
  */
514
536
  async load(params, callback = null) {
537
+ this._assertInitialized('load');
515
538
  if (this.isTree) {
516
539
  return this.loadRootNodes();
517
540
  }
@@ -684,6 +707,7 @@ class AjaxRepository extends Repository {
684
707
  }
685
708
 
686
709
  showMore(params = {}, callback) {
710
+ this._assertInitialized('showMore');
687
711
  params.showMore = true;
688
712
  return this.load(params, callback);
689
713
  }
@@ -696,6 +720,7 @@ class AjaxRepository extends Repository {
696
720
  * @fires reloadEntity,beforeLoad,changeData,load,error
697
721
  */
698
722
  async reloadEntity(entity, callback = null) { // use this notation so we can override it in subclasses
723
+ this._assertInitialized('reloadEntity');
699
724
  if (this.isDestroyed) {
700
725
  this.throwError('this.reloadEntity is no longer valid. Repository has been destroyed.');
701
726
  return;
@@ -759,6 +784,7 @@ class AjaxRepository extends Repository {
759
784
  * @private
760
785
  */
761
786
  _getReloadEntityParams(entity) {
787
+ this._assertInitialized('_getReloadEntityParams');
762
788
  const params = {
763
789
  id: entity.id,
764
790
  };
@@ -770,6 +796,7 @@ class AjaxRepository extends Repository {
770
796
  * @private
771
797
  */
772
798
  _onBeforeSave() {
799
+ this._assertInitialized('_onBeforeSave');
773
800
  this._operations = {
774
801
  add: false,
775
802
  edit: false,
@@ -1171,6 +1198,7 @@ class AjaxRepository extends Repository {
1171
1198
  * @private
1172
1199
  */
1173
1200
  _send(method, url, data, options = {}) {
1201
+ this._assertInitialized('_send');
1174
1202
 
1175
1203
  if (!url) {
1176
1204
  this.throwError('No url submitted');
@@ -1251,6 +1279,7 @@ class AjaxRepository extends Repository {
1251
1279
  * since the server normally sorts, and they haven't yet gone to server.
1252
1280
  */
1253
1281
  sortInMemory() {
1282
+ this._assertInitialized('sortInMemory');
1254
1283
  const sorters = this.sorters;
1255
1284
  let sortNames = [],
1256
1285
  sortDirections = [];
@@ -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,66 @@ 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);
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();
368
+
369
+ this.isInitialized = true;
370
+ this.emit('initialize');
371
+ } finally {
372
+ this.isInitializing = false;
358
373
  }
359
- this.rehash();
374
+ }
360
375
 
361
- this.isInitialized = true;
362
- this.emit('initialize');
376
+ /**
377
+ * Throws when operational methods are used before initialization completes.
378
+ * @param {string} methodName - Name of the method being guarded
379
+ * @private
380
+ */
381
+ _assertInitialized(methodName) {
382
+ const strictMethods = {
383
+ add: true,
384
+ addMultiple: true,
385
+ save: true,
386
+ delete: true,
387
+ load: true,
388
+ reloadEntity: true,
389
+ _send: true,
390
+ };
391
+ if (!strictMethods[methodName]) {
392
+ return;
393
+ }
394
+ if (!this.isInitialized && !this.isInitializing) {
395
+ throw new Error('Repository ' + this.name + ' is not initialized. Cannot call ' + methodName + ' yet.');
396
+ }
363
397
  }
364
398
 
365
399
  /**
@@ -588,6 +622,7 @@ export default class Repository extends EventEmitter {
588
622
  this.throwError('this.sort is no longer valid. Repository has been destroyed.');
589
623
  return;
590
624
  }
625
+ this._assertInitialized('sort');
591
626
  // Assemble sorting definition objects
592
627
  let sorters = [];
593
628
  if (_.isNil(arg1)) {
@@ -648,6 +683,7 @@ export default class Repository extends EventEmitter {
648
683
  this.throwError('this.setSorters is no longer valid. Repository has been destroyed.');
649
684
  return;
650
685
  }
686
+ this._assertInitialized('setSorters');
651
687
  if (!this.allowsMultiSort && sorters.length > 1) {
652
688
  this.throwError('Cannot have more than one sorter at a time.');
653
689
  return;
@@ -757,6 +793,7 @@ export default class Repository extends EventEmitter {
757
793
  this.throwError('this.filter is no longer valid. Repository has been destroyed.');
758
794
  return;
759
795
  }
796
+ this._assertInitialized('filter');
760
797
 
761
798
  if (_.isNil(arg1)) {
762
799
  return this.clearFilters();
@@ -833,6 +870,7 @@ export default class Repository extends EventEmitter {
833
870
  * @return this
834
871
  */
835
872
  setFilters(filters, clearFirst = true) {
873
+ this._assertInitialized('setFilters');
836
874
  const parsed = _.map(filters, (value, name) => {
837
875
  return {
838
876
  name,
@@ -853,6 +891,7 @@ export default class Repository extends EventEmitter {
853
891
  * - repository.clearFilters(['first_name', 'last_name']); // Clear multiple filters
854
892
  */
855
893
  clearFilters(filtersToClear) {
894
+ this._assertInitialized('clearFilters');
856
895
  let filters = [];
857
896
  if (filtersToClear) {
858
897
  if (_.isString(filtersToClear)) {
@@ -875,6 +914,7 @@ export default class Repository extends EventEmitter {
875
914
  this.throwError('this._setFilters is no longer valid. Repository has been destroyed.');
876
915
  return;
877
916
  }
917
+ this._assertInitialized('_setFilters');
878
918
  if (!_.isEqual(this.filters, filters)) {
879
919
  this.filters = filters;
880
920
  this.resetPagination();
@@ -1114,6 +1154,7 @@ export default class Repository extends EventEmitter {
1114
1154
  this.throwError('this.add is no longer valid. Repository has been destroyed.');
1115
1155
  return;
1116
1156
  }
1157
+ this._assertInitialized('add');
1117
1158
  if (!this.canAdd) {
1118
1159
  this.throwError('Adding has been disabled on this repository.');
1119
1160
  return;
@@ -1201,6 +1242,7 @@ export default class Repository extends EventEmitter {
1201
1242
  * @return {array} entities - new Entity objects
1202
1243
  */
1203
1244
  async addMultiple(allData, isPersisted = false) {
1245
+ this._assertInitialized('addMultiple');
1204
1246
 
1205
1247
  if (!this.canAdd) {
1206
1248
  this.throwError('Adding has been disabled on this repository.');
@@ -1386,6 +1428,7 @@ export default class Repository extends EventEmitter {
1386
1428
  this.throwError('this.getByIx is no longer valid. Repository has been destroyed.');
1387
1429
  return;
1388
1430
  }
1431
+ this._assertInitialized('getByIx');
1389
1432
  return this.entities[ix];
1390
1433
  }
1391
1434
 
@@ -1401,6 +1444,7 @@ export default class Repository extends EventEmitter {
1401
1444
  this.throwError('this.getByRange is no longer valid. Repository has been destroyed.');
1402
1445
  return;
1403
1446
  }
1447
+ this._assertInitialized('getByRange');
1404
1448
  return _.slice(this.entities, startIx, endIx+1);
1405
1449
  }
1406
1450
 
@@ -1414,6 +1458,7 @@ export default class Repository extends EventEmitter {
1414
1458
  this.throwError('this.getById is no longer valid. Repository has been destroyed.');
1415
1459
  return;
1416
1460
  }
1461
+ this._assertInitialized('getById');
1417
1462
  if (_.isNil(id)) {
1418
1463
  return null;
1419
1464
  }
@@ -1430,6 +1475,7 @@ export default class Repository extends EventEmitter {
1430
1475
  this.throwError('this.getIxById is no longer valid. Repository has been destroyed.');
1431
1476
  return;
1432
1477
  }
1478
+ this._assertInitialized('getIxById');
1433
1479
  if (_.isNil(id)) {
1434
1480
  return null;
1435
1481
  }
@@ -1451,6 +1497,7 @@ export default class Repository extends EventEmitter {
1451
1497
  this.throwError('this.getBy is no longer valid. Repository has been destroyed.');
1452
1498
  return;
1453
1499
  }
1500
+ this._assertInitialized('getBy');
1454
1501
  return _.filter(this.entities, filter);
1455
1502
  }
1456
1503
 
@@ -1468,6 +1515,7 @@ export default class Repository extends EventEmitter {
1468
1515
  this.throwError('this.getFirstBy is no longer valid. Repository has been destroyed.');
1469
1516
  return;
1470
1517
  }
1518
+ this._assertInitialized('getFirstBy');
1471
1519
  return _.find(this.entities, filter);
1472
1520
  }
1473
1521
 
@@ -1513,6 +1561,7 @@ export default class Repository extends EventEmitter {
1513
1561
  this.throwError('this.getEntities is no longer valid. Repository has been destroyed.');
1514
1562
  return;
1515
1563
  }
1564
+ this._assertInitialized('getEntities');
1516
1565
  return this.entities;
1517
1566
  }
1518
1567
  /* */
@@ -1528,6 +1577,7 @@ export default class Repository extends EventEmitter {
1528
1577
  this.throwError('this.getPagedEntities is no longer valid. Repository has been destroyed.');
1529
1578
  return;
1530
1579
  }
1580
+ this._assertInitialized('getEntitiesOnPage');
1531
1581
  const entities = this.getEntities();
1532
1582
  if (!this.isPaginated) {
1533
1583
  return entities;
@@ -1767,6 +1817,7 @@ export default class Repository extends EventEmitter {
1767
1817
  this.throwError('this.save is no longer valid. Repository has been destroyed.');
1768
1818
  return;
1769
1819
  }
1820
+ this._assertInitialized('save');
1770
1821
 
1771
1822
  this.emit('beforeSave'); // So subclasses can prep anything needed for saving
1772
1823
 
@@ -2011,6 +2062,7 @@ export default class Repository extends EventEmitter {
2011
2062
  * @fires delete
2012
2063
  */
2013
2064
  async delete(entities, moveSubtreeUp = false) {
2065
+ this._assertInitialized('delete');
2014
2066
  if (this.isDestroyed) {
2015
2067
  this.throwError('this.delete is no longer valid. Repository has been destroyed.');
2016
2068
  return;
@@ -2275,10 +2327,23 @@ export default class Repository extends EventEmitter {
2275
2327
  * @param {object} data - optional data object to describe the error
2276
2328
  */
2277
2329
  throwError(obj, data = null) {
2330
+ let errorObject = obj;
2331
+ if (!(errorObject instanceof Error)) {
2332
+ // standardize the errorObject to be an Error instance
2333
+ if (_.isString(errorObject)) {
2334
+ errorObject = new Error(errorObject);
2335
+ } else if (errorObject && _.isString(errorObject.message)) {
2336
+ errorObject = new Error(errorObject.message);
2337
+ } else {
2338
+ errorObject = new Error('Unknown repository error');
2339
+ }
2340
+ }
2341
+ errorObject.context = data;
2342
+
2278
2343
  if (this.errorHandler) {
2279
- this.errorHandler(obj, data);
2344
+ this.errorHandler(errorObject, data);
2280
2345
  } else {
2281
- this.emit('error', obj, data);
2346
+ this.emit('error', errorObject, data);
2282
2347
  }
2283
2348
  }
2284
2349
 
@@ -2311,11 +2376,7 @@ export default class Repository extends EventEmitter {
2311
2376
  * @return {string} className
2312
2377
  */
2313
2378
  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;
2379
+ return this.__proto__.constructor.className || this.constructor.className || 'Repository';
2319
2380
  }
2320
2381
 
2321
2382
  get className() {
@@ -2327,11 +2388,7 @@ export default class Repository extends EventEmitter {
2327
2388
  * @return {string} className
2328
2389
  */
2329
2390
  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;
2391
+ return this.__proto__.constructor.type || this.constructor.type || null;
2335
2392
  }
2336
2393
 
2337
2394
  get type() {
@@ -2339,11 +2396,9 @@ export default class Repository extends EventEmitter {
2339
2396
  }
2340
2397
 
2341
2398
  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;
2399
+ const name = this.name || 'destroyed';
2400
+ const id = this.id || 'unknown';
2401
+ return this.getClassName() + 'Repository {' + name + '} - ' + id;
2347
2402
  }
2348
2403
 
2349
2404
  };