@nymphjs/nymph 1.0.0-beta.5 → 1.0.0-beta.51

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 (50) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/README.md +15 -15
  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 +85 -105
  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/QueryFailedError.js.map +1 -1
  24. package/dist/errors/index.d.ts +1 -0
  25. package/dist/errors/index.js +1 -0
  26. package/dist/errors/index.js.map +1 -1
  27. package/dist/testArtifacts.d.ts +10 -3
  28. package/dist/testArtifacts.js +20 -11
  29. package/dist/testArtifacts.js.map +1 -1
  30. package/dist/testMocks.d.ts +10 -7
  31. package/dist/testMocks.js +22 -10
  32. package/dist/testMocks.js.map +1 -1
  33. package/dist/utils.d.ts +1 -1
  34. package/dist/utils.js +11 -5
  35. package/dist/utils.js.map +1 -1
  36. package/package.json +15 -15
  37. package/src/Entity.test.ts +33 -11
  38. package/src/Entity.ts +303 -127
  39. package/src/Entity.types.ts +13 -1
  40. package/src/Nymph.ts +392 -88
  41. package/src/Nymph.types.ts +95 -24
  42. package/src/driver/NymphDriver.ts +203 -282
  43. package/src/driver/testArtifacts.ts +238 -157
  44. package/src/errors/InvalidStateError.ts +6 -0
  45. package/src/errors/index.ts +2 -0
  46. package/src/nymphJoiProps.ts +2 -2
  47. package/src/testArtifacts.ts +28 -10
  48. package/src/testMocks.ts +42 -25
  49. package/src/utils.ts +22 -8
  50. 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,26 +231,25 @@ 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(
219
250
  JSON.parse(this.$sdata[name]),
220
251
  this.$nymph,
221
- this.$skipAc
252
+ this.$skipAc,
222
253
  );
223
254
  delete this.$sdata[name];
224
255
  }
@@ -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];
@@ -253,9 +284,9 @@ export default class Entity<T extends EntityData = EntityData>
253
284
  defineProperty: (
254
285
  data: EntityData,
255
286
  name: string,
256
- descriptor: PropertyDescriptor
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,13 +296,13 @@ 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(
272
303
  JSON.parse(this.$sdata[name]),
273
304
  this.$nymph,
274
- this.$skipAc
305
+ this.$skipAc,
275
306
  );
276
307
  delete this.$sdata[name];
277
308
  }
@@ -279,10 +310,10 @@ 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
- Object.getOwnPropertyNames(this.$sdata)
316
+ Object.getOwnPropertyNames(this.$sdata),
286
317
  );
287
318
  },
288
319
  };
@@ -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;
@@ -372,7 +389,7 @@ export default class Entity<T extends EntityData = EntityData>
372
389
  defineProperty: (
373
390
  entity: Entity,
374
391
  name: string,
375
- descriptor: PropertyDescriptor
392
+ descriptor: PropertyDescriptor,
376
393
  ) => {
377
394
  if (
378
395
  typeof name !== 'string' ||
@@ -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,19 +414,19 @@ 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
- Object.getOwnPropertyNames(entity.$data)
429
+ Object.getOwnPropertyNames(entity.$data),
413
430
  );
414
431
  },
415
432
  }) as Entity<T>;
@@ -422,7 +439,7 @@ export default class Entity<T extends EntityData = EntityData>
422
439
  {
423
440
  class: this as any as EntityConstructor,
424
441
  },
425
- { type: '&', guid }
442
+ { type: '&', guid },
426
443
  );
427
444
  if (entity != null) {
428
445
  return entity;
@@ -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,12 +672,11 @@ 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(
630
- 'Tried to accept JSON input for the wrong entity.'
679
+ 'Tried to accept JSON input for the wrong entity.',
631
680
  );
632
681
  }
633
682
 
@@ -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,11 +767,11 @@ 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(
710
- 'Tried to accept JSON patch for the wrong entity.'
774
+ 'Tried to accept JSON patch for the wrong entity.',
711
775
  );
712
776
  }
713
777
 
@@ -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;
@@ -731,7 +819,7 @@ export default class Entity<T extends EntityData = EntityData>
731
819
  (this.$data as any)[name] = referencesToEntities(
732
820
  patch.set[name],
733
821
  this.$nymph,
734
- this.$skipAc
822
+ this.$skipAc,
735
823
  );
736
824
  }
737
825
 
@@ -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) {
@@ -784,13 +872,13 @@ export default class Entity<T extends EntityData = EntityData>
784
872
  (this.$dataStore as any)[name] = referencesToEntities(
785
873
  data[name],
786
874
  this.$nymph,
787
- this.$skipAc
875
+ this.$skipAc,
788
876
  );
789
877
  }
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
 
@@ -807,14 +895,14 @@ export default class Entity<T extends EntityData = EntityData>
807
895
  ) {
808
896
  throw new InvalidParametersError(
809
897
  'referenceSleep expects parameter 1 to be a valid Nymph entity ' +
810
- 'reference.'
898
+ 'reference.',
811
899
  );
812
900
  }
813
901
  const thisClass: string = (this.constructor as any).class;
814
902
  if (reference[2] !== thisClass) {
815
903
  throw new InvalidParametersError(
816
904
  'referenceSleep can only be called with an entity reference of the ' +
817
- `same class. Given class: ${reference[2]}; this class: ${thisClass}.`
905
+ `same class. Given class: ${reference[2]}; this class: ${thisClass}.`,
818
906
  );
819
907
  }
820
908
  this.$isASleepingReference = true;
@@ -822,49 +910,137 @@ 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
  },
867
- { type: '&', guid: this.guid }
1043
+ { type: '&', guid: this.guid },
868
1044
  );
869
1045
  if (refresh == null) {
870
1046
  return 0;
@@ -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
  }