@nymphjs/nymph 1.0.0-beta.12 → 1.0.0-beta.121

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 (103) hide show
  1. package/CHANGELOG.md +588 -0
  2. package/README.md +23 -21
  3. package/dist/Entity.d.ts +295 -12
  4. package/dist/Entity.js +560 -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 +590 -13
  11. package/dist/Nymph.js +846 -180
  12. package/dist/Nymph.js.map +1 -1
  13. package/dist/Nymph.types.d.ts +131 -8
  14. package/dist/Nymph.types.js +47 -5
  15. package/dist/Nymph.types.js.map +1 -1
  16. package/dist/conf/defaults.d.ts +1 -1
  17. package/dist/conf/defaults.js +26 -3
  18. package/dist/conf/defaults.js.map +1 -1
  19. package/dist/conf/index.d.ts +50 -2
  20. package/dist/conf/index.js +2 -8
  21. package/dist/conf/index.js.map +1 -1
  22. package/dist/driver/NymphDriver.d.ts +205 -40
  23. package/dist/driver/NymphDriver.js +712 -238
  24. package/dist/driver/NymphDriver.js.map +1 -1
  25. package/dist/driver/index.d.ts +1 -2
  26. package/dist/driver/index.js +2 -23
  27. package/dist/driver/index.js.map +1 -1
  28. package/dist/driver/testArtifacts.d.ts +4 -4
  29. package/dist/driver/testArtifacts.js +1920 -1086
  30. package/dist/driver/testArtifacts.js.map +1 -1
  31. package/dist/errors/ClassNotAvailableError.js +1 -5
  32. package/dist/errors/ClassNotAvailableError.js.map +1 -1
  33. package/dist/errors/EntityConflictError.js +1 -5
  34. package/dist/errors/EntityConflictError.js.map +1 -1
  35. package/dist/errors/EntityInvalidDataError.d.ts +6 -0
  36. package/dist/errors/EntityInvalidDataError.js +8 -6
  37. package/dist/errors/EntityInvalidDataError.js.map +1 -1
  38. package/dist/errors/EntityIsSleepingReferenceError.js +1 -5
  39. package/dist/errors/EntityIsSleepingReferenceError.js.map +1 -1
  40. package/dist/errors/EntityUniqueConstraintError.d.ts +3 -0
  41. package/dist/errors/EntityUniqueConstraintError.js +7 -0
  42. package/dist/errors/EntityUniqueConstraintError.js.map +1 -0
  43. package/dist/errors/HttpError.d.ts +5 -0
  44. package/dist/errors/HttpError.js +11 -0
  45. package/dist/errors/HttpError.js.map +1 -0
  46. package/dist/errors/InvalidParametersError.js +1 -5
  47. package/dist/errors/InvalidParametersError.js.map +1 -1
  48. package/dist/errors/InvalidStateError.d.ts +3 -0
  49. package/dist/errors/InvalidStateError.js +7 -0
  50. package/dist/errors/InvalidStateError.js.map +1 -0
  51. package/dist/errors/NotConfiguredError.js +1 -5
  52. package/dist/errors/NotConfiguredError.js.map +1 -1
  53. package/dist/errors/QueryFailedError.d.ts +2 -1
  54. package/dist/errors/QueryFailedError.js +5 -6
  55. package/dist/errors/QueryFailedError.js.map +1 -1
  56. package/dist/errors/UnableToConnectError.js +1 -5
  57. package/dist/errors/UnableToConnectError.js.map +1 -1
  58. package/dist/errors/index.d.ts +11 -8
  59. package/dist/errors/index.js +11 -24
  60. package/dist/errors/index.js.map +1 -1
  61. package/dist/index.d.ts +11 -11
  62. package/dist/index.js +14 -34
  63. package/dist/index.js.map +1 -1
  64. package/dist/nymphJoiProps.d.ts +1 -1
  65. package/dist/nymphJoiProps.js +10 -15
  66. package/dist/nymphJoiProps.js.map +1 -1
  67. package/dist/testArtifacts.d.ts +19 -6
  68. package/dist/testArtifacts.js +32 -37
  69. package/dist/testArtifacts.js.map +1 -1
  70. package/dist/testMocks.d.ts +27 -12
  71. package/dist/testMocks.js +40 -29
  72. package/dist/testMocks.js.map +1 -1
  73. package/dist/utils.d.ts +4 -4
  74. package/dist/utils.js +40 -29
  75. package/dist/utils.js.map +1 -1
  76. package/jest.config.js +31 -5
  77. package/package.json +33 -23
  78. package/src/Entity.test.ts +50 -7
  79. package/src/Entity.ts +489 -150
  80. package/src/Entity.types.ts +72 -74
  81. package/src/Nymph.ts +991 -324
  82. package/src/Nymph.types.ts +201 -27
  83. package/src/conf/defaults.ts +34 -1
  84. package/src/conf/index.ts +50 -2
  85. package/src/driver/NymphDriver.ts +898 -413
  86. package/src/driver/index.ts +1 -3
  87. package/src/driver/testArtifacts.ts +2557 -1629
  88. package/src/errors/EntityUniqueConstraintError.ts +6 -0
  89. package/src/errors/HttpError.ts +12 -0
  90. package/src/errors/InvalidStateError.ts +6 -0
  91. package/src/errors/QueryFailedError.ts +3 -1
  92. package/src/errors/index.ts +14 -8
  93. package/src/index.ts +11 -11
  94. package/src/nymphJoiProps.ts +2 -2
  95. package/src/testArtifacts.ts +20 -20
  96. package/src/testMocks.ts +76 -35
  97. package/src/utils.ts +39 -18
  98. package/tsconfig.json +6 -3
  99. package/typedoc.json +4 -0
  100. package/dist/conf/d.d.ts +0 -6
  101. package/dist/conf/d.js +0 -3
  102. package/dist/conf/d.js.map +0 -1
  103. package/src/conf/d.ts +0 -24
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.
@@ -87,9 +104,9 @@ import {
87
104
  * will fill its data from the DB. You can call clearCache() to turn all the
88
105
  * entities back into sleeping references.
89
106
  */
90
- export default class Entity<T extends EntityData = EntityData>
91
- implements EntityInterface
92
- {
107
+ export default class Entity<
108
+ T extends EntityData = EntityData,
109
+ > implements EntityInterface {
93
110
  /**
94
111
  * The instance of Nymph to use for queries.
95
112
  */
@@ -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,82 @@ 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 | string) =>
753
+ typeof entity === 'string' ? entity : entity.guid,
754
+ ) ?? null) as (string | null)[] | null;
755
+ }
756
+ public $getAcWriteIds() {
757
+ return (this.$data.acWrite?.map((entity: EntityInterface | string) =>
758
+ typeof entity === 'string' ? entity : entity.guid,
759
+ ) ?? null) as (string | null)[] | null;
760
+ }
761
+ public $getAcFullIds() {
762
+ return (this.$data.acFull?.map((entity: EntityInterface | string) =>
763
+ typeof entity === 'string' ? entity : entity.guid,
764
+ ) ?? null) as (string | null)[] | null;
564
765
  }
