@nymphjs/nymph 1.0.0-beta.1 → 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 +535 -0
  2. package/README.md +14 -22
  3. package/dist/Entity.d.ts +293 -10
  4. package/dist/Entity.js +571 -149
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Entity.test.js +67 -26
  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 +497 -13
  11. package/dist/Nymph.js +761 -188
  12. package/dist/Nymph.js.map +1 -1
  13. package/dist/Nymph.types.d.ts +95 -26
  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 +181 -43
  25. package/dist/driver/NymphDriver.js +532 -285
  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 +939 -219
  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 +28 -11
  68. package/dist/testMocks.js +44 -28
  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 +24 -20
  75. package/src/Entity.test.ts +63 -13
  76. package/src/Entity.ts +498 -156
  77. package/src/Entity.types.ts +72 -74
  78. package/src/Nymph.ts +904 -336
  79. package/src/Nymph.types.ts +179 -44
  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 +650 -460
  84. package/src/driver/index.ts +2 -2
  85. package/src/driver/testArtifacts.ts +966 -196
  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 +85 -37
  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.
@@ -45,9 +62,9 @@ import {
45
62
  *
46
63
  * Some notes about $equals() and $is(), the replacements for "==":
47
64
  *
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.
65
+ * The == operator will likely not give you the result you want, since two
66
+ * instances of the same entity will fail that check, even though they represent
67
+ * the same data in the database.
51
68
  *
52
69
  * $equals() performs a more strict comparison of the entity to another. Use
53
70
  * $equals() instead of the == operator when you want to check both the entities
@@ -56,8 +73,7 @@ import {
56
73
  *
57
74
  * - They must be entities.
58
75
  * - 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.
76
+ * - Their data and tags must be equal.
61
77
  *
62
78
  * $is() performs a less strict comparison of the entity to another. Use $is()
63
79
  * instead of the == operator when the entity's data may have been changed, but
@@ -66,7 +82,7 @@ import {
66
82
  *
67
83
  * - They must be entities.
68
84
  * - They must have equal GUIDs, or both must have no GUID.
69
- * - If they have no GUIDs, their data must be equal.
85
+ * - If they have no GUIDs, their data and tags must be equal.
70
86
  *
71
87
  * Some notes about saving entities in other entity's properties:
72
88
  *
@@ -137,6 +153,10 @@ export default class Entity<T extends EntityData = EntityData>
137
153
  * The reference to use to wake.
138
154
  */
139
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;
140
160
  /**
141
161
  * Properties that will not be serialized into JSON with toJSON(). This
142
162
  * can be considered a denylist, because these properties will not be set
@@ -146,6 +166,18 @@ export default class Entity<T extends EntityData = EntityData>
146
166
  * also listed in searchRestrictedData.
147
167
  */
148
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;
149
181
  /**
150
182
  * Properties that will not be searchable from the frontend. If the frontend
151
183
  * includes any of these properties in any of their clauses, they will be
@@ -157,8 +189,20 @@ export default class Entity<T extends EntityData = EntityData>
157
189
  * be visible on the frontend, unlike $privateData, but any changes to them
158
190
  * that come from the frontend will be ignored.
159
191
  *
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.
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.
162
206
  */
163
207
  protected $protectedData: string[] = [];
164
208
  /**
@@ -197,28 +241,39 @@ export default class Entity<T extends EntityData = EntityData>
197
241
  * The AC properties' values when the entity was loaded.
198
242
  */
199
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;
200
248
 
201
249
  /**
202
- * Load an entity.
203
- * @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.
204
254
  */
205
- 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[]) {
206
261
  this.$nymph = (this.constructor as EntityConstructor).nymph;
207
262
  this.$dataHandler = {
208
263
  has: (data: EntityData, name: string) => {
209
- this.$referenceWake();
264
+ this.$check();
210
265
 
211
266
  return name in data || name in this.$sdata;
212
267
  },
213
268
 
214
269
  get: (data: EntityData, name: string) => {
215
- this.$referenceWake();
270
+ this.$check();
216
271
 
217
272
  if (this.$sdata.hasOwnProperty(name)) {
218
273
  data[name] = referencesToEntities(
219
274
  JSON.parse(this.$sdata[name]),
220
275
  this.$nymph,
221
- this.$skipAc
276
+ this.$skipAc,
222
277
  );
223
278
  delete this.$sdata[name];
224
279
  }
@@ -229,7 +284,7 @@ export default class Entity<T extends EntityData = EntityData>
229
284
  },
230
285
 
231
286
  set: (data: EntityData, name: string, value: any) => {
232
- this.$referenceWake();
287
+ this.$check();
233
288
 
234
289
  if (this.$sdata.hasOwnProperty(name)) {
235
290
  delete this.$sdata[name];
@@ -239,7 +294,7 @@ export default class Entity<T extends EntityData = EntityData>
239
294
  },
240
295
 
241
296
  deleteProperty: (data: EntityData, name: string) => {
242
- this.$referenceWake();
297
+ this.$check();
243
298
 
244
299
  if (this.$sdata.hasOwnProperty(name)) {
245
300
  return delete this.$sdata[name];
@@ -253,9 +308,9 @@ export default class Entity<T extends EntityData = EntityData>
253
308
  defineProperty: (
254
309
  data: EntityData,
255
310
  name: string,
256
- descriptor: PropertyDescriptor
311
+ descriptor: PropertyDescriptor,
257
312
  ) => {
258
- this.$referenceWake();
313
+ this.$check();
259
314
 
260
315
  if (this.$sdata.hasOwnProperty(name)) {
261
316
  delete this.$sdata[name];
@@ -265,13 +320,13 @@ export default class Entity<T extends EntityData = EntityData>
265
320
  },
266
321
 
267
322
  getOwnPropertyDescriptor: (data: EntityData, name: string) => {
268
- this.$referenceWake();
323
+ this.$check();
269
324
 
270
325
  if (this.$sdata.hasOwnProperty(name)) {
271
326
  data[name] = referencesToEntities(
272
327
  JSON.parse(this.$sdata[name]),
273
328
  this.$nymph,
274
- this.$skipAc
329
+ this.$skipAc,
275
330
  );
276
331
  delete this.$sdata[name];
277
332
  }
@@ -279,10 +334,10 @@ export default class Entity<T extends EntityData = EntityData>
279
334
  },
280
335
 
281
336
  ownKeys: (data: EntityData) => {
282
- this.$referenceWake();
337
+ this.$check();
283
338
 
284
339
  return Object.getOwnPropertyNames(data).concat(
285
- Object.getOwnPropertyNames(this.$sdata)
340
+ Object.getOwnPropertyNames(this.$sdata),
286
341
  );
287
342
  },
288
343
  };
@@ -291,20 +346,6 @@ export default class Entity<T extends EntityData = EntityData>
291
346
 
292
347
  this.$data = new Proxy(this.$dataStore, this.$dataHandler);
293
348
 
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
349
  return new Proxy(this, {
309
350
  has: (entity: Entity, name: string) => {
310
351
  if (
@@ -315,7 +356,7 @@ export default class Entity<T extends EntityData = EntityData>
315
356
  return name in entity;
316
357
  }
317
358
 
318
- this.$referenceWake();
359
+ this.$check();
319
360
  return name in entity.$data;
320
361
  },
321
362
 
@@ -326,12 +367,12 @@ export default class Entity<T extends EntityData = EntityData>
326
367
  name.substring(0, 1) === '$'
327
368
  ) {
328
369
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
329
- this.$referenceWake();
370
+ this.$check();
330
371
  }
331
372
  return (entity as any)[name];
332
373
  }
333
374
 
334
- this.$referenceWake();
375
+ this.$check();
335
376
  if (name in entity.$data) {
336
377
  return entity.$data[name];
337
378
  }
@@ -345,11 +386,11 @@ export default class Entity<T extends EntityData = EntityData>
345
386
  name.substring(0, 1) === '$'
346
387
  ) {
347
388
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
348
- this.$referenceWake();
389
+ this.$check();
349
390
  }
350
391
  (entity as any)[name] = value;
351
392
  } else {
352
- this.$referenceWake();
393
+ this.$check();
353
394
  (entity.$data as any)[name] = value;
354
395
  }
355
396
  return true;
@@ -359,7 +400,7 @@ export default class Entity<T extends EntityData = EntityData>
359
400
  if (name in entity) {
360
401
  return delete (entity as any)[name];
361
402
  } else if (name in entity.$data) {
362
- this.$referenceWake();
403
+ this.$check();
363
404
  return delete entity.$data[name];
364
405
  }
365
406
  return true;
@@ -372,7 +413,7 @@ export default class Entity<T extends EntityData = EntityData>
372
413
  defineProperty: (
373
414
  entity: Entity,
374
415
  name: string,
375
- descriptor: PropertyDescriptor
416
+ descriptor: PropertyDescriptor,
376
417
  ) => {
377
418
  if (
378
419
  typeof name !== 'string' ||
@@ -380,11 +421,11 @@ export default class Entity<T extends EntityData = EntityData>
380
421
  name.substring(0, 1) === '$'
381
422
  ) {
382
423
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
383
- this.$referenceWake();
424
+ this.$check();
384
425
  }
385
426
  Object.defineProperty(entity, name, descriptor);
386
427
  } else {
387
- this.$referenceWake();
428
+ this.$check();
388
429
  Object.defineProperty(entity.$data, name, descriptor);
389
430
  }
390
431
  return true;
@@ -397,48 +438,147 @@ export default class Entity<T extends EntityData = EntityData>
397
438
  name.substring(0, 1) === '$'
398
439
  ) {
399
440
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
400
- this.$referenceWake();
441
+ this.$check();
401
442
  }
402
443
  return Object.getOwnPropertyDescriptor(entity, name);
403
444
  } else {
404
- this.$referenceWake();
445
+ this.$check();
405
446
  return Object.getOwnPropertyDescriptor(entity.$data, name);
406
447
  }
407
448
  },
408
449
 
409
450
  ownKeys: (entity: Entity) => {
410
- this.$referenceWake();
451
+ this.$check();
411
452
  return Object.getOwnPropertyNames(entity).concat(
412
- Object.getOwnPropertyNames(entity.$data)
453
+ Object.getOwnPropertyNames(entity.$data),
413
454
  );
414
455
  },
415
456
  }) as Entity<T>;
