@nymphjs/nymph 1.0.0-beta.8 → 1.0.0-beta.80

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 (65) hide show
  1. package/CHANGELOG.md +376 -0
  2. package/README.md +9 -19
  3. package/dist/Entity.d.ts +269 -7
  4. package/dist/Entity.js +491 -105
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Entity.test.js +25 -3
  7. package/dist/Entity.test.js.map +1 -1
  8. package/dist/Entity.types.d.ts +225 -19
  9. package/dist/Nymph.d.ts +430 -11
  10. package/dist/Nymph.js +686 -168
  11. package/dist/Nymph.js.map +1 -1
  12. package/dist/Nymph.types.d.ts +65 -7
  13. package/dist/Nymph.types.js +2 -1
  14. package/dist/Nymph.types.js.map +1 -1
  15. package/dist/conf/d.d.ts +30 -0
  16. package/dist/conf/defaults.js +27 -0
  17. package/dist/conf/defaults.js.map +1 -1
  18. package/dist/driver/NymphDriver.d.ts +136 -38
  19. package/dist/driver/NymphDriver.js +264 -131
  20. package/dist/driver/NymphDriver.js.map +1 -1
  21. package/dist/driver/testArtifacts.d.ts +1 -1
  22. package/dist/driver/testArtifacts.js +493 -51
  23. package/dist/driver/testArtifacts.js.map +1 -1
  24. package/dist/errors/EntityInvalidDataError.d.ts +6 -0
  25. package/dist/errors/EntityInvalidDataError.js +7 -1
  26. package/dist/errors/EntityInvalidDataError.js.map +1 -1
  27. package/dist/errors/EntityUniqueConstraintError.d.ts +3 -0
  28. package/dist/errors/EntityUniqueConstraintError.js +11 -0
  29. package/dist/errors/EntityUniqueConstraintError.js.map +1 -0
  30. package/dist/errors/InvalidStateError.d.ts +3 -0
  31. package/dist/errors/InvalidStateError.js +11 -0
  32. package/dist/errors/InvalidStateError.js.map +1 -0
  33. package/dist/errors/QueryFailedError.js +1 -0
  34. package/dist/errors/QueryFailedError.js.map +1 -1
  35. package/dist/errors/index.d.ts +2 -0
  36. package/dist/errors/index.js +2 -0
  37. package/dist/errors/index.js.map +1 -1
  38. package/dist/testArtifacts.d.ts +17 -5
  39. package/dist/testArtifacts.js +31 -27
  40. package/dist/testArtifacts.js.map +1 -1
  41. package/dist/testMocks.d.ts +13 -10
  42. package/dist/testMocks.js +32 -13
  43. package/dist/testMocks.js.map +1 -1
  44. package/dist/utils.d.ts +1 -1
  45. package/dist/utils.js +21 -5
  46. package/dist/utils.js.map +1 -1
  47. package/package.json +16 -14
  48. package/src/Entity.test.ts +22 -5
  49. package/src/Entity.ts +423 -134
  50. package/src/Entity.types.ts +32 -66
  51. package/src/Nymph.ts +609 -176
  52. package/src/Nymph.types.ts +131 -26
  53. package/src/conf/d.ts +12 -0
  54. package/src/conf/defaults.ts +31 -0
  55. package/src/driver/NymphDriver.ts +289 -311
  56. package/src/driver/testArtifacts.ts +562 -181
  57. package/src/errors/EntityUniqueConstraintError.ts +6 -0
  58. package/src/errors/InvalidStateError.ts +6 -0
  59. package/src/errors/index.ts +4 -0
  60. package/src/nymphJoiProps.ts +2 -2
  61. package/src/testArtifacts.ts +18 -19
  62. package/src/testMocks.ts +55 -30
  63. package/src/utils.ts +22 -8
  64. package/tsconfig.json +3 -3
  65. package/typedoc.json +4 -0
package/src/Entity.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { difference, intersection, isEqual } from 'lodash';
2
2
 
3
3
  import type Nymph from './Nymph';
