@nymphjs/nymph 1.0.0-beta.9 → 1.0.0-beta.91

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 (96) hide show
  1. package/CHANGELOG.md +436 -0
  2. package/README.md +10 -20
  3. package/dist/Entity.d.ts +277 -10
  4. package/dist/Entity.js +543 -148
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Entity.test.js +53 -13
  7. package/dist/Entity.test.js.map +1 -1
  8. package/dist/Entity.types.d.ts +249 -23
  9. package/dist/Entity.types.js +1 -2
  10. package/dist/Nymph.d.ts +431 -12
  11. package/dist/Nymph.js +692 -180
  12. package/dist/Nymph.js.map +1 -1
  13. package/dist/Nymph.types.d.ts +65 -7
  14. package/dist/Nymph.types.js +3 -5
  15. package/dist/Nymph.types.js.map +1 -1
  16. package/dist/conf/d.d.ts +43 -0
  17. package/dist/conf/d.js +1 -2
  18. package/dist/conf/defaults.d.ts +1 -1
  19. package/dist/conf/defaults.js +26 -3
  20. package/dist/conf/defaults.js.map +1 -1
  21. package/dist/conf/index.d.ts +2 -2
  22. package/dist/conf/index.js +2 -8
  23. package/dist/conf/index.js.map +1 -1
  24. package/dist/driver/NymphDriver.d.ts +137 -39
  25. package/dist/driver/NymphDriver.js +344 -194
  26. package/dist/driver/NymphDriver.js.map +1 -1
  27. package/dist/driver/index.d.ts +2 -2
  28. package/dist/driver/index.js +3 -23
  29. package/dist/driver/index.js.map +1 -1
  30. package/dist/driver/testArtifacts.d.ts +2 -2
  31. package/dist/driver/testArtifacts.js +557 -92
  32. package/dist/driver/testArtifacts.js.map +1 -1
  33. package/dist/errors/ClassNotAvailableError.js +1 -5
  34. package/dist/errors/ClassNotAvailableError.js.map +1 -1
  35. package/dist/errors/EntityConflictError.js +1 -5
  36. package/dist/errors/EntityConflictError.js.map +1 -1
  37. package/dist/errors/EntityInvalidDataError.d.ts +6 -0
  38. package/dist/errors/EntityInvalidDataError.js +8 -6
  39. package/dist/errors/EntityInvalidDataError.js.map +1 -1
  40. package/dist/errors/EntityIsSleepingReferenceError.js +1 -5
  41. package/dist/errors/EntityIsSleepingReferenceError.js.map +1 -1
  42. package/dist/errors/EntityUniqueConstraintError.d.ts +3 -0
  43. package/dist/errors/EntityUniqueConstraintError.js +7 -0
  44. package/dist/errors/EntityUniqueConstraintError.js.map +1 -0
  45. package/dist/errors/InvalidParametersError.js +1 -5
  46. package/dist/errors/InvalidParametersError.js.map +1 -1
  47. package/dist/errors/InvalidStateError.d.ts +3 -0
  48. package/dist/errors/InvalidStateError.js +7 -0
  49. package/dist/errors/InvalidStateError.js.map +1 -0
  50. package/dist/errors/NotConfiguredError.js +1 -5
  51. package/dist/errors/NotConfiguredError.js.map +1 -1
  52. package/dist/errors/QueryFailedError.js +2 -5
  53. package/dist/errors/QueryFailedError.js.map +1 -1
  54. package/dist/errors/UnableToConnectError.js +1 -5
  55. package/dist/errors/UnableToConnectError.js.map +1 -1
  56. package/dist/errors/index.d.ts +10 -8
  57. package/dist/errors/index.js +10 -24
  58. package/dist/errors/index.js.map +1 -1
  59. package/dist/index.d.ts +11 -11
  60. package/dist/index.js +14 -34
  61. package/dist/index.js.map +1 -1
  62. package/dist/nymphJoiProps.js +10 -15
  63. package/dist/nymphJoiProps.js.map +1 -1
  64. package/dist/testArtifacts.d.ts +18 -6
  65. package/dist/testArtifacts.js +32 -37
  66. package/dist/testArtifacts.js.map +1 -1
  67. package/dist/testMocks.d.ts +15 -12
  68. package/dist/testMocks.js +40 -29
  69. package/dist/testMocks.js.map +1 -1
  70. package/dist/utils.d.ts +4 -4
  71. package/dist/utils.js +40 -29
  72. package/dist/utils.js.map +1 -1
  73. package/jest.config.js +11 -2
  74. package/package.json +22 -19
  75. package/src/Entity.test.ts +52 -7
  76. package/src/Entity.ts +458 -147
  77. package/src/Entity.types.ts +64 -74
  78. package/src/Nymph.ts +754 -321
  79. package/src/Nymph.types.ts +131 -26
  80. package/src/conf/d.ts +25 -0
  81. package/src/conf/defaults.ts +34 -1
  82. package/src/conf/index.ts +2 -2
  83. package/src/driver/NymphDriver.ts +362 -359
  84. package/src/driver/index.ts +2 -2
  85. package/src/driver/testArtifacts.ts +609 -185
  86. package/src/errors/EntityUniqueConstraintError.ts +6 -0
  87. package/src/errors/InvalidStateError.ts +6 -0
  88. package/src/errors/index.ts +12 -8
  89. package/src/index.ts +11 -11
  90. package/src/nymphJoiProps.ts +2 -2
  91. package/src/testArtifacts.ts +19 -20
  92. package/src/testMocks.ts +60 -35
  93. package/src/type/locutus.d.ts +3 -0
  94. package/src/utils.ts +39 -18
  95. package/tsconfig.json +5 -3
  96. package/typedoc.json +4 -0