416
457
  }
417
458
 
418
- 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>> {
419
472
  const entity = new this();
420
473
  if (guid != null) {
421
- const entity = await this.nymph.getEntity(
474
+ const entity = await (
475
+ this as unknown as EntityConstructor
476
+ ).nymph.getEntity(
422
477
  {
423
- class: this as any as EntityConstructor,
478
+ class: this as unknown as EntityConstructor,
424
479
  },
425
- { type: '&', guid }
480
+ { type: '&', guid },
426
481
  );
427
482
  if (entity != null) {
428
- return entity;
483
+ return entity as E & EntityDataType<E>;
429
484
  }
430
485
  }
431
- return entity;
486
+ return entity as E & EntityDataType<E>;
432
487
  }
433
488
 
434
- public static factorySync(guid?: string) {
435
- 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>;
436
496
  }
437
497
 
438
- 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> {
439
513
  const entity = new this();
440
514
  entity.$referenceSleep(reference);
441
- 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;
442
582
  }
443
583
 
444
584
  public toJSON() {
@@ -453,17 +593,29 @@ export default class Entity<T extends EntityData = EntityData>
453
593
  tags: [...this.tags],
454
594
  data: {},
455
595
  };
456
- const data = this.$getData(true);
457
- for (const key in data) {
596
+ for (const key of [
597
+ ...Object.keys(this.$dataStore),
598
+ ...Object.keys(this.$sdata),
599
+ ]) {
458
600
  if (this.$privateData.indexOf(key) === -1) {
459
- obj.data[key] = entitiesToReferences(data[key]);
601
+ obj.data[key] = entitiesToReferences(this.$data[key], true);
460
602
  }
461
603
  }
462
604
  return obj;
463
605
  }
464
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
+
465
617
  public $addTag(...tags: string[]) {
466
- this.$referenceWake();
618
+ this.$check();
467
619
 
468
620
  if (tags.length < 1) {
469
621
  return;
@@ -472,7 +624,7 @@ export default class Entity<T extends EntityData = EntityData>
472
624
  }
473
625
 
474
626
  public $arraySearch(array: any[], strict = false) {
475
- this.$referenceWake();
627
+ this.$check();
476
628
 
477
629
  if (!Array.isArray(array)) {
478
630
  return -1;
@@ -487,9 +639,9 @@ export default class Entity<T extends EntityData = EntityData>
487
639
  }
488
640
 
489
641
  public $clearCache() {
490
- this.$referenceWake();
642
+ this.$check();
491
643
 
492
- this.$putData(this.$getData(), this.$getSData());
644
+ this.$putData(this.$getData(false, true), this.$getSData());
493
645
  }
494
646
 
495
647
  public $getClientEnabledMethods() {
@@ -497,13 +649,13 @@ export default class Entity<T extends EntityData = EntityData>
497
649
  }
498
650
 
499
651
  public async $delete(): Promise<boolean> {
500
- this.$referenceWake();
652
+ this.$check();
501
653
 
502
654
  return await this.$nymph.deleteEntity(this);
503
655
  }
504
656
 
505
657
  public $equals(object: any) {
506
- this.$referenceWake();
658
+ this.$check();
507
659
 
508
660
  if (!(object instanceof Entity)) {
509
661
  return false;
@@ -513,22 +665,24 @@ export default class Entity<T extends EntityData = EntityData>
513
665
  return false;
514
666
  }
515
667
  }
516
- if (object.constructor !== this.constructor) {
517
- return false;
518
- }
519
668
  if (object.cdate !== this.cdate) {
520
669
  return false;
521
670
  }
522
671
  if (object.mdate !== this.mdate) {
523
672
  return false;
524
673
  }
525
- const obData = object.$getData(true);
526
- const myData = this.$getData(true);
674
+ const obTags = [...object.tags].sort();
675
+ const myTags = [...this.tags].sort();
676
+ if (!isEqual(obTags, myTags)) {
677
+ return false;
678
+ }
679
+ const obData = object.$getData(true, true);
680
+ const myData = this.$getData(true, true);
527
681
  return isEqual(obData, myData);
528
682
  }
529
683
 
530
- public $getData(includeSData = false) {
531
- this.$referenceWake();
684
+ public $getData(includeSData = false, referenceOnlyExisting?: boolean) {
685
+ this.$check();
532
686
 
533
687
  if (includeSData) {
534
688
  // Access all the serialized properties to initialize them.
@@ -536,34 +690,79 @@ export default class Entity<T extends EntityData = EntityData>
536
690
  const _unused: any = (this as any)[key];
537
691
  }
538
692
  }
539
- return entitiesToReferences({ ...this.$dataStore });
693
+ return entitiesToReferences({ ...this.$dataStore }, referenceOnlyExisting);
540
694
  }
541
695
 
542
696
  public $getSData() {
543
- this.$referenceWake();
697
+ this.$check();
544
698
 
545
699
  return this.$sdata;
546
700
  }
547
701
 
548
- public $getOriginalAcValues() {
549
- 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();
550
714
 
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,
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
+ }
726
+
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];
561
735
  }