4
- import {
4
+ import type { Options } from './Nymph.types';
5
+ import type {
5
6
  ACProperties,
6
7
  EntityConstructor,
7
8
  EntityData,
@@ -11,13 +12,25 @@ import {
11
12
  EntityReference,
12
13
  SerializedEntityData,
13
14
  } from './Entity.types';
14
- import { EntityConflictError, InvalidParametersError } from './errors';
15
+ import {
16
+ EntityConflictError,
17
+ EntityIsSleepingReferenceError,
18
+ InvalidParametersError,
19
+ InvalidStateError,
20
+ } from './errors';
15
21
  import {
16
22
  entitiesToReferences,
17
23
  referencesToEntities,
18
24
  uniqueStrings,
19
25
  } from './utils';
20
26
 
27
+ export type EntityDataType<T> = T extends Entity<infer DataType>
28
+ ? DataType
29
+ : never;
30
+
31
+ export type EntityInstanceType<T extends EntityConstructor> =
32
+ T extends new () => infer E ? E & EntityDataType<E> : never;
33
+
21
34
  /**
22
35
  * Database abstraction object.
23
36
  *
@@ -45,9 +58,9 @@ import {
45
58
  *
46
59
  * Some notes about $equals() and $is(), the replacements for "==":
47
60
  *
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.
61
+ * The == operator will likely not give you the result you want, since two
62
+ * instances of the same entity will fail that check, even though they represent
63
+ * the same data in the database.
51
64
  *
52
65
  * $equals() performs a more strict comparison of the entity to another. Use
53
66
  * $equals() instead of the == operator when you want to check both the entities
@@ -56,8 +69,7 @@ import {
56
69
  *
57
70
  * - They must be entities.
58
71
  * - 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.
72
+ * - Their data and tags must be equal.
61
73
  *
62
74
  * $is() performs a less strict comparison of the entity to another. Use $is()
63
75
  * instead of the == operator when the entity's data may have been changed, but
@@ -66,7 +78,7 @@ import {
66
78
  *
67
79
  * - They must be entities.
68
80
  * - They must have equal GUIDs, or both must have no GUID.
69
- * - If they have no GUIDs, their data must be equal.
81
+ * - If they have no GUIDs, their data and tags must be equal.
70
82
  *
71
83
  * Some notes about saving entities in other entity's properties:
72
84
  *
@@ -137,6 +149,10 @@ export default class Entity<T extends EntityData = EntityData>
137
149
  * The reference to use to wake.
138
150
  */
139
151
  protected $sleepingReference: EntityReference | null = null;
152
+ /**
153
+ * A promise that resolved when the entity's data is wake.
154
+ */
155
+ protected $wakePromise: Promise<Entity<T>> | null = null;
140
156
  /**
141
157
  * Properties that will not be serialized into JSON with toJSON(). This
142
158
  * can be considered a denylist, because these properties will not be set
@@ -146,6 +162,18 @@ export default class Entity<T extends EntityData = EntityData>
146
162
  * also listed in searchRestrictedData.
147
163
  */
148
164
  protected $privateData: string[] = [];
165
+ /**
166
+ * Whether this entity should publish changes to PubSub servers.
167
+ */
168
+ public static pubSubEnabled = true;
169
+ /**
170
+ * Whether this entity should be accessible on the frontend through the REST
171
+ * server.
172
+ *
173
+ * If this is false, any request from the client that attempts to use this
174
+ * entity will fail.
175
+ */
176
+ public static restEnabled = true;
149
177
  /**
150
178
  * Properties that will not be searchable from the frontend. If the frontend
151
179
  * includes any of these properties in any of their clauses, they will be
@@ -157,8 +185,20 @@ export default class Entity<T extends EntityData = EntityData>
157
185
  * be visible on the frontend, unlike $privateData, but any changes to them
158
186
  * that come from the frontend will be ignored.
159
187
  *
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.
188
+ * In addition to what's listed here, all of the access control properties
189
+ * will be included when Tilmeld is being used. These are:
190
+ *
191
+ * - acUser
192
+ * - acGroup
193
+ * - acOther
194
+ * - acRead
195
+ * - acWrite
196
+ * - acFull
197
+ * - user
198
+ * - group
199
+ *
200
+ * You should modify these through client enabled methods or the $save method
201
+ * instead, for safety.
162
202
  */
163
203
  protected $protectedData: string[] = [];
164
204
  /**
@@ -199,26 +239,33 @@ export default class Entity<T extends EntityData = EntityData>
199
239
  private $originalAcValues: ACProperties | null = null;
200
240
 
201
241
  /**
202
- * Load an entity.
203
- * @param guid The ID of the entity to load, undefined for a new entity.
242
+ * Alter the options for a query for this entity.
243
+ *
244
+ * @param options The current options.
245
+ * @returns The altered options.
204
246
  */
205
- public constructor(guid?: string) {
247
+ static alterOptions?<T extends Options>(options: T): T;
248
+
249
+ /**
250
+ * Initialize an entity.
251
+ */
252
+ public constructor(..._rest: any[]) {
206
253
  this.$nymph = (this.constructor as EntityConstructor).nymph;
207
254
  this.$dataHandler = {
208
255
  has: (data: EntityData, name: string) => {
209
- this.$referenceWake();
256
+ this.$check();
210
257
 
211
258
  return name in data || name in this.$sdata;
212
259
  },
213
260
 
214
261
  get: (data: EntityData, name: string) => {
215
- this.$referenceWake();
262
+ this.$check();
216
263
 
217
264
  if (this.$sdata.hasOwnProperty(name)) {
218
265
  data[name] = referencesToEntities(
219
266
  JSON.parse(this.$sdata[name]),
220
267
  this.$nymph,
221
- this.$skipAc
268
+ this.$skipAc,
222
269
  );
223
270
  delete this.$sdata[name];
224
271
  }
@@ -229,7 +276,7 @@ export default class Entity<T extends EntityData = EntityData>
229
276
  },
230
277
 
231
278
  set: (data: EntityData, name: string, value: any) => {
232
- this.$referenceWake();
279
+ this.$check();
233
280
 
234
281
  if (this.$sdata.hasOwnProperty(name)) {
235
282
  delete this.$sdata[name];
@@ -239,7 +286,7 @@ export default class Entity<T extends EntityData = EntityData>
239
286
  },
240
287
 
241
288
  deleteProperty: (data: EntityData, name: string) => {
242
- this.$referenceWake();
289
+ this.$check();
243
290
 
244
291
  if (this.$sdata.hasOwnProperty(name)) {
245
292
  return delete this.$sdata[name];
@@ -253,9 +300,9 @@ export default class Entity<T extends EntityData = EntityData>
253
300
  defineProperty: (
254
301
  data: EntityData,
255
302
  name: string,
256
- descriptor: PropertyDescriptor
303
+ descriptor: PropertyDescriptor,
257
304
  ) => {
258
- this.$referenceWake();
305
+ this.$check();
259
306
 
260
307
  if (this.$sdata.hasOwnProperty(name)) {
261
308
  delete this.$sdata[name];
@@ -265,13 +312,13 @@ export default class Entity<T extends EntityData = EntityData>
265
312
  },
266
313
 
267
314
  getOwnPropertyDescriptor: (data: EntityData, name: string) => {
268
- this.$referenceWake();
315
+ this.$check();
269
316
 
270
317
  if (this.$sdata.hasOwnProperty(name)) {
271
318
  data[name] = referencesToEntities(
272
319
  JSON.parse(this.$sdata[name]),
273
320
  this.$nymph,
274
- this.$skipAc
321
+ this.$skipAc,
275
322
  );
276
323
  delete this.$sdata[name];
277
324
  }
@@ -279,10 +326,10 @@ export default class Entity<T extends EntityData = EntityData>
279
326
  },
280
327
 
281
328
  ownKeys: (data: EntityData) => {
282
- this.$referenceWake();
329
+ this.$check();
283
330
 
284
331
  return Object.getOwnPropertyNames(data).concat(
285
- Object.getOwnPropertyNames(this.$sdata)
332
+ Object.getOwnPropertyNames(this.$sdata),
286
333
  );
287
334
  },
288
335
  };
@@ -291,20 +338,6 @@ export default class Entity<T extends EntityData = EntityData>
291
338
 
292
339
  this.$data = new Proxy(this.$dataStore, this.$dataHandler);
293
340
 
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
341
  return new Proxy(this, {
309
342
  has: (entity: Entity, name: string) => {
310
343
  if (
@@ -315,7 +348,7 @@ export default class Entity<T extends EntityData = EntityData>
315
348
  return name in entity;
316
349
  }
317
350
 
318
- this.$referenceWake();
351
+ this.$check();
319
352
  return name in entity.$data;
320
353
  },
321
354
 
@@ -326,12 +359,12 @@ export default class Entity<T extends EntityData = EntityData>
326
359
  name.substring(0, 1) === '$'
327
360
  ) {
328
361
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
329
- this.$referenceWake();
362
+ this.$check();
330
363
  }
331
364
  return (entity as any)[name];
332
365
  }
333
366
 
334
- this.$referenceWake();
367
+ this.$check();
335
368
  if (name in entity.$data) {
336
369
  return entity.$data[name];
337
370
  }
@@ -345,11 +378,11 @@ export default class Entity<T extends EntityData = EntityData>
345
378
  name.substring(0, 1) === '$'
346
379
  ) {
347
380
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
348
- this.$referenceWake();
381
+ this.$check();
349
382
  }
350
383
  (entity as any)[name] = value;
351
384
  } else {
352
- this.$referenceWake();
385
+ this.$check();
353
386
  (entity.$data as any)[name] = value;
354
387
  }
355
388
  return true;
@@ -359,7 +392,7 @@ export default class Entity<T extends EntityData = EntityData>
359
392
  if (name in entity) {
360
393
  return delete (entity as any)[name];
361
394
  } else if (name in entity.$data) {
362
- this.$referenceWake();
395
+ this.$check();
363
396
  return delete entity.$data[name];
364
397
  }
365
398
  return true;
@@ -372,7 +405,7 @@ export default class Entity<T extends EntityData = EntityData>
372
405
  defineProperty: (
373
406
  entity: Entity,
374
407
  name: string,
375
- descriptor: PropertyDescriptor
408
+ descriptor: PropertyDescriptor,
376
409
  ) => {
377
410
  if (
378
411
  typeof name !== 'string' ||
@@ -380,11 +413,11 @@ export default class Entity<T extends EntityData = EntityData>
380
413
  name.substring(0, 1) === '$'
381
414
  ) {
382
415
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
383
- this.$referenceWake();
416
+ this.$check();
384
417
  }
385
418
  Object.defineProperty(entity, name, descriptor);
386
419
  } else {
387
- this.$referenceWake();
420
+ this.$check();
388
421
  Object.defineProperty(entity.$data, name, descriptor);
389
422
  }
390
423
  return true;
@@ -397,48 +430,126 @@ export default class Entity<T extends EntityData = EntityData>
397
430
  name.substring(0, 1) === '$'
398
431
  ) {
399
432
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
400
- this.$referenceWake();
433
+ this.$check();
401
434
  }
402
435
  return Object.getOwnPropertyDescriptor(entity, name);
403
436
  } else {
404
- this.$referenceWake();
437
+ this.$check();
405
438
  return Object.getOwnPropertyDescriptor(entity.$data, name);
406
439
  }
407
440
  },
408
441
 
409
442
  ownKeys: (entity: Entity) => {
410
- this.$referenceWake();
443
+ this.$check();
411
444
  return Object.getOwnPropertyNames(entity).concat(
412
- Object.getOwnPropertyNames(entity.$data)
445
+ Object.getOwnPropertyNames(entity.$data),
413
446
  );
414
447
  },
415
448
  }) as Entity<T>;
416
449
  }
