@nymphjs/nymph 1.0.0-beta.4 → 1.0.0-beta.41

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +195 -0
  2. package/README.md +12 -12
  3. package/dist/Entity.d.ts +24 -3
  4. package/dist/Entity.js +194 -86
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Entity.test.js +34 -17
  7. package/dist/Entity.test.js.map +1 -1
  8. package/dist/Entity.types.d.ts +3 -1
  9. package/dist/Nymph.d.ts +22 -7
  10. package/dist/Nymph.js +214 -68
  11. package/dist/Nymph.js.map +1 -1
  12. package/dist/Nymph.types.d.ts +18 -5
  13. package/dist/Nymph.types.js +1 -1
  14. package/dist/Nymph.types.js.map +1 -1
  15. package/dist/driver/NymphDriver.d.ts +1 -22
  16. package/dist/driver/NymphDriver.js +2 -16
  17. package/dist/driver/NymphDriver.js.map +1 -1
  18. package/dist/driver/testArtifacts.js +208 -159
  19. package/dist/driver/testArtifacts.js.map +1 -1
  20. package/dist/errors/InvalidStateError.d.ts +3 -0
  21. package/dist/errors/InvalidStateError.js +11 -0
  22. package/dist/errors/InvalidStateError.js.map +1 -0
  23. package/dist/errors/index.d.ts +1 -0
  24. package/dist/errors/index.js +1 -0
  25. package/dist/errors/index.js.map +1 -1
  26. package/dist/testArtifacts.d.ts +10 -3
  27. package/dist/testArtifacts.js +20 -11
  28. package/dist/testArtifacts.js.map +1 -1
  29. package/dist/testMocks.d.ts +10 -7
  30. package/dist/testMocks.js +22 -10
  31. package/dist/testMocks.js.map +1 -1
  32. package/dist/utils.d.ts +1 -1
  33. package/dist/utils.js +11 -5
  34. package/dist/utils.js.map +1 -1
  35. package/package.json +13 -13
  36. package/src/Entity.test.ts +31 -9
  37. package/src/Entity.ts +289 -113
  38. package/src/Entity.types.ts +13 -1
  39. package/src/Nymph.ts +386 -82
  40. package/src/Nymph.types.ts +76 -5
  41. package/src/driver/NymphDriver.ts +19 -85
  42. package/src/driver/testArtifacts.ts +125 -44
  43. package/src/errors/InvalidStateError.ts +6 -0
  44. package/src/errors/index.ts +2 -0
  45. package/src/testArtifacts.ts +28 -10
  46. package/src/testMocks.ts +38 -21
  47. package/src/utils.ts +21 -7
  48. package/typedoc.json +4 -0
package/src/Entity.ts CHANGED
@@ -11,7 +11,12 @@ import {
11
11
  EntityReference,
12
12
  SerializedEntityData,
13
13
  } from './Entity.types';