562
- );
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;
563
762
  }
564
763
 
565
764
  public $getValidatable() {
566
- this.$referenceWake();
765
+ this.$check();
567
766
 
568
767
  // Access all the serialized properties to initialize them.
569
768
  for (const key in this.$sdata) {
@@ -579,13 +778,13 @@ export default class Entity<T extends EntityData = EntityData>
579
778
  }
580
779
 
581
780
  public $getTags() {
582
- this.$referenceWake();
781
+ this.$check();
583
782
 
584
783
  return this.tags;
585
784
  }
586
785
 
587
786
  public $hasTag(...tags: string[]) {
588
- this.$referenceWake();
787
+ this.$check();
589
788
 
590
789
  if (!tags.length) {
591
790
  return false;
@@ -607,27 +806,35 @@ export default class Entity<T extends EntityData = EntityData>
607
806
  return false;
608
807
  }
609
808
 
610
- if (this.guid || object.guid) {
809
+ if (this === object) {
810
+ return true;
811
+ }
812
+
813
+ if (this.guid != null || object.guid != null) {
611
814
  return this.guid === object.guid;
612
815
  }
613
816
 
614
- this.$referenceWake();
817
+ this.$check();
615
818
  if (typeof object.$getData !== 'function') {
616
819
  return false;
617
820
  } else {
618
- const obData = object.$getData(true);
619
- const myData = this.$getData(true);
821
+ const obTags = [...object.tags].sort();
822
+ const myTags = [...this.tags].sort();
823
+ if (!isEqual(obTags, myTags)) {
824
+ return false;
825
+ }
826
+ const obData = object.$getData(true, true);
827
+ const myData = this.$getData(true, true);
620
828
  return isEqual(obData, myData);
621
829
  }
622
830
  }
623
831
 
624
832
  public $jsonAcceptData(input: EntityJson, allowConflict = false) {
625
- // TODO: Do this without causing everything to become unserialized.
626
- this.$referenceWake();
833
+ this.$check();
627
834
 
628
835
  if (this.guid != input.guid) {
629
836
  throw new EntityConflictError(
630
- 'Tried to accept JSON input for the wrong entity.'
837
+ 'Tried to accept JSON input for the wrong entity.',
631
838
  );
632
839
  }
633
840
 
@@ -652,7 +859,7 @@ export default class Entity<T extends EntityData = EntityData>
652
859
  this.$addTag(...protectedTags, ...tags);
653
860
 
654
861
  // Accept the data.
655
- const data = input.data;
862
+ const data = { ...input.data };
656
863
  const privateData: EntityData = {};
657
864
  for (const name of this.$privateData) {
658
865
  if (name in this.$data) {
@@ -665,12 +872,27 @@ export default class Entity<T extends EntityData = EntityData>
665
872
 
666
873
  const protectedData: EntityData = {};
667
874
  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');
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
+ }
674
896
  }
675
897
  for (const name of protectedProps) {
676
898
  if (name in this.$data) {
@@ -683,7 +905,7 @@ export default class Entity<T extends EntityData = EntityData>
683
905
 
684
906
  let nonAllowlistData: EntityData = {};
685
907
  if (this.$allowlistData != null) {
686
- nonAllowlistData = { ...this.$getData(true) };
908
+ nonAllowlistData = { ...this.$getData(true, true) };
687
909
  for (const name of this.$allowlistData) {
688
910
  delete nonAllowlistData[name];
689
911
  }
@@ -703,11 +925,11 @@ export default class Entity<T extends EntityData = EntityData>
703
925
  }
704
926
 
705
927
  public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
706
- this.$referenceWake();
928
+ this.$check();
707
929
 
708
930
  if (this.guid != patch.guid) {
709
931
  throw new EntityConflictError(
710
- 'Tried to accept JSON patch for the wrong entity.'
932
+ 'Tried to accept JSON patch for the wrong entity.',
711
933
  );
712
934
  }
713
935
 
@@ -719,11 +941,35 @@ export default class Entity<T extends EntityData = EntityData>
719
941
  }
720
942
  this.mdate = patch.mdate;
721
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
+
722
968
  for (const name in patch.set) {
723
969
  if (
724
970
  (this.$allowlistData != null &&
725
971
  this.$allowlistData.indexOf(name) === -1) ||
726
- this.$protectedData.indexOf(name) !== -1 ||
972
+ protectedProps.indexOf(name) !== -1 ||
727
973
  this.$privateData.indexOf(name) !== -1
728
974
  ) {
729
975
  continue;
@@ -731,7 +977,7 @@ export default class Entity<T extends EntityData = EntityData>
731
977
  (this.$data as any)[name] = referencesToEntities(
732
978
  patch.set[name],
733
979
  this.$nymph,
734
- this.$skipAc
980
+ this.$skipAc,
735
981
  );
736
982
  }
737
983
 
@@ -739,7 +985,7 @@ export default class Entity<T extends EntityData = EntityData>
739
985
  if (
740
986
  (this.$allowlistData != null &&
741
987
  this.$allowlistData.indexOf(name) === -1) ||
742
- this.$protectedData.indexOf(name) !== -1 ||
988
+ protectedProps.indexOf(name) !== -1 ||
743
989
  this.$privateData.indexOf(name) !== -1
744
990
  ) {
745
991
  continue;
@@ -770,8 +1016,12 @@ export default class Entity<T extends EntityData = EntityData>
770
1016
  }
771
1017
  }
772
1018
 
773
- public $putData(data: EntityData, sdata?: SerializedEntityData) {
774
- this.$referenceWake();
1019
+ public $putData(
1020
+ data: EntityData,
1021
+ sdata?: SerializedEntityData,
1022
+ _source?: 'server',
1023
+ ) {
1024
+ this.$check();
775
1025
 
776
1026
  const mySdata = sdata ?? this.$getSData();
777
1027
  for (const name in data) {
@@ -784,13 +1034,13 @@ export default class Entity<T extends EntityData = EntityData>
784
1034
  (this.$dataStore as any)[name] = referencesToEntities(
785
1035
  data[name],
786
1036
  this.$nymph,
787
- this.$skipAc
1037
+ this.$skipAc,
788
1038
  );
789
1039
  }
790
1040
  this.$sdata = mySdata;
791
1041
  // Set original AC values if not set..
792
1042
  if (this.$originalAcValues == null) {
793
- this.$originalAcValues = this.$getOriginalAcValues();
1043
+ this.$originalAcValues = this.$getCurrentAcValues();
794
1044
  }
795
1045
  }
796
1046
 
@@ -807,14 +1057,14 @@ export default class Entity<T extends EntityData = EntityData>
807
1057
  ) {
808
1058
  throw new InvalidParametersError(
809
1059
  'referenceSleep expects parameter 1 to be a valid Nymph entity ' +
810
- 'reference.'
1060
+ 'reference.',
811
1061
  );
812
1062
  }
813
1063
  const thisClass: string = (this.constructor as any).class;
814
1064
  if (reference[2] !== thisClass) {
815
1065
  throw new InvalidParametersError(
816
1066
  'referenceSleep can only be called with an entity reference of the ' +
817
- `same class. Given class: ${reference[2]}; this class: ${thisClass}.`
1067
+ `same class. Given class: ${reference[2]}; this class: ${thisClass}.`,
818
1068
  );
819
1069
  }
820
1070
  this.$isASleepingReference = true;
@@ -822,49 +1072,141 @@ export default class Entity<T extends EntityData = EntityData>
822
1072
  this.$sleepingReference = reference;
823
1073
  }
824
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
+
825
1093
  /**
826
1094
  * Wake from a sleeping reference.
827
- *
828
- * @returns True on success, false on failure.
829
1095
  */
830
- protected $referenceWake() {
1096
+ public $wake() {
831
1097
  if (!this.$isASleepingReference || this.$sleepingReference == null) {
832
- return true;
1098
+ this.$wakePromise = null;
1099
+ return Promise.resolve(this);
833
1100
  }
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;
1101
+ if (this.$sleepingReference?.[1] == null) {
1102
+ throw new InvalidStateError(
1103
+ 'Tried to wake a sleeping reference with no GUID.',
1104
+ );
844
1105
  }
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;
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>>;
853
1193
  }
854
1194
 
855
1195
  public async $refresh() {
856
- this.$referenceWake();
1196
+ await this.$wake();
857
1197
 
858
1198
  if (this.guid == null) {
859
1199
  return false;
860
1200
  }
861
1201
  const refresh = await this.$nymph.getEntity(
862
1202
  {
863
- class: this.constructor as EntityConstructor,
1203
+ class: this.$nymph.getEntityClass(
1204
+ this.constructor as EntityConstructor,
1205
+ ),
864
1206
  skipCache: true,
865
1207
  skipAc: this.$skipAc,
866
1208
  },
867
- { type: '&', guid: this.guid }
1209
+ { type: '&', guid: this.guid },
868
1210
  );
869
1211
  if (refresh == null) {
870
1212
  return 0;
@@ -872,32 +1214,32 @@ export default class Entity<T extends EntityData = EntityData>
872
1214
  this.tags = refresh.tags;
873
1215
  this.cdate = refresh.cdate;
874
1216
  this.mdate = refresh.mdate;
875
- this.$putData(refresh.$getData(), refresh.$getSData());
1217
+ this.$putData(refresh.$getData(false, true), refresh.$getSData(), 'server');
876
1218
  return true;
877
1219
  }
878
1220
 
879
1221
  public $removeTag(...tags: string[]) {
880
- this.$referenceWake();
1222
+ this.$check();
881
1223
 
882
1224
  this.tags = difference(this.tags, tags);
883
1225
  }
884
1226
 
885
1227
  public async $save(): Promise<boolean> {
886
- this.$referenceWake();
1228
+ await this.$wake();
887
1229
 
888
1230
  return await this.$nymph.saveEntity(this);
889
1231
  }
890
1232
 
891
- public $toReference() {
1233
+ public $toReference(existingOnly?: boolean) {
892
1234
  if (this.$isASleepingReference && this.$sleepingReference != null) {
893
1235
  return this.$sleepingReference;
894
1236
  }
895
- if (this.guid == null) {
1237
+ if (existingOnly && this.guid == null) {
896
1238
  return this;
897
1239
  }
898
1240
  return [
899
1241
  'nymph_entity_reference',
900
- this.guid,
1242
+ this.$getGuaranteedGUID(),
901
1243
  (this.constructor as any).class as string,
902
1244
  ] as EntityReference;
903
1245
  }