417
450
 
418
- public static async factory(guid?: string) {
451
+ /**
452
+ * Create or retrieve a new entity instance.
453
+ *
454
+ * Note that this will always return an entity, even if the GUID is not found.
455
+ *
456
+ * @param guid An optional GUID to retrieve.
457
+ */
458
+ public static async factory<E extends Entity>(
459
+ this: {
460
+ new (): E;
461
+ },
462
+ guid?: string,
463
+ ): Promise<E & EntityDataType<E>> {
419
464
  const entity = new this();
420
465
  if (guid != null) {
421
- const entity = await this.nymph.getEntity(
466
+ const entity = await (
467
+ this as unknown as EntityConstructor
468
+ ).nymph.getEntity(
422
469
  {
423
- class: this as any as EntityConstructor,
470
+ class: this as unknown as EntityConstructor,
424
471
  },
425
- { type: '&', guid }
472
+ { type: '&', guid },
426
473
  );
427
474
  if (entity != null) {
428
- return entity;
475
+ return entity as E & EntityDataType<E>;
429
476
  }
430
477
  }
431
- return entity;
478
+ return entity as E & EntityDataType<E>;
432
479
  }
433
480
 
434
- public static factorySync(guid?: string) {
435
- return new this(guid);
481
+ /**
482
+ * Create a new entity instance.
483
+ */
484
+ public static factorySync<E extends Entity>(this: {
485
+ new (): E;
486
+ }): E & EntityDataType<E> {
487
+ return new this() as E & EntityDataType<E>;
436
488
  }
437
489
 
438
- public static factoryReference(reference: EntityReference) {
490
+ /**
491
+ * Create a new sleeping reference instance.
492
+ *
493
+ * Sleeping references won't retrieve their data from the database until they
494
+ * are readied with `$wake()` or a parent's `$wakeAll()`.
495
+ *
496
+ * @param reference The Nymph Entity Reference to use to wake.
497
+ * @returns The new instance.
498
+ */
499
+ public static factoryReference<E extends Entity>(
500
+ this: {
501
+ new (): E;
502
+ },
503
+ reference: EntityReference,
504
+ ): E & EntityDataType<E> {
439
505
  const entity = new this();
440
506
  entity.$referenceSleep(reference);
441
- return entity;
507
+ return entity as E & EntityDataType<E>;
508
+ }
509
+
510
+ /**
511
+ * Get an array of strings that **must** be unique across the current etype.
512
+ *
513
+ * When you try to save another entity with any of the same unique strings,
514
+ * Nymph will throw an error.
515
+ *
516
+ * The default implementation of this static method instantiates the entity,
517
+ * assigns all of the given data, then calls `$getUniques` and returns its
518
+ * output. This can have a performance impact if a lot of extra processing
519
+ * happens during any of these steps. You can override this method to
520
+ * calculate the unique strings faster, but you must return the same strings
521
+ * that would be returned by `$getUniques`.
522
+ *
523
+ * @returns Resolves to an array of entity's unique constraint strings.
524
+ */
525
+ public static async getUniques({
526
+ guid,
527
+ cdate,
528
+ mdate,
529
+ tags,
530
+ data,
531
+ sdata,
532
+ }: {
533
+ guid?: string;
534
+ cdate?: number;
535
+ mdate?: number;
536
+ tags: string[];
537
+ data: EntityData;
538
+ sdata?: SerializedEntityData;
539
+ }) {
540
+ const entity = new this();
541
+ if (guid != null) {
542
+ entity.guid = guid;
543
+ }
544
+ if (cdate != null) {
545
+ entity.cdate = cdate;
546
+ }
547
+ if (mdate != null) {
548
+ entity.mdate = mdate;
549
+ }
550
+ entity.tags = tags;
551
+ entity.$putData(data, sdata);
552
+ return await entity.$getUniques();
442
553
  }
443
554
 
444
555
  public toJSON() {
@@ -463,7 +574,7 @@ export default class Entity<T extends EntityData = EntityData>
463
574
  }
464
575
 
465
576
  public $addTag(...tags: string[]) {
466
- this.$referenceWake();
577
+ this.$check();
467
578
 
468
579
  if (tags.length < 1) {
469
580
  return;
@@ -472,7 +583,7 @@ export default class Entity<T extends EntityData = EntityData>
472
583
  }
473
584
 
474
585
  public $arraySearch(array: any[], strict = false) {
475
- this.$referenceWake();
586
+ this.$check();
476
587
 
477
588
  if (!Array.isArray(array)) {
478
589
  return -1;
@@ -487,7 +598,7 @@ export default class Entity<T extends EntityData = EntityData>
487
598
  }
488
599
 
489
600
  public $clearCache() {
490
- this.$referenceWake();
601
+ this.$check();
491
602
 
492
603
  this.$putData(this.$getData(), this.$getSData());
493
604
  }
@@ -497,13 +608,13 @@ export default class Entity<T extends EntityData = EntityData>
497
608
  }
498
609
 
499
610
  public async $delete(): Promise<boolean> {
500
- this.$referenceWake();
611
+ this.$check();
501
612
 
502
613
  return await this.$nymph.deleteEntity(this);
503
614
  }
504
615
 
505
616
  public $equals(object: any) {
506
- this.$referenceWake();
617
+ this.$check();
507
618
 
508
619
  if (!(object instanceof Entity)) {
509
620
  return false;
@@ -513,22 +624,24 @@ export default class Entity<T extends EntityData = EntityData>
513
624
  return false;
514
625
  }
515
626
  }
516
- if (object.constructor !== this.constructor) {
517
- return false;
518
- }
519
627
  if (object.cdate !== this.cdate) {
520
628
  return false;
521
629
  }
522
630
  if (object.mdate !== this.mdate) {
523
631
  return false;
524
632
  }
633
+ const obTags = [...object.tags].sort();
634
+ const myTags = [...this.tags].sort();
635
+ if (!isEqual(obTags, myTags)) {
636
+ return false;
637
+ }
525
638
  const obData = object.$getData(true);
526
639
  const myData = this.$getData(true);
527
640
  return isEqual(obData, myData);
528
641
  }
529
642
 
530
643
  public $getData(includeSData = false) {
531
- this.$referenceWake();
644
+ this.$check();
532
645
 
533
646
  if (includeSData) {
534
647
  // Access all the serialized properties to initialize them.
@@ -540,30 +653,75 @@ export default class Entity<T extends EntityData = EntityData>
540
653
  }
541
654
 
542
655
  public $getSData() {
543
- this.$referenceWake();
656
+ this.$check();
544
657
 
545
658
  return this.$sdata;
546
659
  }
547
660
 
548
- public $getOriginalAcValues() {
549
- this.$referenceWake();
661
+ public async $getUniques(): Promise<string[]> {
662
+ return [];
663
+ }
664
+
665
+ public $getOriginalAcValues(): ACProperties {
666
+ this.$check();
667
+
668
+ return this.$originalAcValues ?? this.$getCurrentAcValues();
669
+ }
670
+
671
+ public $getCurrentAcValues(): ACProperties {
672
+ this.$check();
673
+
674
+ return {
675
+ user: this.$getAcUid(),
676
+ group: this.$getAcGid(),
677
+ acUser: this.$data.acUser ?? null,
678
+ acGroup: this.$data.acGroup ?? null,
679
+ acOther: this.$data.acOther ?? null,
680
+ acRead: this.$getAcReadIds(),
681
+ acWrite: this.$getAcWriteIds(),
682
+ acFull: this.$getAcFullIds(),
683
+ };
684
+ }
550
685
 
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,
686
+ public $getAcUid() {
687
+ if ('user' in this.$sdata) {
688
+ const userValue = JSON.parse(this.$sdata.user);
689
+ if (
690
+ Array.isArray(userValue) &&
691
+ userValue[0] === 'nymph_entity_reference'
692
+ ) {
693
+ return userValue[1];
561
694
  }
562
- );
695
+ }
696
+ return (this.$data.user?.guid ?? null) as string | null;
697
+ }
698
+ public $getAcGid() {
699
+ if ('group' in this.$sdata) {
700
+ const groupValue = JSON.parse(this.$sdata.group);
701
+ if (
702
+ Array.isArray(groupValue) &&
703
+ groupValue[0] === 'nymph_entity_reference'
704
+ ) {
705
+ return groupValue[1];
706
+ }
707
+ }
708
+ return (this.$data.group?.guid ?? null) as string | null;
709
+ }
710
+ public $getAcReadIds() {
711
+ return (this.$data.acRead?.map((entity: EntityInterface) => entity.guid) ??
712
+ null) as (string | null)[] | null;
713
+ }
714
+ public $getAcWriteIds() {
715
+ return (this.$data.acWrite?.map((entity: EntityInterface) => entity.guid) ??
716
+ null) as (string | null)[] | null;
717
+ }
718
+ public $getAcFullIds() {
719
+ return (this.$data.acFull?.map((entity: EntityInterface) => entity.guid) ??
720
+ null) as (string | null)[] | null;
563
721
  }
564
722
 
565
723
  public $getValidatable() {
566
- this.$referenceWake();
724
+ this.$check();
567
725
 
568
726
  // Access all the serialized properties to initialize them.
569
727
  for (const key in this.$sdata) {
@@ -579,13 +737,13 @@ export default class Entity<T extends EntityData = EntityData>
579
737
  }
580
738
 
581
739
  public $getTags() {
582
- this.$referenceWake();
740
+ this.$check();
583
741
 
584
742
  return this.tags;
585
743
  }
586
744
 
587
745
  public $hasTag(...tags: string[]) {
588
- this.$referenceWake();
746
+ this.$check();
589
747
 
590
748
  if (!tags.length) {
591
749
  return false;
@@ -611,10 +769,15 @@ export default class Entity<T extends EntityData = EntityData>
611
769
  return this.guid === object.guid;
612
770
  }
613
771
 
614
- this.$referenceWake();
772
+ this.$check();
615
773
  if (typeof object.$getData !== 'function') {
616
774
  return false;
617
775
  } else {
776
+ const obTags = [...object.tags].sort();
777
+ const myTags = [...this.tags].sort();
778
+ if (!isEqual(obTags, myTags)) {
779
+ return false;
780
+ }
618
781
  const obData = object.$getData(true);
619
782
  const myData = this.$getData(true);
620
783
  return isEqual(obData, myData);
@@ -622,12 +785,11 @@ export default class Entity<T extends EntityData = EntityData>
622
785
  }
623
786
 
624
787
  public $jsonAcceptData(input: EntityJson, allowConflict = false) {
625
- // TODO: Do this without causing everything to become unserialized.
626
- this.$referenceWake();
788
+ this.$check();
627
789
 
628
790
  if (this.guid != input.guid) {
629
791
  throw new EntityConflictError(
630
- 'Tried to accept JSON input for the wrong entity.'
792
+ 'Tried to accept JSON input for the wrong entity.',
631
793
  );
632
794
  }
633
795
 
@@ -665,12 +827,27 @@ export default class Entity<T extends EntityData = EntityData>
665
827
 
666
828
  const protectedData: EntityData = {};
667
829
  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');
830
+ if (this.$nymph.tilmeld) {
831
+ protectedProps.push('acUser');
832
+ protectedProps.push('acGroup');
833
+ protectedProps.push('acOther');
834
+ protectedProps.push('acRead');
835
+ protectedProps.push('acWrite');
836
+ protectedProps.push('acFull');
837
+ if (
838
+ ((this.constructor as EntityConstructor).class !== 'User' &&
839
+ (this.constructor as EntityConstructor).class !== 'Group') ||
840
+ !(this.$nymph.tilmeld as any).currentUser ||
841
+ (!(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
842
+ 'tilmeld/admin',
843
+ ) &&
844
+ !(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
845
+ 'system/admin',
846
+ ))
847
+ ) {
848
+ protectedProps.push('user');
849
+ protectedProps.push('group');
850
+ }
674
851
  }
675
852
  for (const name of protectedProps) {
676
853
  if (name in this.$data) {
@@ -703,11 +880,11 @@ export default class Entity<T extends EntityData = EntityData>
703
880
  }
704
881
 
705
882
  public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
706
- this.$referenceWake();
883
+ this.$check();
707
884
 
708
885
  if (this.guid != patch.guid) {
709
886
  throw new EntityConflictError(
710
- 'Tried to accept JSON patch for the wrong entity.'
887
+ 'Tried to accept JSON patch for the wrong entity.',
711
888
  );
712
889
  }
713
890
 
@@ -719,11 +896,35 @@ export default class Entity<T extends EntityData = EntityData>
719
896
  }
720
897
  this.mdate = patch.mdate;
721
898
 
899
+ const protectedProps = [...this.$protectedData];
900
+ if (this.$nymph.tilmeld) {
901
+ protectedProps.push('acUser');
902
+ protectedProps.push('acGroup');
903
+ protectedProps.push('acOther');
904
+ protectedProps.push('acRead');
905
+ protectedProps.push('acWrite');
906
+ protectedProps.push('acFull');
907
+ if (
908
+ ((this.constructor as EntityConstructor).class !== 'User' &&
909
+ (this.constructor as EntityConstructor).class !== 'Group') ||
910
+ !(this.$nymph.tilmeld as any).currentUser ||
911
+ (!(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
912
+ 'tilmeld/admin',
913
+ ) &&
914
+ !(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
915
+ 'system/admin',
916
+ ))
917
+ ) {
918
+ protectedProps.push('user');
919
+ protectedProps.push('group');
920
+ }
921
+ }
922
+
722
923
  for (const name in patch.set) {
723
924
  if (
724
925
  (this.$allowlistData != null &&
725
926
  this.$allowlistData.indexOf(name) === -1) ||
726
- this.$protectedData.indexOf(name) !== -1 ||
927
+ protectedProps.indexOf(name) !== -1 ||
727
928
  this.$privateData.indexOf(name) !== -1
728
929
  ) {
729
930
  continue;
@@ -731,7 +932,7 @@ export default class Entity<T extends EntityData = EntityData>
731
932
  (this.$data as any)[name] = referencesToEntities(
732
933
  patch.set[name],
733
934
  this.$nymph,
734
- this.$skipAc
935
+ this.$skipAc,
735
936
  );
736
937
  }
737
938
 
@@ -739,7 +940,7 @@ export default class Entity<T extends EntityData = EntityData>
739
940
  if (
740
941
  (this.$allowlistData != null &&
741
942
  this.$allowlistData.indexOf(name) === -1) ||
742
- this.$protectedData.indexOf(name) !== -1 ||
943
+ protectedProps.indexOf(name) !== -1 ||
743
944
  this.$privateData.indexOf(name) !== -1
744
945
  ) {
745
946
  continue;
@@ -771,7 +972,7 @@ export default class Entity<T extends EntityData = EntityData>
771
972
  }
772
973
 
773
974
  public $putData(data: EntityData, sdata?: SerializedEntityData) {
774
- this.$referenceWake();
975
+ this.$check();
775
976
 
776
977
  const mySdata = sdata ?? this.$getSData();
777
978
  for (const name in data) {
@@ -784,13 +985,13 @@ export default class Entity<T extends EntityData = EntityData>
784
985
  (this.$dataStore as any)[name] = referencesToEntities(
785
986
  data[name],
786
987
  this.$nymph,
787
- this.$skipAc
988
+ this.$skipAc,
788
989
  );
789
990
  }
790
991
  this.$sdata = mySdata;
791
992
  // Set original AC values if not set..
792
993
  if (this.$originalAcValues == null) {
793
- this.$originalAcValues = this.$getOriginalAcValues();
994
+ this.$originalAcValues = this.$getCurrentAcValues();
794
995
  }
795
996
  }
796
997
 
@@ -807,14 +1008,14 @@ export default class Entity<T extends EntityData = EntityData>
807
1008
  ) {
808
1009
  throw new InvalidParametersError(
809
1010
  'referenceSleep expects parameter 1 to be a valid Nymph entity ' +
810
- 'reference.'
1011
+ 'reference.',
811
1012
  );
812
1013
  }
813
1014
  const thisClass: string = (this.constructor as any).class;
814
1015
  if (reference[2] !== thisClass) {
815
1016
  throw new InvalidParametersError(
816
1017
  'referenceSleep can only be called with an entity reference of the ' +
817
- `same class. Given class: ${reference[2]}; this class: ${thisClass}.`
1018
+ `same class. Given class: ${reference[2]}; this class: ${thisClass}.`,
818
1019
  );
819
1020
  }
820
1021
  this.$isASleepingReference = true;
@@ -822,49 +1023,137 @@ export default class Entity<T extends EntityData = EntityData>
822
1023
  this.$sleepingReference = reference;
823
1024
  }
824
1025
 
1026
+ /**
1027
+ * Check if this is a sleeping reference and throw an error if so.
1028
+ */
1029
+ protected $check() {
1030
+ if (this.$isASleepingReference || this.$sleepingReference != null) {
1031
+ throw new EntityIsSleepingReferenceError(
1032
+ 'This entity is in a sleeping reference state. You must use .$wake() to wake it.',
1033
+ );
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * Check if this is a sleeping reference.
1039
+ */
1040
+ public $asleep() {
1041
+ return this.$isASleepingReference || this.$sleepingReference != null;
1042
+ }
1043
+
825
1044
  /**
826
1045
  * Wake from a sleeping reference.
827
- *
828
- * @returns True on success, false on failure.
829
1046
  */
830
- protected $referenceWake() {
1047
+ public $wake() {
831
1048
  if (!this.$isASleepingReference || this.$sleepingReference == null) {
832
- return true;
1049
+ this.$wakePromise = null;
1050
+ return Promise.resolve(this);
833
1051
  }
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;
1052
+ if (this.$sleepingReference?.[1] == null) {
1053
+ throw new InvalidStateError(
1054
+ 'Tried to wake a sleeping reference with no GUID.',
1055
+ );
844
1056
  }
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;
1057
+ if (!this.$wakePromise) {
1058
+ const EntityClass = this.$nymph.getEntityClass(
1059
+ this.$sleepingReference[2],
1060
+ );
1061
+ this.$wakePromise = this.$nymph
1062
+ .getEntity(
1063
+ {
1064
+ class: EntityClass,
1065
+ skipAc: this.$skipAc,
1066
+ },
1067
+ { type: '&', guid: this.$sleepingReference[1] },
1068
+ )
1069
+ .then((entity) => {
1070
+ if (entity == null || entity.guid == null) {
1071
+ return Promise.reject(
1072
+ new InvalidStateError(
1073
+ 'The sleeping reference could not be retrieved.',
1074
+ ),
1075
+ );
1076
+ }
1077
+ this.$isASleepingReference = false;
1078
+ this.$sleepingReference = null;
1079
+ this.guid = entity.guid;
1080
+ this.tags = entity.tags;
1081
+ this.cdate = entity.cdate;
1082
+ this.mdate = entity.mdate;
1083
+ this.$putData(entity.$getData(), entity.$getSData());
1084
+
1085
+ return this;
1086
+ })
1087
+ .finally(() => {
1088
+ this.$wakePromise = null;
1089
+ });
1090
+ }
1091
+ return this.$wakePromise;
1092
+ }
1093
+
1094
+ public $wakeAll(level?: number) {
1095
+ return new Promise((resolve, reject) => {
1096
+ // Run this once this entity is wake.
1097
+ const wakeProps = () => {
1098
+ let newLevel;
1099
+ // If level is undefined, keep going forever, otherwise, stop once we've
1100
+ // gone deep enough.
1101
+ if (level !== undefined) {
1102
+ newLevel = level - 1;
1103
+ }
1104
+ if (newLevel !== undefined && newLevel < 0) {
1105
+ resolve(this);
1106
+ return;
1107
+ }
1108
+ const promises = [];
1109
+ // Go through data looking for entities to wake.
1110
+ for (let [key, value] of Object.entries(this.$data)) {
1111
+ if (value instanceof Entity && value.$isASleepingReference) {
1112
+ promises.push(value.$wakeAll(newLevel));
1113
+ } else if (Array.isArray(value)) {
1114
+ for (let i = 0; i < value.length; i++) {
1115
+ if (
1116
+ value[i] instanceof Entity &&
1117
+ value[i].$isASleepingReference
1118
+ ) {
1119
+ promises.push(value[i].$wakeAll(newLevel));
1120
+ }
1121
+ }
1122
+ }
1123
+ }
1124
+ if (promises.length) {
1125
+ Promise.all(promises).then(
1126
+ () => resolve(this),
1127
+ (errObj) => reject(errObj),
1128
+ );
1129
+ } else {
1130
+ resolve(this);
1131
+ }
1132
+ };
1133
+
1134
+ if (this.$isASleepingReference) {
1135
+ this.$wake().then(wakeProps, (errObj) => reject(errObj));
1136
+ } else {
1137
+ wakeProps();
1138
+ }
1139
+ }) as Promise<Entity<T>>;
853
1140
  }
854
1141
 
855
1142
  public async $refresh() {
856
- this.$referenceWake();
1143
+ await this.$wake();
857
1144
 
858
1145
  if (this.guid == null) {
859
1146
  return false;
860
1147
  }
861
1148
  const refresh = await this.$nymph.getEntity(
862
1149
  {
863
- class: this.constructor as EntityConstructor,
1150
+ class: this.$nymph.getEntityClass(
1151
+ this.constructor as EntityConstructor,
1152
+ ),
864
1153
  skipCache: true,
865
1154
  skipAc: this.$skipAc,
866
1155
  },
867
- { type: '&', guid: this.guid }
1156
+ { type: '&', guid: this.guid },
868
1157
  );
869
1158
  if (refresh == null) {
870
1159
  return 0;
@@ -877,13 +1166,13 @@ export default class Entity<T extends EntityData = EntityData>
877
1166
  }
878
1167
 
879
1168
  public $removeTag(...tags: string[]) {
880
- this.$referenceWake();
1169
+ this.$check();
881
1170
 
882
1171
  this.tags = difference(this.tags, tags);
883
1172
  }
884
1173
 
885
1174
  public async $save(): Promise<boolean> {
886
- this.$referenceWake();
1175
+ await this.$wake();
887
1176
 
888
1177
  return await this.$nymph.saveEntity(this);
889
1178
  }