package/src/Entity.ts CHANGED
@@ -1,7 +1,9 @@
1
- import { difference, intersection, isEqual } from 'lodash';
1
+ import { difference, intersection, isEqual } from 'lodash-es';
2
+ import { guid } from '@nymphjs/guid';
2
3
 
3
- import type Nymph from './Nymph';
4
- import {
4
+ import type Nymph from './Nymph.js';
5
+ import type { Options } from './Nymph.types.js';
6
+ import type {
5
7
  ACProperties,
6
8
  EntityConstructor,
7
9
  EntityData,
@@ -10,13 +12,24 @@ import {
10
12
  EntityPatch,
11
13
  EntityReference,
12
14
  SerializedEntityData,
13
- } from './Entity.types';
14
- import { EntityConflictError, InvalidParametersError } from './errors';
15
+ } from './Entity.types.js';
16
+ import {
17
+ EntityConflictError,
18
+ EntityIsSleepingReferenceError,
19
+ InvalidParametersError,
20
+ InvalidStateError,
21
+ } from './errors/index.js';
15
22
  import {
16
23
  entitiesToReferences,
17
24
  referencesToEntities,
18
25
  uniqueStrings,
19
- } from './utils';
26
+ } from './utils.js';
27
+
28
+ export type EntityDataType<T> =
29
+ T extends Entity<infer DataType> ? DataType : never;
30
+
31
+ export type EntityInstanceType<T extends EntityConstructor> =
32
+ T extends new () => infer E ? E & EntityDataType<E> : never;
20
33
 
21
34
  /**
22
35
  * Database abstraction object.
@@ -136,6 +149,10 @@ export default class Entity<T extends EntityData = EntityData>
136
149
  * The reference to use to wake.
137
150
  */
138
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;
139
156
  /**
140
157
  * Properties that will not be serialized into JSON with toJSON(). This
141
158
  * can be considered a denylist, because these properties will not be set
@@ -145,6 +162,18 @@ export default class Entity<T extends EntityData = EntityData>
145
162
  * also listed in searchRestrictedData.
146
163
  */
147
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;
148
177
  /**
149
178
  * Properties that will not be searchable from the frontend. If the frontend
150
179
  * includes any of these properties in any of their clauses, they will be
@@ -156,8 +185,20 @@ export default class Entity<T extends EntityData = EntityData>
156
185
  * be visible on the frontend, unlike $privateData, but any changes to them
157
186
  * that come from the frontend will be ignored.
158
187
  *
159
- * In addition to what's listed here, the 'user' and 'group' properties will
160
- * 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.
161
202
  */
162
203
  protected $protectedData: string[] = [];
163
204
  /**
@@ -196,28 +237,39 @@ export default class Entity<T extends EntityData = EntityData>
196
237
  * The AC properties' values when the entity was loaded.
197
238
  */
198
239
  private $originalAcValues: ACProperties | null = null;
240
+ /**
241
+ * This is used to hold a generated GUID for a new entity.
242
+ */
243
+ private $guaranteedGUID: string | null = null;
199
244
 
200
245
  /**
201
- * Load an entity.
202
- * @param guid The ID of the entity to load, undefined for a new entity.
246
+ * Alter the options for a query for this entity.
247
+ *
248
+ * @param options The current options.
249
+ * @returns The altered options.
203
250
  */
