@nymphjs/nymph 1.0.0-beta.10 → 1.0.0-beta.100

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