14
- import { EntityConflictError, InvalidParametersError } from './errors';
14
+ import {
15
+ EntityConflictError,
16
+ EntityIsSleepingReferenceError,
17
+ InvalidParametersError,
18
+ InvalidStateError,
19
+ } from './errors';
15
20
  import {
16
21
  entitiesToReferences,
17
22
  referencesToEntities,
@@ -45,9 +50,9 @@ import {
45
50
  *
46
51
  * Some notes about $equals() and $is(), the replacements for "==":
47
52
  *
48
- * The == operator will likely not give you the result you want, since any yet
49
- * to be unserialized data causes == to return false when you probably want it
50
- * to return true.
53
+ * The == operator will likely not give you the result you want, since two
54
+ * instances of the same entity will fail that check, even though they represent
55
+ * the same data in the database.
51
56
  *
52
57
  * $equals() performs a more strict comparison of the entity to another. Use
53
58
  * $equals() instead of the == operator when you want to check both the entities
@@ -56,8 +61,7 @@ import {
56
61
  *
57
62
  * - They must be entities.
58
63
  * - They must have equal GUIDs, or both must have no GUID.
59
- * - They must be instances of the same class.
60
- * - Their data must be equal.
64
+ * - Their data and tags must be equal.
61
65
  *
62
66
  * $is() performs a less strict comparison of the entity to another. Use $is()
63
67
  * instead of the == operator when the entity's data may have been changed, but
@@ -66,7 +70,7 @@ import {
66
70
  *
67
71
  * - They must be entities.
68
72
  * - They must have equal GUIDs, or both must have no GUID.
69
- * - If they have no GUIDs, their data must be equal.
73
+ * - If they have no GUIDs, their data and tags must be equal.
70
74
  *
71
75
  * Some notes about saving entities in other entity's properties:
72
76
  *
@@ -137,6 +141,10 @@ export default class Entity<T extends EntityData = EntityData>
137
141
  * The reference to use to wake.
138
142
  */
139
143
  protected $sleepingReference: EntityReference | null = null;
144
+ /**
145
+ * A promise that resolved when the entity's data is wake.
146
+ */
147
+ protected $wakePromise: Promise<Entity<T>> | null = null;
140
148
  /**
141
149
  * Properties that will not be serialized into JSON with toJSON(). This
142
150
  * can be considered a denylist, because these properties will not be set
@@ -146,6 +154,18 @@ export default class Entity<T extends EntityData = EntityData>
146
154
  * also listed in searchRestrictedData.
147
155
  */
148
156
  protected $privateData: string[] = [];
157
+ /**
158
+ * Whether this entity should publish changes to PubSub servers.
159
+ */
160
+ public static pubSubEnabled = true;
161
+ /**
162
+ * Whether this entity should be accessible on the frontend through the REST
163
+ * server.
164
+ *
165
+ * If this is false, any request from the client that attempts to use this
166
+ * entity will fail.
167
+ */
168
+ public static restEnabled = true;
149
169
  /**
150
170
  * Properties that will not be searchable from the frontend. If the frontend
151
171
  * includes any of these properties in any of their clauses, they will be
@@ -157,8 +177,20 @@ export default class Entity<T extends EntityData = EntityData>
157
177
  * be visible on the frontend, unlike $privateData, but any changes to them
158
178
  * that come from the frontend will be ignored.
159
179
  *
160
- * In addition to what's listed here, the 'user' and 'group' properties will
161
- * be filtered for non-admins when Tilmeld is being used.
180
+ * In addition to what's listed here, all of the access control properties
181
+ * will be included when Tilmeld is being used. These are:
182
+ *
183
+ * - acUser
184
+ * - acGroup
185
+ * - acOther
186
+ * - acRead
187
+ * - acWrite
188
+ * - acFull
189
+ * - user
190
+ * - group
191
+ *
192
+ * You should modify these through client enabled methods or the $save method
193
+ * instead, for safety.
162
194
  */
163
195
  protected $protectedData: string[] = [];
164
196
  /**
@@ -199,20 +231,19 @@ export default class Entity<T extends EntityData = EntityData>
199
231
  private $originalAcValues: ACProperties | null = null;
200
232
 
201
233
  /**
202
- * Load an entity.
203
- * @param guid The ID of the entity to load, undefined for a new entity.
234
+ * Initialize an entity.
204
235
  */
205
- public constructor(guid?: string) {
236
+ public constructor() {
206
237
  this.$nymph = (this.constructor as EntityConstructor).nymph;
207
238
  this.$dataHandler = {
208
239
  has: (data: EntityData, name: string) => {
209
- this.$referenceWake();
240
+ this.$check();
210
241
 
211
242
  return name in data || name in this.$sdata;
212
243
  },
213
244
 
214
245
  get: (data: EntityData, name: string) => {
215
- this.$referenceWake();
246
+ this.$check();
216
247
 
217
248
  if (this.$sdata.hasOwnProperty(name)) {
218
249
  data[name] = referencesToEntities(
@@ -229,7 +260,7 @@ export default class Entity<T extends EntityData = EntityData>
229
260
  },
230
261
 
231
262
  set: (data: EntityData, name: string, value: any) => {
232
- this.$referenceWake();
263
+ this.$check();
233
264
 
234
265
  if (this.$sdata.hasOwnProperty(name)) {
235
266
  delete this.$sdata[name];
@@ -239,7 +270,7 @@ export default class Entity<T extends EntityData = EntityData>
239
270
  },
240
271
 
241
272
  deleteProperty: (data: EntityData, name: string) => {
242
- this.$referenceWake();
273
+ this.$check();
243
274
 
244
275
  if (this.$sdata.hasOwnProperty(name)) {
245
276
  return delete this.$sdata[name];
@@ -255,7 +286,7 @@ export default class Entity<T extends EntityData = EntityData>
255
286
  name: string,
256
287
  descriptor: PropertyDescriptor
257
288
  ) => {
258
- this.$referenceWake();
289
+ this.$check();
259
290
 
260
291
  if (this.$sdata.hasOwnProperty(name)) {
261
292
  delete this.$sdata[name];
@@ -265,7 +296,7 @@ export default class Entity<T extends EntityData = EntityData>
265
296
  },
266
297
 
267
298
  getOwnPropertyDescriptor: (data: EntityData, name: string) => {
268
- this.$referenceWake();
299
+ this.$check();
269
300
 
270
301
  if (this.$sdata.hasOwnProperty(name)) {
271
302
  data[name] = referencesToEntities(
@@ -279,7 +310,7 @@ export default class Entity<T extends EntityData = EntityData>
279
310
  },
280
311
 
281
312
  ownKeys: (data: EntityData) => {
282
- this.$referenceWake();
313
+ this.$check();
283
314
 
284
315
  return Object.getOwnPropertyNames(data).concat(
285
316
  Object.getOwnPropertyNames(this.$sdata)
@@ -291,20 +322,6 @@ export default class Entity<T extends EntityData = EntityData>
291
322
 
292
323
  this.$data = new Proxy(this.$dataStore, this.$dataHandler);
293
324
 
294
- if (guid != null) {
295
- const entity = this.$nymph.driver.getEntitySync(
296
- { class: this.constructor as EntityConstructor },
297
- { type: '&', guid }
298
- );
299
- if (entity) {
300
- this.guid = entity.guid;
301
- this.tags = entity.tags;
302
- this.cdate = entity.cdate;
303
- this.mdate = entity.mdate;
304
- this.$putData(entity.$getData(), entity.$getSData());
305
- }
306
- }
307
-
308
325
  return new Proxy(this, {
309
326
  has: (entity: Entity, name: string) => {
310
327
  if (
@@ -315,7 +332,7 @@ export default class Entity<T extends EntityData = EntityData>
315
332
  return name in entity;
316
333
  }
317
334
 
318
- this.$referenceWake();
335
+ this.$check();
319
336
  return name in entity.$data;
320
337
  },
321
338
 
@@ -326,12 +343,12 @@ export default class Entity<T extends EntityData = EntityData>
326
343
  name.substring(0, 1) === '$'
327
344
  ) {
328
345
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
329
- this.$referenceWake();
346
+ this.$check();
330
347
  }
331
348
  return (entity as any)[name];
332
349
  }
333
350
 
334
- this.$referenceWake();
351
+ this.$check();
335
352
  if (name in entity.$data) {
336
353
  return entity.$data[name];
337
354
  }
@@ -345,11 +362,11 @@ export default class Entity<T extends EntityData = EntityData>
345
362
  name.substring(0, 1) === '$'
346
363
  ) {
347
364
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
348
- this.$referenceWake();
365
+ this.$check();
349
366
  }
350
367
  (entity as any)[name] = value;
351
368
  } else {
352
- this.$referenceWake();
369
+ this.$check();
353
370
  (entity.$data as any)[name] = value;
354
371
  }
355
372
  return true;
@@ -359,7 +376,7 @@ export default class Entity<T extends EntityData = EntityData>
359
376
  if (name in entity) {
360
377
  return delete (entity as any)[name];
361
378
  } else if (name in entity.$data) {
362
- this.$referenceWake();
379
+ this.$check();
363
380
  return delete entity.$data[name];
364
381
  }
365
382
  return true;
@@ -380,11 +397,11 @@ export default class Entity<T extends EntityData = EntityData>
380
397
  name.substring(0, 1) === '$'
381
398
  ) {
382
399
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
383
- this.$referenceWake();
400
+ this.$check();
384
401
  }
385
402
  Object.defineProperty(entity, name, descriptor);
386
403
  } else {
387
- this.$referenceWake();
404
+ this.$check();
388
405
  Object.defineProperty(entity.$data, name, descriptor);
389
406
  }
390
407
  return true;
@@ -397,17 +414,17 @@ export default class Entity<T extends EntityData = EntityData>
397
414
  name.substring(0, 1) === '$'
398
415
  ) {
399
416
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
400
- this.$referenceWake();
417
+ this.$check();
401
418
  }
402
419
  return Object.getOwnPropertyDescriptor(entity, name);
403
420
  } else {
404
- this.$referenceWake();
421
+ this.$check();
405
422
  return Object.getOwnPropertyDescriptor(entity.$data, name);
406
423
  }
407
424
  },
408
425
 
409
426
  ownKeys: (entity: Entity) => {
410
- this.$referenceWake();
427
+ this.$check();
411
428
  return Object.getOwnPropertyNames(entity).concat(
412
429
  Object.getOwnPropertyNames(entity.$data)
413
430
  );
@@ -431,8 +448,8 @@ export default class Entity<T extends EntityData = EntityData>
431
448
  return entity;
432
449
  }
433
450
 
434
- public static factorySync(guid?: string) {
435
- return new this(guid);
451
+ public static factorySync() {
452
+ return new this();
436
453
  }
437
454
 
438
455
  public static factoryReference(reference: EntityReference) {
@@ -463,7 +480,7 @@ export default class Entity<T extends EntityData = EntityData>
463
480
  }
464
481
 
465
482
  public $addTag(...tags: string[]) {
466
- this.$referenceWake();
483
+ this.$check();
467
484
 
468
485
  if (tags.length < 1) {
469
486
  return;
@@ -472,7 +489,7 @@ export default class Entity<T extends EntityData = EntityData>
472
489
  }
473
490
 
474
491
  public $arraySearch(array: any[], strict = false) {
475
- this.$referenceWake();
492
+ this.$check();
476
493
 
477
494
  if (!Array.isArray(array)) {
478
495
  return -1;
@@ -487,7 +504,7 @@ export default class Entity<T extends EntityData = EntityData>
487
504
  }
488
505
 
489
506
  public $clearCache() {
490
- this.$referenceWake();
507
+ this.$check();
491
508
 
492
509
  this.$putData(this.$getData(), this.$getSData());
493
510
  }
@@ -497,13 +514,13 @@ export default class Entity<T extends EntityData = EntityData>
497
514
  }
498
515
 
499
516
  public async $delete(): Promise<boolean> {
500
- this.$referenceWake();
517
+ this.$check();
501
518
 
502
519
  return await this.$nymph.deleteEntity(this);
503
520
  }
504
521
 
505
522
  public $equals(object: any) {
506
- this.$referenceWake();
523
+ this.$check();
507
524
 
508
525
  if (!(object instanceof Entity)) {
509
526
  return false;
@@ -513,22 +530,24 @@ export default class Entity<T extends EntityData = EntityData>
513
530
  return false;
514
531
  }
515
532
  }
516
- if (object.constructor !== this.constructor) {
517
- return false;
518
- }
519
533
  if (object.cdate !== this.cdate) {
520
534
  return false;
521
535
  }
522
536
  if (object.mdate !== this.mdate) {
523
537
  return false;
524
538
  }
539
+ const obTags = [...object.tags].sort();
540
+ const myTags = [...this.tags].sort();
541
+ if (!isEqual(obTags, myTags)) {
542
+ return false;
543
+ }
525
544
  const obData = object.$getData(true);
526
545
  const myData = this.$getData(true);
527
546
  return isEqual(obData, myData);
528
547
  }
529
548
 
530
549
  public $getData(includeSData = false) {
531
- this.$referenceWake();
550
+ this.$check();
532
551
 
533
552
  if (includeSData) {
534
553
  // Access all the serialized properties to initialize them.
@@ -540,30 +559,56 @@ export default class Entity<T extends EntityData = EntityData>
540
559
  }
541
560
 
542
561
  public $getSData() {
543
- this.$referenceWake();
562
+ this.$check();
544
563
 
545
564
  return this.$sdata;
546
565
  }
547
566
 
548
567
  public $getOriginalAcValues() {
549
- this.$referenceWake();
550
-
551
- return (
552
- this.$originalAcValues ?? {
553
- user: this.$data.user,
554
- group: this.$data.group,
555
- acUser: this.$data.acUser,
556
- acGroup: this.$data.acGroup,
557
- acOther: this.$data.acOther,
558
- acRead: this.$data.acRead,
559
- acWrite: this.$data.acWrite,
560
- acFull: this.$data.acFull,
561
- }
562
- );
568
+ this.$check();
569
+
570
+ return this.$originalAcValues ?? this.$getCurrentAcValues();
571
+ }
572
+
573
+ public $getCurrentAcValues() {
574
+ this.$check();
575
+
576
+ return {
577
+ user: this.$getAcUid(),
578
+ group: this.$getAcGid(),
579
+ acUser: this.$data.acUser,
580
+ acGroup: this.$data.acGroup,
581
+ acOther: this.$data.acOther,
582
+ acRead: this.$getAcReadIds(),
583
+ acWrite: this.$getAcWriteIds(),
584
+ acFull: this.$getAcFullIds(),
585
+ };
586
+ }
587
+
588
+ public $getAcUid() {
589
+ return this.$data.user?.guid as string | null;
590
+ }
591
+ public $getAcGid() {
592
+ return this.$data.group?.guid as string | null;
593
+ }
594
+ public $getAcReadIds() {
595
+ return this.$data.acRead?.map((entity: EntityInterface) => entity.guid) as
596
+ | string[]
597
+ | null;
598
+ }
599
+ public $getAcWriteIds() {
600
+ return this.$data.acWrite?.map((entity: EntityInterface) => entity.guid) as
601
+ | string[]
602
+ | null;
603
+ }
604
+ public $getAcFullIds() {
605
+ return this.$data.acFull?.map((entity: EntityInterface) => entity.guid) as
606
+ | string[]
607
+ | null;
563
608
  }
564
609
 
565
610
  public $getValidatable() {
566
- this.$referenceWake();
611
+ this.$check();
567
612
 
568
613
  // Access all the serialized properties to initialize them.
569
614
  for (const key in this.$sdata) {
@@ -579,13 +624,13 @@ export default class Entity<T extends EntityData = EntityData>
579
624
  }
580
625
 
581
626
  public $getTags() {
582
- this.$referenceWake();
627
+ this.$check();
583
628
 
584
629
  return this.tags;
585
630
  }
586
631
 
587
632
  public $hasTag(...tags: string[]) {
588
- this.$referenceWake();
633
+ this.$check();
589
634
 
590
635
  if (!tags.length) {
591
636
  return false;
@@ -611,10 +656,15 @@ export default class Entity<T extends EntityData = EntityData>
611
656
  return this.guid === object.guid;
612
657
  }
613
658
 
614
- this.$referenceWake();
659
+ this.$check();
615
660
  if (typeof object.$getData !== 'function') {
616
661
  return false;
617
662
  } else {
663
+ const obTags = [...object.tags].sort();
664
+ const myTags = [...this.tags].sort();
665
+ if (!isEqual(obTags, myTags)) {
666
+ return false;
667
+ }
618
668
  const obData = object.$getData(true);
619
669
  const myData = this.$getData(true);
620
670
  return isEqual(obData, myData);
@@ -622,8 +672,7 @@ export default class Entity<T extends EntityData = EntityData>
622
672
  }
623
673
 
624
674
  public $jsonAcceptData(input: EntityJson, allowConflict = false) {
625
- // TODO: Do this without causing everything to become unserialized.
626
- this.$referenceWake();
675
+ this.$check();
627
676
 
628
677
  if (this.guid != input.guid) {
629
678
  throw new EntityConflictError(
@@ -665,12 +714,27 @@ export default class Entity<T extends EntityData = EntityData>
665
714
 
666
715
  const protectedData: EntityData = {};
667
716
  const protectedProps = [...this.$protectedData];
668
- if (
669
- this.$nymph.tilmeld &&
670
- !this.$nymph.tilmeld.gatekeeper('tilmeld/admin')
671
- ) {
672
- protectedProps.push('user');
673
- protectedProps.push('group');
717
+ if (this.$nymph.tilmeld) {
718
+ protectedProps.push('acUser');
719
+ protectedProps.push('acGroup');
720
+ protectedProps.push('acOther');
721
+ protectedProps.push('acRead');
722
+ protectedProps.push('acWrite');
723
+ protectedProps.push('acFull');
724
+ if (
725
+ ((this.constructor as typeof Entity).class !== 'User' &&
726
+ (this.constructor as typeof Entity).class !== 'Group') ||
727
+ !(this.$nymph.tilmeld as any).currentUser ||
728
+ (!(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
729
+ 'tilmeld/admin'
730
+ ) &&
731
+ !(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
732
+ 'system/admin'
733
+ ))
734
+ ) {
735
+ protectedProps.push('user');
736
+ protectedProps.push('group');
737
+ }
674
738
  }
675
739
  for (const name of protectedProps) {
676
740
  if (name in this.$data) {
@@ -703,7 +767,7 @@ export default class Entity<T extends EntityData = EntityData>
703
767
  }
704
768
 
705
769
  public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
706
- this.$referenceWake();
770
+ this.$check();
707
771
 
708
772
  if (this.guid != patch.guid) {
709
773
  throw new EntityConflictError(
@@ -719,11 +783,35 @@ export default class Entity<T extends EntityData = EntityData>
719
783
  }
720
784
  this.mdate = patch.mdate;
721
785
 
786
+ const protectedProps = [...this.$protectedData];
787
+ if (this.$nymph.tilmeld) {
788
+ protectedProps.push('acUser');
789
+ protectedProps.push('acGroup');
790
+ protectedProps.push('acOther');
791
+ protectedProps.push('acRead');
792
+ protectedProps.push('acWrite');
793
+ protectedProps.push('acFull');
794
+ if (
795
+ ((this.constructor as typeof Entity).class !== 'User' &&
796
+ (this.constructor as typeof Entity).class !== 'Group') ||
797
+ !(this.$nymph.tilmeld as any).currentUser ||
798
+ (!(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
799
+ 'tilmeld/admin'
800
+ ) &&
801
+ !(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
802
+ 'system/admin'
803
+ ))
804
+ ) {
805
+ protectedProps.push('user');
806
+ protectedProps.push('group');
807
+ }
808
+ }
809
+
722
810
  for (const name in patch.set) {
723
811
  if (
724
812
  (this.$allowlistData != null &&
725
813
  this.$allowlistData.indexOf(name) === -1) ||
726
- this.$protectedData.indexOf(name) !== -1 ||
814
+ protectedProps.indexOf(name) !== -1 ||
727
815
  this.$privateData.indexOf(name) !== -1
728
816
  ) {
729
817
  continue;
@@ -739,7 +827,7 @@ export default class Entity<T extends EntityData = EntityData>
739
827
  if (
740
828
  (this.$allowlistData != null &&
741
829
  this.$allowlistData.indexOf(name) === -1) ||
742
- this.$protectedData.indexOf(name) !== -1 ||
830
+ protectedProps.indexOf(name) !== -1 ||
743
831
  this.$privateData.indexOf(name) !== -1
744
832
  ) {
745
833
  continue;
@@ -771,7 +859,7 @@ export default class Entity<T extends EntityData = EntityData>
771
859
  }
772
860
 
773
861
  public $putData(data: EntityData, sdata?: SerializedEntityData) {
774
- this.$referenceWake();
862
+ this.$check();
775
863
 
776
864
  const mySdata = sdata ?? this.$getSData();
777
865
  for (const name in data) {
@@ -790,7 +878,7 @@ export default class Entity<T extends EntityData = EntityData>
790
878
  this.$sdata = mySdata;
791
879
  // Set original AC values if not set..
792
880
  if (this.$originalAcValues == null) {
793
- this.$originalAcValues = this.$getOriginalAcValues();
881
+ this.$originalAcValues = this.$getCurrentAcValues();
794
882
  }
795
883
  }
796
884
 
@@ -822,45 +910,133 @@ export default class Entity<T extends EntityData = EntityData>
822
910
  this.$sleepingReference = reference;
823
911
  }
824
912
 
913
+ /**
914
+ * Check if this is a sleeping reference and throw an error if so.
915
+ */
916
+ protected $check() {
917
+ if (this.$isASleepingReference || this.$sleepingReference != null) {
918
+ throw new EntityIsSleepingReferenceError(
919
+ 'This entity is in a sleeping reference state. You must use .$wake() to wake it.'
920
+ );
921
+ }
922
+ }
923
+
924
+ /**
925
+ * Check if this is a sleeping reference.
926
+ */
927
+ public $asleep() {
928
+ return this.$isASleepingReference || this.$sleepingReference != null;
929
+ }
930
+
825
931
  /**
826
932
  * Wake from a sleeping reference.
827
- *
828
- * @returns True on success, false on failure.
829
933
  */
830
- protected $referenceWake() {
934
+ public $wake() {
831
935
  if (!this.$isASleepingReference || this.$sleepingReference == null) {
832
- return true;
936
+ this.$wakePromise = null;
937
+ return Promise.resolve(this);
833
938
  }
834
- const EntityClass = this.$nymph.getEntityClass(this.$sleepingReference[2]);
835
- const entity = this.$nymph.driver.getEntitySync(
836
- {
837
- class: EntityClass,
838
- skipAc: this.$skipAc,
839
- },
840
- { type: '&', guid: this.$sleepingReference[1] }
841
- );
842
- if (entity == null) {
843
- return false;
939
+ if (this.$sleepingReference?.[1] == null) {
940
+ throw new InvalidStateError(
941
+ 'Tried to wake a sleeping reference with no GUID.'
942
+ );
844
943
  }
845
- this.$isASleepingReference = false;
846
- this.$sleepingReference = null;
847
- this.guid = entity.guid;
848
- this.tags = entity.tags;
849
- this.cdate = entity.cdate;
850
- this.mdate = entity.mdate;
851
- this.$putData(entity.$getData(), entity.$getSData());
852
- return true;
944
+ if (!this.$wakePromise) {
945
+ const EntityClass = this.$nymph.getEntityClass(
946
+ this.$sleepingReference[2]
947
+ );
948
+ this.$wakePromise = this.$nymph
949
+ .getEntity(
950
+ {
951
+ class: EntityClass,
952
+ skipAc: this.$skipAc,
953
+ },
954
+ { type: '&', guid: this.$sleepingReference[1] }
955
+ )
956
+ .then((entity) => {
957
+ if (entity == null || entity.guid == null) {
958
+ return Promise.reject(
959
+ new InvalidStateError(
960
+ 'The sleeping reference could not be retrieved.'
961
+ )
962
+ );
963
+ }
964
+ this.$isASleepingReference = false;
965
+ this.$sleepingReference = null;
966
+ this.guid = entity.guid;
967
+ this.tags = entity.tags;
968
+ this.cdate = entity.cdate;
969
+ this.mdate = entity.mdate;
970
+ this.$putData(entity.$getData(), entity.$getSData());
971
+
972
+ return this;
973
+ })
974
+ .finally(() => {
975
+ this.$wakePromise = null;
976
+ });
977
+ }
978
+ return this.$wakePromise;
979
+ }
980
+
981
+ public $wakeAll(level?: number) {
982
+ return new Promise((resolve, reject) => {
983
+ // Run this once this entity is wake.
984
+ const wakeProps = () => {
985
+ let newLevel;
986
+ // If level is undefined, keep going forever, otherwise, stop once we've
987
+ // gone deep enough.
988
+ if (level !== undefined) {
989
+ newLevel = level - 1;
990
+ }
991
+ if (newLevel !== undefined && newLevel < 0) {
992
+ resolve(this);
993
+ return;
994
+ }
995
+ const promises = [];
996
+ // Go through data looking for entities to wake.
997
+ for (let [key, value] of Object.entries(this.$data)) {
998
+ if (value instanceof Entity && value.$isASleepingReference) {
999
+ promises.push(value.$wakeAll(newLevel));
1000
+ } else if (Array.isArray(value)) {
1001
+ for (let i = 0; i < value.length; i++) {
1002
+ if (
1003
+ value[i] instanceof Entity &&
1004
+ value[i].$isASleepingReference
1005
+ ) {
1006
+ promises.push(value[i].$wakeAll(newLevel));
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+ if (promises.length) {
1012
+ Promise.all(promises).then(
1013
+ () => resolve(this),
1014
+ (errObj) => reject(errObj)
1015
+ );
1016
+ } else {
1017
+ resolve(this);
1018
+ }
1019
+ };
1020
+
1021
+ if (this.$isASleepingReference) {
1022
+ this.$wake().then(wakeProps, (errObj) => reject(errObj));
1023
+ } else {
1024
+ wakeProps();
1025
+ }
1026
+ }) as Promise<Entity<T>>;
853
1027
  }
854
1028
 
855
1029
  public async $refresh() {
856
- this.$referenceWake();
1030
+ await this.$wake();
857
1031
 
858
1032
  if (this.guid == null) {
859
1033
  return false;
860
1034
  }
861
1035
  const refresh = await this.$nymph.getEntity(
862
1036
  {
863
- class: this.constructor as EntityConstructor,
1037
+ class: this.$nymph.getEntityClass(
1038
+ this.constructor as EntityConstructor
1039
+ ),
864
1040
  skipCache: true,
865
1041
  skipAc: this.$skipAc,
866
1042
  },
@@ -877,13 +1053,13 @@ export default class Entity<T extends EntityData = EntityData>
877
1053
  }
878
1054
 
879
1055
  public $removeTag(...tags: string[]) {
880
- this.$referenceWake();
1056
+ this.$check();
881
1057
 
882
1058
  this.tags = difference(this.tags, tags);
883
1059
  }
884
1060
 
885
1061
  public async $save(): Promise<boolean> {
886
- this.$referenceWake();
1062
+ await this.$wake();
887
1063
 
888
1064
  return await this.$nymph.saveEntity(this);
889
1065
  }