204
- public constructor(guid?: string) {
251
+ static alterOptions?<T extends Options>(options: T): T;
252
+
253
+ /**
254
+ * Initialize an entity.
255
+ */
256
+ public constructor(..._rest: any[]) {
205
257
  this.$nymph = (this.constructor as EntityConstructor).nymph;
206
258
  this.$dataHandler = {
207
259
  has: (data: EntityData, name: string) => {
208
- this.$referenceWake();
260
+ this.$check();
209
261
 
210
262
  return name in data || name in this.$sdata;
211
263
  },
212
264
 
213
265
  get: (data: EntityData, name: string) => {
214
- this.$referenceWake();
266
+ this.$check();
215
267
 
216
268
  if (this.$sdata.hasOwnProperty(name)) {
217
269
  data[name] = referencesToEntities(
218
270
  JSON.parse(this.$sdata[name]),
219
271
  this.$nymph,
220
- this.$skipAc
272
+ this.$skipAc,
221
273
  );
222
274
  delete this.$sdata[name];
223
275
  }
@@ -228,7 +280,7 @@ export default class Entity<T extends EntityData = EntityData>
228
280
  },
229
281
 
230
282
  set: (data: EntityData, name: string, value: any) => {
231
- this.$referenceWake();
283
+ this.$check();
232
284
 
233
285
  if (this.$sdata.hasOwnProperty(name)) {
234
286
  delete this.$sdata[name];
@@ -238,7 +290,7 @@ export default class Entity<T extends EntityData = EntityData>
238
290
  },
239
291
 
240
292
  deleteProperty: (data: EntityData, name: string) => {
241
- this.$referenceWake();
293
+ this.$check();
242
294
 
243
295
  if (this.$sdata.hasOwnProperty(name)) {
244
296
  return delete this.$sdata[name];
@@ -252,9 +304,9 @@ export default class Entity<T extends EntityData = EntityData>
252
304
  defineProperty: (
253
305
  data: EntityData,
254
306
  name: string,
255
- descriptor: PropertyDescriptor
307
+ descriptor: PropertyDescriptor,
256
308
  ) => {
257
- this.$referenceWake();
309
+ this.$check();
258
310
 
259
311
  if (this.$sdata.hasOwnProperty(name)) {
260
312
  delete this.$sdata[name];
@@ -264,13 +316,13 @@ export default class Entity<T extends EntityData = EntityData>
264
316
  },
265
317
 
266
318
  getOwnPropertyDescriptor: (data: EntityData, name: string) => {
267
- this.$referenceWake();
319
+ this.$check();
268
320
 
269
321
  if (this.$sdata.hasOwnProperty(name)) {
270
322
  data[name] = referencesToEntities(
271
323
  JSON.parse(this.$sdata[name]),
272
324
  this.$nymph,
273
- this.$skipAc
325
+ this.$skipAc,
274
326
  );
275
327
  delete this.$sdata[name];
276
328
  }
@@ -278,10 +330,10 @@ export default class Entity<T extends EntityData = EntityData>
278
330
  },
279
331
 
280
332
  ownKeys: (data: EntityData) => {
281
- this.$referenceWake();
333
+ this.$check();
282
334
 
283
335
  return Object.getOwnPropertyNames(data).concat(
284
- Object.getOwnPropertyNames(this.$sdata)
336
+ Object.getOwnPropertyNames(this.$sdata),
285
337
  );
286
338
  },
287
339
  };
@@ -290,20 +342,6 @@ export default class Entity<T extends EntityData = EntityData>
290
342
 
291
343
  this.$data = new Proxy(this.$dataStore, this.$dataHandler);
292
344
 
293
- if (guid != null) {
294
- const entity = this.$nymph.driver.getEntitySync(
295
- { class: this.constructor as EntityConstructor },
296
- { type: '&', guid }
297
- );
298
- if (entity) {
299
- this.guid = entity.guid;
300
- this.tags = entity.tags;
301
- this.cdate = entity.cdate;
302
- this.mdate = entity.mdate;
303
- this.$putData(entity.$getData(), entity.$getSData());
304
- }
305
- }
306
-
307
345
  return new Proxy(this, {
308
346
  has: (entity: Entity, name: string) => {
309
347
  if (
@@ -314,7 +352,7 @@ export default class Entity<T extends EntityData = EntityData>
314
352
  return name in entity;
315
353
  }
316
354
 
317
- this.$referenceWake();
355
+ this.$check();
318
356
  return name in entity.$data;
319
357
  },
320
358
 
@@ -325,12 +363,12 @@ export default class Entity<T extends EntityData = EntityData>
325
363
  name.substring(0, 1) === '$'
326
364
  ) {
327
365
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
328
- this.$referenceWake();
366
+ this.$check();
329
367
  }
330
368
  return (entity as any)[name];
331
369
  }
332
370
 
333
- this.$referenceWake();
371
+ this.$check();
334
372
  if (name in entity.$data) {
335
373
  return entity.$data[name];
336
374
  }
@@ -344,11 +382,11 @@ export default class Entity<T extends EntityData = EntityData>
344
382
  name.substring(0, 1) === '$'
345
383
  ) {
346
384
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
347
- this.$referenceWake();
385
+ this.$check();
348
386
  }
349
387
  (entity as any)[name] = value;
350
388
  } else {
351
- this.$referenceWake();
389
+ this.$check();
352
390
  (entity.$data as any)[name] = value;
353
391
  }
354
392
  return true;
@@ -358,7 +396,7 @@ export default class Entity<T extends EntityData = EntityData>
358
396
  if (name in entity) {
359
397
  return delete (entity as any)[name];
360
398
  } else if (name in entity.$data) {
361
- this.$referenceWake();
399
+ this.$check();
362
400
  return delete entity.$data[name];
363
401
  }
364
402
  return true;
@@ -371,7 +409,7 @@ export default class Entity<T extends EntityData = EntityData>
371
409
  defineProperty: (
372
410
  entity: Entity,
373
411
  name: string,
374
- descriptor: PropertyDescriptor
412
+ descriptor: PropertyDescriptor,
375
413
  ) => {
376
414
  if (
377
415
  typeof name !== 'string' ||
@@ -379,11 +417,11 @@ export default class Entity<T extends EntityData = EntityData>
379
417
  name.substring(0, 1) === '$'
380
418
  ) {
381
419
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
382
- this.$referenceWake();
420
+ this.$check();
383
421
  }
384
422
  Object.defineProperty(entity, name, descriptor);
385
423
  } else {
386
- this.$referenceWake();
424
+ this.$check();
387
425
  Object.defineProperty(entity.$data, name, descriptor);
388
426
  }
389
427
  return true;
@@ -396,48 +434,126 @@ export default class Entity<T extends EntityData = EntityData>
396
434
  name.substring(0, 1) === '$'
397
435
  ) {
398
436
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
399
- this.$referenceWake();
437
+ this.$check();
400
438
  }
401
439
  return Object.getOwnPropertyDescriptor(entity, name);
402
440
  } else {
403
- this.$referenceWake();
441
+ this.$check();
404
442
  return Object.getOwnPropertyDescriptor(entity.$data, name);
405
443
  }
406
444
  },
407
445
 
408
446
  ownKeys: (entity: Entity) => {
409
- this.$referenceWake();
447
+ this.$check();
410
448
  return Object.getOwnPropertyNames(entity).concat(
411
- Object.getOwnPropertyNames(entity.$data)
449
+ Object.getOwnPropertyNames(entity.$data),
412
450
  );
413
451
  },
414
452
  }) as Entity<T>;
