@nymphjs/nymph 1.0.0-beta.13 → 1.0.0-beta.131

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