565
766
 
566
767
  public $getValidatable() {
567
- this.$referenceWake();
768
+ this.$check();
568
769
 
569
770
  // Access all the serialized properties to initialize them.
570
771
  for (const key in this.$sdata) {
@@ -580,13 +781,13 @@ export default class Entity<T extends EntityData = EntityData>
580
781
  }
581
782
 
582
783
  public $getTags() {
583
- this.$referenceWake();
784
+ this.$check();
584
785
 
585
786
  return this.tags;
586
787
  }
587
788
 
588
789
  public $hasTag(...tags: string[]) {
589
- this.$referenceWake();
790
+ this.$check();
590
791
 
591
792
  if (!tags.length) {
592
793
  return false;
@@ -608,11 +809,15 @@ export default class Entity<T extends EntityData = EntityData>
608
809
  return false;
609
810
  }
610
811
 
611
- if (this.guid || object.guid) {
812
+ if (this === object) {
813
+ return true;
814
+ }
815
+
816
+ if (this.guid != null || object.guid != null) {
612
817
  return this.guid === object.guid;
613
818
  }
614
819
 
615
- this.$referenceWake();
820
+ this.$check();
616
821
  if (typeof object.$getData !== 'function') {
617
822
  return false;
618
823
  } else {
@@ -621,19 +826,18 @@ export default class Entity<T extends EntityData = EntityData>
621
826
  if (!isEqual(obTags, myTags)) {
622
827
  return false;
623
828
  }
624
- const obData = object.$getData(true);
625
- const myData = this.$getData(true);
829
+ const obData = object.$getData(true, true);
830
+ const myData = this.$getData(true, true);
626
831
  return isEqual(obData, myData);
627
832
  }
628
833
  }
629
834
 
630
835
  public $jsonAcceptData(input: EntityJson, allowConflict = false) {
631
- // TODO: Do this without causing everything to become unserialized.
632
- this.$referenceWake();
836
+ this.$check();
633
837
 
634
838
  if (this.guid != input.guid) {
635
839
  throw new EntityConflictError(
636
- 'Tried to accept JSON input for the wrong entity.'
840
+ 'Tried to accept JSON input for the wrong entity.',
637
841
  );
638
842
  }
639
843
 
@@ -658,7 +862,7 @@ export default class Entity<T extends EntityData = EntityData>
658
862
  this.$addTag(...protectedTags, ...tags);
659
863
 
660
864
  // Accept the data.
661
- const data = input.data;
865
+ const data = { ...input.data };
662
866
  const privateData: EntityData = {};
663
867
  for (const name of this.$privateData) {
664
868
  if (name in this.$data) {
@@ -671,12 +875,27 @@ export default class Entity<T extends EntityData = EntityData>
671
875
 
672
876
  const protectedData: EntityData = {};
673
877
  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');
878
+ if (this.$nymph.tilmeld) {
879
+ protectedProps.push('acUser');
880
+ protectedProps.push('acGroup');
881
+ protectedProps.push('acOther');
882
+ protectedProps.push('acRead');
883
+ protectedProps.push('acWrite');
884
+ protectedProps.push('acFull');
885
+ if (
886
+ ((this.constructor as EntityConstructor).class !== 'User' &&
887
+ (this.constructor as EntityConstructor).class !== 'Group') ||
888
+ !(this.$nymph.tilmeld as any).currentUser ||
889
+ (!(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
890
+ 'tilmeld/admin',
891
+ ) &&
892
+ !(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
893
+ 'system/admin',
894
+ ))
895
+ ) {
896
+ protectedProps.push('user');
897
+ protectedProps.push('group');
898
+ }
680
899
  }
681
900
  for (const name of protectedProps) {
682
901
  if (name in this.$data) {
@@ -689,7 +908,7 @@ export default class Entity<T extends EntityData = EntityData>
689
908
 
690
909
  let nonAllowlistData: EntityData = {};
691
910
  if (this.$allowlistData != null) {
692
- nonAllowlistData = { ...this.$getData(true) };
911
+ nonAllowlistData = { ...this.$getData(true, true) };
693
912
  for (const name of this.$allowlistData) {
694
913
  delete nonAllowlistData[name];
695
914
  }
@@ -709,11 +928,11 @@ export default class Entity<T extends EntityData = EntityData>
709
928
  }
710
929
 
711
930
  public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
712
- this.$referenceWake();
931
+ this.$check();
713
932
 
714
933
  if (this.guid != patch.guid) {
715
934
  throw new EntityConflictError(
716
- 'Tried to accept JSON patch for the wrong entity.'
935
+ 'Tried to accept JSON patch for the wrong entity.',
717
936
  );
718
937
  }
719
938
 
@@ -725,11 +944,35 @@ export default class Entity<T extends EntityData = EntityData>
725
944
  }
726
945
  this.mdate = patch.mdate;
727
946
 
947
+ const protectedProps = [...this.$protectedData];
948
+ if (this.$nymph.tilmeld) {
949
+ protectedProps.push('acUser');
950
+ protectedProps.push('acGroup');
951
+ protectedProps.push('acOther');
952
+ protectedProps.push('acRead');
953
+ protectedProps.push('acWrite');
954
+ protectedProps.push('acFull');
955
+ if (
956
+ ((this.constructor as EntityConstructor).class !== 'User' &&
957
+ (this.constructor as EntityConstructor).class !== 'Group') ||
958
+ !(this.$nymph.tilmeld as any).currentUser ||
959
+ (!(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
960
+ 'tilmeld/admin',
961
+ ) &&
962
+ !(this.$nymph.tilmeld as any).currentUser.abilities?.includes(
963
+ 'system/admin',
964
+ ))
965
+ ) {
966
+ protectedProps.push('user');
967
+ protectedProps.push('group');
968
+ }
969
+ }
970
+
728
971
  for (const name in patch.set) {
729
972
  if (
730
973
  (this.$allowlistData != null &&
731
974
  this.$allowlistData.indexOf(name) === -1) ||
732
- this.$protectedData.indexOf(name) !== -1 ||
975
+ protectedProps.indexOf(name) !== -1 ||
733
976
  this.$privateData.indexOf(name) !== -1
734
977
  ) {
735
978
  continue;
@@ -737,7 +980,7 @@ export default class Entity<T extends EntityData = EntityData>
737
980
  (this.$data as any)[name] = referencesToEntities(
738
981
  patch.set[name],
739
982
  this.$nymph,
740
- this.$skipAc
983
+ this.$skipAc,
741
984
  );
742
985
  }
743
986
 
@@ -745,7 +988,7 @@ export default class Entity<T extends EntityData = EntityData>
745
988
  if (
746
989
  (this.$allowlistData != null &&
747
990
  this.$allowlistData.indexOf(name) === -1) ||
748
- this.$protectedData.indexOf(name) !== -1 ||
991
+ protectedProps.indexOf(name) !== -1 ||
749
992
  this.$privateData.indexOf(name) !== -1
750
993
  ) {
751
994
  continue;
@@ -776,8 +1019,12 @@ export default class Entity<T extends EntityData = EntityData>
776
1019
  }
777
1020
  }
778
1021
 
779
- public $putData(data: EntityData, sdata?: SerializedEntityData) {
780
- this.$referenceWake();
1022
+ public $putData(
1023
+ data: EntityData,
1024
+ sdata?: SerializedEntityData,
1025
+ _source?: 'server',
1026
+ ) {
1027
+ this.$check();
781
1028
 
782
1029
  const mySdata = sdata ?? this.$getSData();
783
1030
  for (const name in data) {
@@ -790,13 +1037,13 @@ export default class Entity<T extends EntityData = EntityData>
790
1037
  (this.$dataStore as any)[name] = referencesToEntities(
791
1038
  data[name],
792
1039
  this.$nymph,
793
- this.$skipAc
1040
+ this.$skipAc,
794
1041
  );
795
1042
  }
796
1043
  this.$sdata = mySdata;
797
1044
  // Set original AC values if not set..
798
1045
  if (this.$originalAcValues == null) {
799
- this.$originalAcValues = this.$getOriginalAcValues();
1046
+ this.$originalAcValues = this.$getCurrentAcValues();
800
1047
  }
801
1048
  }
802
1049
 
@@ -813,14 +1060,14 @@ export default class Entity<T extends EntityData = EntityData>
813
1060
  ) {
814
1061
  throw new InvalidParametersError(
815
1062
  'referenceSleep expects parameter 1 to be a valid Nymph entity ' +
816
- 'reference.'
1063
+ 'reference.',
817
1064
  );
818
1065
  }
819
1066
  const thisClass: string = (this.constructor as any).class;
820
1067
  if (reference[2] !== thisClass) {
821
1068
  throw new InvalidParametersError(
822
1069
  'referenceSleep can only be called with an entity reference of the ' +
823
- `same class. Given class: ${reference[2]}; this class: ${thisClass}.`
1070
+ `same class. Given class: ${reference[2]}; this class: ${thisClass}.`,
824
1071
  );
825
1072
  }
826
1073
  this.$isASleepingReference = true;
@@ -828,49 +1075,141 @@ export default class Entity<T extends EntityData = EntityData>
828
1075
  this.$sleepingReference = reference;
829
1076
  }
830
1077
 
1078
+ /**
1079
+ * Check if this is a sleeping reference and throw an error if so.
1080
+ */
1081
+ protected $check() {
1082
+ if (this.$isASleepingReference || this.$sleepingReference != null) {
1083
+ throw new EntityIsSleepingReferenceError(
1084
+ 'This entity is in a sleeping reference state. You must use .$wake() to wake it.',
1085
+ );
1086
+ }
1087
+ }
1088
+
1089
+ /**
1090
+ * Check if this is a sleeping reference.
1091
+ */
1092
+ public $asleep() {
1093
+ return this.$isASleepingReference || this.$sleepingReference != null;
1094
+ }
1095
+
831
1096
  /**
832
1097
  * Wake from a sleeping reference.
833
- *
834
- * @returns True on success, false on failure.
835
1098
  */
836
- protected $referenceWake() {
1099
+ public $wake() {
837
1100
  if (!this.$isASleepingReference || this.$sleepingReference == null) {
838
- return true;
1101
+ this.$wakePromise = null;
1102
+ return Promise.resolve(this);
839
1103
  }
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;
1104
+ if (this.$sleepingReference?.[1] == null) {
1105
+ throw new InvalidStateError(
1106
+ 'Tried to wake a sleeping reference with no GUID.',
1107
+ );
850
1108
  }
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;
1109
+ if (!this.$wakePromise) {
1110
+ const EntityClass = this.$nymph.getEntityClass(
1111
+ this.$sleepingReference[2],
1112
+ );
1113
+ this.$wakePromise = this.$nymph
1114
+ .getEntity(
1115
+ {
1116
+ class: EntityClass,
1117
+ skipAc: this.$skipAc,
1118
+ },
1119
+ { type: '&', guid: this.$sleepingReference[1] },
1120
+ )
1121
+ .then((entity) => {
1122
+ if (entity == null || entity.guid == null) {
1123
+ return Promise.reject(
1124
+ new InvalidStateError(
1125
+ 'The sleeping reference could not be retrieved.',
1126
+ ),
1127
+ );
1128
+ }
1129
+ this.$isASleepingReference = false;
1130
+ this.$sleepingReference = null;
1131
+ this.guid = entity.guid;
1132
+ this.tags = entity.tags;
1133
+ this.cdate = entity.cdate;
1134
+ this.mdate = entity.mdate;
1135
+ this.$putData(
1136
+ entity.$getData(false, true),
1137
+ entity.$getSData(),
1138
+ 'server',
1139
+ );
1140
+
1141
+ return this;
1142
+ })
1143
+ .finally(() => {
1144
+ this.$wakePromise = null;
1145
+ });
1146
+ }
1147
+ return this.$wakePromise;
1148
+ }
1149
+
1150
+ public $wakeAll(level?: number) {
1151
+ return new Promise((resolve, reject) => {
1152
+ // Run this once this entity is wake.
1153
+ const wakeProps = () => {
1154
+ let newLevel;
1155
+ // If level is undefined, keep going forever, otherwise, stop once we've
1156
+ // gone deep enough.
1157
+ if (level !== undefined) {
1158
+ newLevel = level - 1;
1159
+ }
1160
+ if (newLevel !== undefined && newLevel < 0) {
1161
+ resolve(this);
1162
+ return;
1163
+ }
1164
+ const promises = [];
1165
+ // Go through data looking for entities to wake.
1166
+ for (let [key, value] of Object.entries(this.$data)) {
1167
+ if (value instanceof Entity && value.$isASleepingReference) {
1168
+ promises.push(value.$wakeAll(newLevel));
1169
+ } else if (Array.isArray(value)) {
1170
+ for (let i = 0; i < value.length; i++) {
1171
+ if (
1172
+ value[i] instanceof Entity &&
1173
+ value[i].$isASleepingReference
1174
+ ) {
1175
+ promises.push(value[i].$wakeAll(newLevel));
1176
+ }
1177
+ }
1178
+ }
1179
+ }
1180
+ if (promises.length) {
1181
+ Promise.all(promises).then(
1182
+ () => resolve(this),
1183
+ (errObj) => reject(errObj),
1184
+ );
1185
+ } else {
1186
+ resolve(this);
1187
+ }
1188
+ };
1189
+
1190
+ if (this.$isASleepingReference) {
1191
+ this.$wake().then(wakeProps, (errObj) => reject(errObj));
1192
+ } else {
1193
+ wakeProps();
1194
+ }
1195
+ }) as Promise<Entity<T>>;
859
1196
  }
860
1197
 
861
1198
  public async $refresh() {
862
- this.$referenceWake();
1199
+ await this.$wake();
863
1200
 
864
1201
  if (this.guid == null) {
865
1202
  return false;
866
1203
  }
867
1204
  const refresh = await this.$nymph.getEntity(
868
1205
  {
869
- class: this.constructor as EntityConstructor,
1206
+ class: this.$nymph.getEntityClass(
1207
+ this.constructor as EntityConstructor,
1208
+ ),
870
1209
  skipCache: true,
871
1210
  skipAc: this.$skipAc,
872
1211
  },
873
- { type: '&', guid: this.guid }
1212
+ { type: '&', guid: this.guid },
874
1213
  );
875
1214
  if (refresh == null) {
876
1215
  return 0;
@@ -878,32 +1217,32 @@ export default class Entity<T extends EntityData = EntityData>
878
1217
  this.tags = refresh.tags;
879
1218
  this.cdate = refresh.cdate;
880
1219
  this.mdate = refresh.mdate;
881
- this.$putData(refresh.$getData(), refresh.$getSData());
1220
+ this.$putData(refresh.$getData(false, true), refresh.$getSData(), 'server');
882
1221
  return true;
883
1222
  }
884
1223
 
885
1224
  public $removeTag(...tags: string[]) {
886
- this.$referenceWake();
1225
+ this.$check();
887
1226
 
888
1227
  this.tags = difference(this.tags, tags);
889
1228
  }
890
1229
 
891
1230
  public async $save(): Promise<boolean> {
892
- this.$referenceWake();
1231
+ await this.$wake();
893
1232
 
894
1233
  return await this.$nymph.saveEntity(this);
895
1234
  }
896
1235
 
897
- public $toReference() {
1236
+ public $toReference(existingOnly?: boolean) {
898
1237
  if (this.$isASleepingReference && this.$sleepingReference != null) {
899
1238
  return this.$sleepingReference;
900
1239
  }
901
- if (this.guid == null) {
1240
+ if (existingOnly && this.guid == null) {
902
1241
  return this;
903
1242
  }
904
1243
  return [
905
1244
  'nymph_entity_reference',
906
- this.guid,
1245
+ this.$getGuaranteedGUID(),
907
1246
  (this.constructor as any).class as string,
908
1247
  ] as EntityReference;
909
1248
  }