415
453
  }
416
454
 
417
- public static async factory(guid?: string) {
455
+ /**
456
+ * Create or retrieve a new entity instance.
457
+ *
458
+ * Note that this will always return an entity, even if the GUID is not found.
459
+ *
460
+ * @param guid An optional GUID to retrieve.
461
+ */
462
+ public static async factory<E extends Entity>(
463
+ this: {
464
+ new (): E;
465
+ },
466
+ guid?: string,
467
+ ): Promise<E & EntityDataType<E>> {
418
468
  const entity = new this();
419
469
  if (guid != null) {
420
- const entity = await this.nymph.getEntity(
470
+ const entity = await (
471
+ this as unknown as EntityConstructor
472
+ ).nymph.getEntity(
421
473
  {
422
- class: this as any as EntityConstructor,
474
+ class: this as unknown as EntityConstructor,
423
475
  },
424
- { type: '&', guid }
476
+ { type: '&', guid },
425
477
  );
426
478
  if (entity != null) {
427
- return entity;
479
+ return entity as E & EntityDataType<E>;
428
480
  }
429
481
  }
430
- return entity;
482
+ return entity as E & EntityDataType<E>;
431
483
  }
432
484
 
433
- public static factorySync(guid?: string) {
434
- return new this(guid);
485
+ /**
486
+ * Create a new entity instance.
487
+ */
488
+ public static factorySync<E extends Entity>(this: {
489
+ new (): E;
490
+ }): E & EntityDataType<E> {
491
+ return new this() as E & EntityDataType<E>;
435
492
  }
436
493
 
437
- public static factoryReference(reference: EntityReference) {
494
+ /**
495
+ * Create a new sleeping reference instance.
496
+ *
497
+ * Sleeping references won't retrieve their data from the database until they
498
+ * are readied with `$wake()` or a parent's `$wakeAll()`.
499
+ *
500
+ * @param reference The Nymph Entity Reference to use to wake.
501
+ * @returns The new instance.
502
+ */
503
+ public static factoryReference<E extends Entity>(
504
+ this: {
505
+ new (): E;
506
+ },
507
+ reference: EntityReference,
508
+ ): E & EntityDataType<E> {
438
509
  const entity = new this();
439
510
  entity.$referenceSleep(reference);
440
- return entity;
511
+ return entity as E & EntityDataType<E>;
512
+ }
513
+
514
+ /**
515
+ * Get an array of strings that **must** be unique across the current etype.
516
+ *
517
+ * When you try to save another entity with any of the same unique strings,
518
+ * Nymph will throw an error.
519
+ *
520
+ * The default implementation of this static method instantiates the entity,
521
+ * assigns all of the given data, then calls `$getUniques` and returns its
522
+ * output. This can have a performance impact if a lot of extra processing
523
+ * happens during any of these steps. You can override this method to
524
+ * calculate the unique strings faster, but you must return the same strings
525
+ * that would be returned by `$getUniques`.
526
+ *
527
+ * @returns Resolves to an array of entity's unique constraint strings.
528
+ */
529
+ public static async getUniques({
530
+ guid,
531
+ cdate,
532
+ mdate,
533
+ tags,
534
+ data,
535
+ sdata,
536
+ }: {
537
+ guid?: string;
538
+ cdate?: number;
539
+ mdate?: number;
540
+ tags: string[];
541
+ data: EntityData;
542
+ sdata?: SerializedEntityData;
543
+ }) {
544
+ const entity = new this();
545
+ if (guid != null) {
546
+ entity.guid = guid;
547
+ }
548
+ if (cdate != null) {
549
+ entity.cdate = cdate;
550
+ }
551
+ if (mdate != null) {
552
+ entity.mdate = mdate;
553
+ }
554
+ entity.tags = tags;
555
+ entity.$putData(data, sdata);
556
+ return await entity.$getUniques();
441
557
  }
442
558
 
443
559
  public toJSON() {
@@ -452,17 +568,29 @@ export default class Entity<T extends EntityData = EntityData>
452
568
  tags: [...this.tags],
453
569
  data: {},
454
570
  };
455
- const data = this.$getData(true);
456
- for (const key in data) {
571
+ for (const key of [
572
+ ...Object.keys(this.$dataStore),
573
+ ...Object.keys(this.$sdata),
574
+ ]) {
457
575
  if (this.$privateData.indexOf(key) === -1) {
458
- obj.data[key] = entitiesToReferences(data[key]);
576
+ obj.data[key] = entitiesToReferences(this.$data[key], true);
459
577
  }
460
578
  }
461
579
  return obj;
462
580
  }
463
581
 
582
+ public $getGuaranteedGUID() {
583
+ if (this.guid !== null) {
584
+ return this.guid;
585
+ }
586
+ if (this.$guaranteedGUID == null) {
587
+ this.$guaranteedGUID = guid();
588
+ }
589
+ return this.$guaranteedGUID;
590
+ }
591
+
464
592
  public $addTag(...tags: string[]) {
465
- this.$referenceWake();
593
+ this.$check();
466
594
 
467
595
  if (tags.length < 1) {
468
596
  return;
@@ -471,7 +599,7 @@ export default class Entity<T extends EntityData = EntityData>
471
599
  }
472
600
 
473
601
  public $arraySearch(array: any[], strict = false) {
474
- this.$referenceWake();
602
+ this.$check();
475
603
 
476
604
  if (!Array.isArray(array)) {
477
605
  return -1;
@@ -486,9 +614,9 @@ export default class Entity<T extends EntityData = EntityData>
486
614
  }
487
615
 
488
616
  public $clearCache() {
489
- this.$referenceWake();
617
+ this.$check();
490
618
 
491
- this.$putData(this.$getData(), this.$getSData());
619
+ this.$putData(this.$getData(false, true), this.$getSData());
492
620
  }
493
621
 
494
622
  public $getClientEnabledMethods() {
@@ -496,13 +624,13 @@ export default class Entity<T extends EntityData = EntityData>
496
624
  }
497
625
 
498
626
  public async $delete(): Promise<boolean> {
499
- this.$referenceWake();
627
+ this.$check();
500
628
 
501
629
  return await this.$nymph.deleteEntity(this);
502
630
  }
503
631
 
504
632
  public $equals(object: any) {
505
- this.$referenceWake();
633
+ this.$check();
506
634
 
507
635
  if (!(object instanceof Entity)) {
508
636
  return false;
@@ -523,13 +651,13 @@ export default class Entity<T extends EntityData = EntityData>
523
651
  if (!isEqual(obTags, myTags)) {
524
652
  return false;
525
653
  }
526
- const obData = object.$getData(true);
527
- const myData = this.$getData(true);
654
+ const obData = object.$getData(true, true);
655
+ const myData = this.$getData(true, true);
528
656
  return isEqual(obData, myData);
529
657
  }
530
658
 
531
- public $getData(includeSData = false) {
532
- this.$referenceWake();
659
+ public $getData(includeSData = false, referenceOnlyExisting?: boolean) {
660
+ this.$check();
533
661
 
534
662
  if (includeSData) {
535
663
  // Access all the serialized properties to initialize them.
@@ -537,34 +665,79 @@ export default class Entity<T extends EntityData = EntityData>
537
665
  const _unused: any = (this as any)[key];
538
666
  }
539
667
  }
540
- return entitiesToReferences({ ...this.$dataStore });
668
+ return entitiesToReferences({ ...this.$dataStore }, referenceOnlyExisting);
541
669
  }
542
670
 
543
671
  public $getSData() {
544
- this.$referenceWake();
672
+ this.$check();
545
673
 
546
674
  return this.$sdata;
547
675
  }
548
676
 
549
- public $getOriginalAcValues() {
550
- this.$referenceWake();
677
+ public async $getUniques(): Promise<string[]> {
678
+ return [];
679
+ }
680
+
681
+ public $getOriginalAcValues(): ACProperties {
682
+ this.$check();
683
+
684
+ return this.$originalAcValues ?? this.$getCurrentAcValues();
685
+ }
551
686
 
552
- return (
553
- this.$originalAcValues ?? {
554
- user: this.$data.user,
555
- group: this.$data.group,
556
- acUser: this.$data.acUser,
557
- acGroup: this.$data.acGroup,
558
- acOther: this.$data.acOther,
559
- acRead: this.$data.acRead,
560
- acWrite: this.$data.acWrite,
561
- acFull: this.$data.acFull,
687
+ public $getCurrentAcValues(): ACProperties {
688
+ this.$check();
689
+
690
+ return {
691
+ user: this.$getAcUid(),
692
+ group: this.$getAcGid(),
693
+ acUser: this.$data.acUser ?? null,
694
+ acGroup: this.$data.acGroup ?? null,
695
+ acOther: this.$data.acOther ?? null,
696
+ acRead: this.$getAcReadIds(),
697
+ acWrite: this.$getAcWriteIds(),
698
+ acFull: this.$getAcFullIds(),
699
+ };
700
+ }
701
+
702
+ public $getAcUid() {
703
+ if ('user' in this.$sdata) {
704
+ const userValue = JSON.parse(this.$sdata.user);
705
+ if (
706
+ Array.isArray(userValue) &&
707
+ userValue[0] === 'nymph_entity_reference'
708
+ ) {
709
+ return userValue[1];
562
710
  }
563
- );
711
+ }
712
+ return (this.$data.user?.guid ?? null) as string | null;
713
+ }
714
+ public $getAcGid() {
715
+ if ('group' in this.$sdata) {
716
+ const groupValue = JSON.parse(this.$sdata.group);
717
+ if (
718
+ Array.isArray(groupValue) &&
719
+ groupValue[0] === 'nymph_entity_reference'
720
+ ) {
721
+ return groupValue[1];
722
+ }
723
+ }
724
+ return (this.$data.group?.guid ?? null) as string | null;
725
+ }
726
+ public $getAcReadIds() {
727
+ return (this.$data.acRead?.map((entity: EntityInterface) => entity.guid) ??
728
+ null) as (string | null)[] | null;
729
+ }
730
+ public $getAcWriteIds() {
731
+ return (this.$data.acWrite?.map((entity: EntityInterface) => entity.guid) ??
732
+ null) as (string | null)[] | null;
733
+ }
734
+ public $getAcFullIds() {
735
+ return (this.$data.acFull?.map((entity: EntityInterface) => entity.guid) ??
736
+ null) as (string | null)[] | null;
564
737
  }
565
738
 
566
739
  public $getValidatable() {
567
- this.$referenceWake();
740
+ this.$check();
568
741
 
569
742
  // Access all the serialized properties to initialize them.
570
743
  for (const key in this.$sdata) {
@@ -580,13 +753,13 @@ export default class Entity<T extends EntityData = EntityData>
580
753
  }
581
754
 
582
755
  public $getTags() {
583
- this.$referenceWake();
756
+ this.$check();
584
757
 
585
758
  return this.tags;
586
759
  }
587
760
 
588
761
  public $hasTag(...tags: string[]) {
589
- this.$referenceWake();
762
+ this.$check();
590
763
 
591
764
  if (!tags.length) {
592
765
  return false;
@@ -608,11 +781,15 @@ export default class Entity<T extends EntityData = EntityData>
608
781
  return false;
609
782
  }
610
783
 
611
- if (this.guid || object.guid) {
784
+ if (this === object) {
785
+ return true;
786
+ }
787
+
788
+ if (this.guid != null || object.guid != null) {
612
789
  return this.guid === object.guid;
613
790
  }
614
791
 
615
- this.$referenceWake();
792
+ this.$check();
616
793
  if (typeof object.$getData !== 'function') {
617
794
  return false;
618
795
  } else {
@@ -621,19 +798,18 @@ export default class Entity<T extends EntityData = EntityData>
621
798
  if (!isEqual(obTags, myTags)) {
622
799
  return false;
623
800
  }
624
- const obData = object.$getData(true);
625
- const myData = this.$getData(true);
801
+ const obData = object.$getData(true, true);
802
+ const myData = this.$getData(true, true);
626
803
  return isEqual(obData, myData);
627
804
  }
628
805
  }
629
806
 
630
807
  public $jsonAcceptData(input: EntityJson, allowConflict = false) {
631
- // TODO: Do this without causing everything to become unserialized.
632
- this.$referenceWake();
808
+ this.$check();
633
809
 
634
810
  if (this.guid != input.guid) {
635
811
  throw new EntityConflictError(
636
- 'Tried to accept JSON input for the wrong entity.'
812
+ 'Tried to accept JSON input for the wrong entity.',
637
813
  );
638
814
  }
639
815
 
@@ -658,7 +834,7 @@ export default class Entity<T extends EntityData = EntityData>
658
834
  this.$addTag(...protectedTags, ...tags);
659
835
 
660
836
  // Accept the data.
661
- const data = input.data;
837
+ const data = { ...input.data };
662
838
  const privateData: EntityData = {};
663
839
  for (const name of this.$privateData) {
664
840
  if (name in this.$data) {
@@ -671,12 +847,27 @@ export default class Entity<T extends EntityData = EntityData>
671
847
 
672
848
  const protectedData: EntityData = {};
673
849
  const protectedProps = [...this.$protectedData];
674
- if (
675
- this.$nymph.tilmeld &&
676
- !this.$nymph.tilmeld.gatekeeper('tilmeld/admin')
677
- ) {
678
- protectedProps.push('user');
679
- protectedProps.push('group');
850
+ if (this.$nymph.tilmeld) {
851
+ protectedProps.push('acUser');
852
+ protectedProps.push('acGroup');
853
+ protectedProps.push('acOther');
854
+ protectedProps.push('acRead');
855
+ protectedProps.push('acWrite');
856
+ protectedProps.push('acFull');
857
+ if (
858
+ ((this.constructor as EntityConstructor).class !== 'User' &&
859
+ (this.constructor as EntityConstructor).class !== 'Group') ||
860
+ !(this.$nymph.tilmeld as any).currentUser ||
861
+ (!(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
862
+ 'tilmeld/admin',
863
+ ) &&
864
+ !(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
865
+ 'system/admin',
866
+ ))
867
+ ) {
868
+ protectedProps.push('user');
869
+ protectedProps.push('group');
870
+ }
680
871
  }
681
872
  for (const name of protectedProps) {
682
873
  if (name in this.$data) {
@@ -689,7 +880,7 @@ export default class Entity<T extends EntityData = EntityData>
689
880
 
690
881
  let nonAllowlistData: EntityData = {};
691
882
  if (this.$allowlistData != null) {
692
- nonAllowlistData = { ...this.$getData(true) };
883
+ nonAllowlistData = { ...this.$getData(true, true) };
693
884
  for (const name of this.$allowlistData) {
694
885
  delete nonAllowlistData[name];
695
886
  }
@@ -709,11 +900,11 @@ export default class Entity<T extends EntityData = EntityData>
709
900
  }
710
901
 
711
902
  public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
712
- this.$referenceWake();
903
+ this.$check();
713
904
 
714
905
  if (this.guid != patch.guid) {
715
906
  throw new EntityConflictError(
716
- 'Tried to accept JSON patch for the wrong entity.'
907
+ 'Tried to accept JSON patch for the wrong entity.',
717
908
  );
718
909
  }
719
910
 
@@ -725,11 +916,35 @@ export default class Entity<T extends EntityData = EntityData>
725
916
  }
726
917
  this.mdate = patch.mdate;
727
918
 
919
+ const protectedProps = [...this.$protectedData];
920
+ if (this.$nymph.tilmeld) {
921
+ protectedProps.push('acUser');
922
+ protectedProps.push('acGroup');
923
+ protectedProps.push('acOther');
924
+ protectedProps.push('acRead');
925
+ protectedProps.push('acWrite');
926
+ protectedProps.push('acFull');
927
+ if (
928
+ ((this.constructor as EntityConstructor).class !== 'User' &&
929
+ (this.constructor as EntityConstructor).class !== 'Group') ||
930
+ !(this.$nymph.tilmeld as any).currentUser ||
931
+ (!(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
932
+ 'tilmeld/admin',
933
+ ) &&
934
+ !(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
935
+ 'system/admin',
936
+ ))
937
+ ) {
938
+ protectedProps.push('user');
939
+ protectedProps.push('group');
940
+ }
941
+ }
942
+
728
943
  for (const name in patch.set) {
729
944
  if (
730
945
  (this.$allowlistData != null &&
731
946
  this.$allowlistData.indexOf(name) === -1) ||
732
- this.$protectedData.indexOf(name) !== -1 ||
947
+ protectedProps.indexOf(name) !== -1 ||
733
948
  this.$privateData.indexOf(name) !== -1
734
949
  ) {
735
950
  continue;
@@ -737,7 +952,7 @@ export default class Entity<T extends EntityData = EntityData>
737
952
  (this.$data as any)[name] = referencesToEntities(
738
953
  patch.set[name],
739
954
  this.$nymph,
740
- this.$skipAc
955
+ this.$skipAc,
741
956
  );
742
957
  }
743
958
 
@@ -745,7 +960,7 @@ export default class Entity<T extends EntityData = EntityData>
745
960
  if (
746
961
  (this.$allowlistData != null &&
747
962
  this.$allowlistData.indexOf(name) === -1) ||
748
- this.$protectedData.indexOf(name) !== -1 ||
963
+ protectedProps.indexOf(name) !== -1 ||
749
964
  this.$privateData.indexOf(name) !== -1
750
965
  ) {
751
966
  continue;
@@ -776,8 +991,12 @@ export default class Entity<T extends EntityData = EntityData>
776
991
  }
777
992
  }
778
993
 
779
- public $putData(data: EntityData, sdata?: SerializedEntityData) {
780
- this.$referenceWake();
994
+ public $putData(
995
+ data: EntityData,
996
+ sdata?: SerializedEntityData,
997
+ _source?: 'server',
998
+ ) {
999
+ this.$check();
781
1000
 
782
1001
  const mySdata = sdata ?? this.$getSData();
783
1002
  for (const name in data) {
@@ -790,13 +1009,13 @@ export default class Entity<T extends EntityData = EntityData>
790
1009
  (this.$dataStore as any)[name] = referencesToEntities(
791
1010
  data[name],
792
1011
  this.$nymph,
793
- this.$skipAc
1012
+ this.$skipAc,
794
1013
  );
795
1014
  }
796
1015
  this.$sdata = mySdata;
797
1016
  // Set original AC values if not set..
798
1017
  if (this.$originalAcValues == null) {
799
- this.$originalAcValues = this.$getOriginalAcValues();
1018
+ this.$originalAcValues = this.$getCurrentAcValues();
800
1019
  }
801
1020
  }
802
1021
 
@@ -813,14 +1032,14 @@ export default class Entity<T extends EntityData = EntityData>
813
1032
  ) {
814
1033
  throw new InvalidParametersError(
815
1034
  'referenceSleep expects parameter 1 to be a valid Nymph entity ' +
816
- 'reference.'
1035
+ 'reference.',
817
1036
  );
818
1037
  }
819
1038
  const thisClass: string = (this.constructor as any).class;
820
1039
  if (reference[2] !== thisClass) {
821
1040
  throw new InvalidParametersError(
822
1041
  'referenceSleep can only be called with an entity reference of the ' +
823
- `same class. Given class: ${reference[2]}; this class: ${thisClass}.`
1042
+ `same class. Given class: ${reference[2]}; this class: ${thisClass}.`,
824
1043
  );
825
1044
  }
826
1045
  this.$isASleepingReference = true;
@@ -828,49 +1047,141 @@ export default class Entity<T extends EntityData = EntityData>
828
1047
  this.$sleepingReference = reference;
829
1048
  }
830
1049
 
1050
+ /**
1051
+ * Check if this is a sleeping reference and throw an error if so.
1052
+ */
1053
+ protected $check() {
1054
+ if (this.$isASleepingReference || this.$sleepingReference != null) {
1055
+ throw new EntityIsSleepingReferenceError(
1056
+ 'This entity is in a sleeping reference state. You must use .$wake() to wake it.',
1057
+ );
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * Check if this is a sleeping reference.
1063
+ */
1064
+ public $asleep() {
1065
+ return this.$isASleepingReference || this.$sleepingReference != null;
1066
+ }
1067
+
831
1068
  /**
832
1069
  * Wake from a sleeping reference.
833
- *
834
- * @returns True on success, false on failure.
835
1070
  */
836
- protected $referenceWake() {
1071
+ public $wake() {
837
1072
  if (!this.$isASleepingReference || this.$sleepingReference == null) {
838
- return true;
1073
+ this.$wakePromise = null;
1074
+ return Promise.resolve(this);
839
1075
  }
840
- const EntityClass = this.$nymph.getEntityClass(this.$sleepingReference[2]);
841
- const entity = this.$nymph.driver.getEntitySync(
842
- {
843
- class: EntityClass,
844
- skipAc: this.$skipAc,
845
- },
846
- { type: '&', guid: this.$sleepingReference[1] }
847
- );
848
- if (entity == null) {
849
- return false;
1076
+ if (this.$sleepingReference?.[1] == null) {
1077
+ throw new InvalidStateError(
1078
+ 'Tried to wake a sleeping reference with no GUID.',
1079
+ );
850
1080
  }
851
- this.$isASleepingReference = false;
852
- this.$sleepingReference = null;
853
- this.guid = entity.guid;
854
- this.tags = entity.tags;
855
- this.cdate = entity.cdate;
856
- this.mdate = entity.mdate;
857
- this.$putData(entity.$getData(), entity.$getSData());
858
- return true;
1081
+ if (!this.$wakePromise) {
1082
+ const EntityClass = this.$nymph.getEntityClass(
1083
+ this.$sleepingReference[2],
1084
+ );
1085
+ this.$wakePromise = this.$nymph
1086
+ .getEntity(
1087
+ {
1088
+ class: EntityClass,
1089
+ skipAc: this.$skipAc,
1090
+ },
1091
+ { type: '&', guid: this.$sleepingReference[1] },
1092
+ )
1093
+ .then((entity) => {
1094
+ if (entity == null || entity.guid == null) {
1095
+ return Promise.reject(
1096
+ new InvalidStateError(
1097
+ 'The sleeping reference could not be retrieved.',
1098
+ ),
1099
+ );
1100
+ }
1101
+ this.$isASleepingReference = false;
1102
+ this.$sleepingReference = null;
1103
+ this.guid = entity.guid;
1104
+ this.tags = entity.tags;
1105
+ this.cdate = entity.cdate;
1106
+ this.mdate = entity.mdate;
1107
+ this.$putData(
1108
+ entity.$getData(false, true),
1109
+ entity.$getSData(),
1110
+ 'server',
1111
+ );
1112
+
1113
+ return this;
1114
+ })
1115
+ .finally(() => {
1116
+ this.$wakePromise = null;
1117
+ });
1118
+ }
1119
+ return this.$wakePromise;
1120
+ }
1121
+
1122
+ public $wakeAll(level?: number) {
1123
+ return new Promise((resolve, reject) => {
1124
+ // Run this once this entity is wake.
1125
+ const wakeProps = () => {
1126
+ let newLevel;
1127
+ // If level is undefined, keep going forever, otherwise, stop once we've
1128
+ // gone deep enough.
1129
+ if (level !== undefined) {
1130
+ newLevel = level - 1;
1131
+ }
1132
+ if (newLevel !== undefined && newLevel < 0) {
1133
+ resolve(this);
1134
+ return;
1135
+ }
1136
+ const promises = [];
1137
+ // Go through data looking for entities to wake.
1138
+ for (let [key, value] of Object.entries(this.$data)) {
1139
+ if (value instanceof Entity && value.$isASleepingReference) {
1140
+ promises.push(value.$wakeAll(newLevel));
1141
+ } else if (Array.isArray(value)) {
1142
+ for (let i = 0; i < value.length; i++) {
1143
+ if (
1144
+ value[i] instanceof Entity &&
1145
+ value[i].$isASleepingReference
1146
+ ) {
1147
+ promises.push(value[i].$wakeAll(newLevel));
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+ if (promises.length) {
1153
+ Promise.all(promises).then(
1154
+ () => resolve(this),
1155
+ (errObj) => reject(errObj),
1156
+ );
1157
+ } else {
1158
+ resolve(this);
1159
+ }
1160
+ };
1161
+
1162
+ if (this.$isASleepingReference) {
1163
+ this.$wake().then(wakeProps, (errObj) => reject(errObj));
1164
+ } else {
1165
+ wakeProps();
1166
+ }
1167
+ }) as Promise<Entity<T>>;
859
1168
  }
860
1169
 
861
1170
  public async $refresh() {
862
- this.$referenceWake();
1171
+ await this.$wake();
863
1172
 
864
1173
  if (this.guid == null) {
865
1174
  return false;
866
1175
  }
867
1176
  const refresh = await this.$nymph.getEntity(
868
1177
  {
869
- class: this.constructor as EntityConstructor,
1178
+ class: this.$nymph.getEntityClass(
1179
+ this.constructor as EntityConstructor,
1180
+ ),
870
1181
  skipCache: true,
871
1182
  skipAc: this.$skipAc,
872
1183
  },
873
- { type: '&', guid: this.guid }
1184
+ { type: '&', guid: this.guid },
874
1185
  );
875
1186
  if (refresh == null) {
876
1187
  return 0;
@@ -878,32 +1189,32 @@ export default class Entity<T extends EntityData = EntityData>
878
1189
  this.tags = refresh.tags;
879
1190
  this.cdate = refresh.cdate;
880
1191
  this.mdate = refresh.mdate;
881
- this.$putData(refresh.$getData(), refresh.$getSData());
1192
+ this.$putData(refresh.$getData(false, true), refresh.$getSData(), 'server');
882
1193
  return true;
883
1194
  }
884
1195
 
885
1196
  public $removeTag(...tags: string[]) {
886
- this.$referenceWake();
1197
+ this.$check();
887
1198
 
888
1199
  this.tags = difference(this.tags, tags);
889
1200
  }
890
1201
 
891
1202
  public async $save(): Promise<boolean> {
892
- this.$referenceWake();
1203
+ await this.$wake();
893
1204
 
894
1205
  return await this.$nymph.saveEntity(this);
895
1206
  }
896
1207
 
897
- public $toReference() {
1208
+ public $toReference(existingOnly?: boolean) {
898
1209
  if (this.$isASleepingReference && this.$sleepingReference != null) {
899
1210
  return this.$sleepingReference;
900
1211
  }
901
- if (this.guid == null) {
1212
+ if (existingOnly && this.guid == null) {
902
1213
  return this;
903
1214
  }
904
1215
  return [
905
1216
  'nymph_entity_reference',
906
- this.guid,
1217
+ this.$getGuaranteedGUID(),
907
1218
  (this.constructor as any).class as string,
908
1219
  ] as EntityReference;
909
1220
  }