@nymphjs/nymph 1.0.0-beta.6 → 1.0.0-beta.60

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 (64) hide show
  1. package/CHANGELOG.md +273 -0
  2. package/README.md +13 -21
  3. package/dist/Entity.d.ts +269 -7
  4. package/dist/Entity.js +431 -89
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Entity.test.js +38 -16
  7. package/dist/Entity.test.js.map +1 -1
  8. package/dist/Entity.types.d.ts +225 -19
  9. package/dist/Nymph.d.ts +423 -11
  10. package/dist/Nymph.js +649 -156
  11. package/dist/Nymph.js.map +1 -1
  12. package/dist/Nymph.types.d.ts +31 -7
  13. package/dist/Nymph.types.js +2 -1
  14. package/dist/Nymph.types.js.map +1 -1
  15. package/dist/conf/d.d.ts +26 -0
  16. package/dist/conf/defaults.js +22 -0
  17. package/dist/conf/defaults.js.map +1 -1
  18. package/dist/driver/NymphDriver.d.ts +111 -36
  19. package/dist/driver/NymphDriver.js +169 -68
  20. package/dist/driver/NymphDriver.js.map +1 -1
  21. package/dist/driver/testArtifacts.d.ts +1 -1
  22. package/dist/driver/testArtifacts.js +285 -18
  23. package/dist/driver/testArtifacts.js.map +1 -1
  24. package/dist/errors/EntityInvalidDataError.d.ts +6 -0
  25. package/dist/errors/EntityInvalidDataError.js +6 -0
  26. package/dist/errors/EntityInvalidDataError.js.map +1 -1
  27. package/dist/errors/EntityUniqueConstraintError.d.ts +3 -0
  28. package/dist/errors/EntityUniqueConstraintError.js +11 -0
  29. package/dist/errors/EntityUniqueConstraintError.js.map +1 -0
  30. package/dist/errors/InvalidStateError.d.ts +3 -0
  31. package/dist/errors/InvalidStateError.js +11 -0
  32. package/dist/errors/InvalidStateError.js.map +1 -0
  33. package/dist/errors/QueryFailedError.js.map +1 -1
  34. package/dist/errors/index.d.ts +2 -0
  35. package/dist/errors/index.js +2 -0
  36. package/dist/errors/index.js.map +1 -1
  37. package/dist/testArtifacts.d.ts +17 -5
  38. package/dist/testArtifacts.js +23 -19
  39. package/dist/testArtifacts.js.map +1 -1
  40. package/dist/testMocks.d.ts +15 -10
  41. package/dist/testMocks.js +34 -11
  42. package/dist/testMocks.js.map +1 -1
  43. package/dist/utils.d.ts +1 -1
  44. package/dist/utils.js +21 -5
  45. package/dist/utils.js.map +1 -1
  46. package/package.json +16 -14
  47. package/src/Entity.test.ts +30 -10
  48. package/src/Entity.ts +406 -135
  49. package/src/Entity.types.ts +32 -66
  50. package/src/Nymph.ts +626 -196
  51. package/src/Nymph.types.ts +97 -26
  52. package/src/conf/d.ts +8 -0
  53. package/src/conf/defaults.ts +24 -0
  54. package/src/driver/NymphDriver.ts +187 -243
  55. package/src/driver/testArtifacts.ts +327 -146
  56. package/src/errors/EntityUniqueConstraintError.ts +6 -0
  57. package/src/errors/InvalidStateError.ts +6 -0
  58. package/src/errors/index.ts +4 -0
  59. package/src/nymphJoiProps.ts +2 -2
  60. package/src/testArtifacts.ts +18 -19
  61. package/src/testMocks.ts +64 -32
  62. package/src/utils.ts +22 -8
  63. package/tsconfig.json +1 -1
  64. package/typedoc.json +4 -0
package/dist/Entity.js CHANGED
@@ -3,28 +3,152 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const lodash_1 = require("lodash");
4
4
  const errors_1 = require("./errors");
5
5
  const utils_1 = require("./utils");
6
+ /**
7
+ * Database abstraction object.
8
+ *
9
+ * Provides a way to access, manipulate, and store data in Nymph.
10
+ *
11
+ * The GUID is not set until the entity is saved. GUIDs must be unique forever,
12
+ * even after deletion. It's the job of the Nymph DB driver to make sure no two
13
+ * entities ever have the same GUID. This is generally done by using a large
14
+ * randomly generated ID.
15
+ *
16
+ * Each entity class has an etype that determines which table(s) in the database
17
+ * it belongs to. If two entity classes have the same etype, their data will be
18
+ * stored in the same table(s). This isn't a good idea, however, because
19
+ * references to an entity store a class name, not an etype.
20
+ *
21
+ * Tags are used to classify entities. Where an etype is used to separate data
22
+ * by tables, tags can be used to separate entities within a table. You can
23
+ * define specific tags to be protected, meaning they cannot be added/removed
24
+ * from the client. It can be useful to allow user defined tags, such as for
25
+ * blog posts.
26
+ *
27
+ * Simply calling $delete() will not unset the entity. It will still take up
28
+ * memory. Likewise, simply calling unset will not delete the entity from the
29
+ * DB.
30
+ *
31
+ * Some notes about $equals() and $is(), the replacements for "==":
32
+ *
33
+ * The == operator will likely not give you the result you want, since two
34
+ * instances of the same entity will fail that check, even though they represent
35
+ * the same data in the database.
36
+ *
37
+ * $equals() performs a more strict comparison of the entity to another. Use
38
+ * $equals() instead of the == operator when you want to check both the entities
39
+ * they represent, and the data inside them. In order to return true for
40
+ * $equals(), the entity and object must meet the following criteria:
41
+ *
42
+ * - They must be entities.
43
+ * - They must have equal GUIDs, or both must have no GUID.
44
+ * - Their data and tags must be equal.
45
+ *
46
+ * $is() performs a less strict comparison of the entity to another. Use $is()
47
+ * instead of the == operator when the entity's data may have been changed, but
48
+ * you only care if they represent the same entity. In order to return true, the
49
+ * entity and object must meet the following criteria:
50
+ *
51
+ * - They must be entities.
52
+ * - They must have equal GUIDs, or both must have no GUID.
53
+ * - If they have no GUIDs, their data and tags must be equal.
54
+ *
55
+ * Some notes about saving entities in other entity's properties:
56
+ *
57
+ * Entities use references in the DB to store an entity in their properties. The
58
+ * reference is stored as an array with the values:
59
+ *
60
+ * - 0 => The string 'nymph_entity_reference'
61
+ * - 1 => The referenced entity's GUID.
62
+ * - 2 => The referenced entity's class name.
63
+ *
64
+ * Since the referenced entity's class name (meaning the `class` static
65
+ * property, not the name of the class itself) is stored in the reference on the
66
+ * parent entity, if you change the class name in an update, you need to
67
+ * reassign all referenced entities of that class and resave.
68
+ *
69
+ * When an entity is loaded, it does not request its referenced entities from
70
+ * Nymph. Instead, it creates instances without data called sleeping references.
71
+ * When you first access an entity's data, if it is a sleeping reference, it
72
+ * will fill its data from the DB. You can call clearCache() to turn all the
73
+ * entities back into sleeping references.
74
+ */
6
75
  class Entity {
7
- constructor(guid) {
76
+ /**
77
+ * Initialize an entity.
78
+ */
79
+ constructor(..._rest) {
8
80
  this.guid = null;
9
81
  this.cdate = null;
10
82
  this.mdate = null;
11
83
  this.tags = [];
84
+ /**
85
+ * Whether this instance is a sleeping reference.
86
+ */
12
87
  this.$isASleepingReference = false;
88
+ /**
89
+ * The reference to use to wake.
90
+ */
13
91
  this.$sleepingReference = null;
92
+ /**
93
+ * A promise that resolved when the entity's data is wake.
94
+ */
95
+ this.$wakePromise = null;
96
+ /**
97
+ * Properties that will not be serialized into JSON with toJSON(). This
98
+ * can be considered a denylist, because these properties will not be set
99
+ * with incoming JSON.
100
+ *
101
+ * Clients CAN still determine what is in these properties, unless they are
102
+ * also listed in searchRestrictedData.
103
+ */
14
104
  this.$privateData = [];
105
+ /**
106
+ * Properties that can only be modified by server side code. They will still
107
+ * be visible on the frontend, unlike $privateData, but any changes to them
108
+ * that come from the frontend will be ignored.
109
+ *
110
+ * In addition to what's listed here, all of the access control properties
111
+ * will be included when Tilmeld is being used. These are:
112
+ *
113
+ * - acUser
114
+ * - acGroup
115
+ * - acOther
116
+ * - acRead
117
+ * - acWrite
118
+ * - acFull
119
+ * - user
120
+ * - group
121
+ *
122
+ * You should modify these through client enabled methods or the $save method
123
+ * instead, for safety.
124
+ */
15
125
  this.$protectedData = [];
126
+ /**
127
+ * Tags that can only be added/removed by server side code. They will still be
128
+ * visible on the frontend, but any changes to them that come from the
129
+ * frontend will be ignored.
130
+ */
16
131
  this.$protectedTags = [];
132
+ /**
133
+ * The names of methods allowed to be called by the frontend with serverCall.
134
+ */
17
135
  this.$clientEnabledMethods = [];
136
+ /**
137
+ * Whether to use "skipAc" when accessing entity references.
138
+ */
18
139
  this.$skipAc = false;
140
+ /**
141
+ * The AC properties' values when the entity was loaded.
142
+ */
19
143
  this.$originalAcValues = null;
20
144
  this.$nymph = this.constructor.nymph;
21
145
  this.$dataHandler = {
22
146
  has: (data, name) => {
23
- this.$referenceWake();
147
+ this.$check();
24
148
  return name in data || name in this.$sdata;
25
149
  },
26
150
  get: (data, name) => {
27
- this.$referenceWake();
151
+ this.$check();
28
152
  if (this.$sdata.hasOwnProperty(name)) {
29
153
  data[name] = (0, utils_1.referencesToEntities)(JSON.parse(this.$sdata[name]), this.$nymph, this.$skipAc);
30
154
  delete this.$sdata[name];
@@ -35,7 +159,7 @@ class Entity {
35
159
  return undefined;
36
160
  },
37
161
  set: (data, name, value) => {
38
- this.$referenceWake();
162
+ this.$check();
39
163
  if (this.$sdata.hasOwnProperty(name)) {
40
164
  delete this.$sdata[name];
41
165
  }
@@ -43,7 +167,7 @@ class Entity {
43
167
  return true;
44
168
  },
45
169
  deleteProperty: (data, name) => {
46
- this.$referenceWake();
170
+ this.$check();
47
171
  if (this.$sdata.hasOwnProperty(name)) {
48
172
  return delete this.$sdata[name];
49
173
  }
@@ -53,7 +177,7 @@ class Entity {
53
177
  return true;
54
178
  },
55
179
  defineProperty: (data, name, descriptor) => {
56
- this.$referenceWake();
180
+ this.$check();
57
181
  if (this.$sdata.hasOwnProperty(name)) {
58
182
  delete this.$sdata[name];
59
183
  }
@@ -61,7 +185,7 @@ class Entity {
61
185
  return true;
62
186
  },
63
187
  getOwnPropertyDescriptor: (data, name) => {
64
- this.$referenceWake();
188
+ this.$check();
65
189
  if (this.$sdata.hasOwnProperty(name)) {
66
190
  data[name] = (0, utils_1.referencesToEntities)(JSON.parse(this.$sdata[name]), this.$nymph, this.$skipAc);
67
191
  delete this.$sdata[name];
@@ -69,23 +193,13 @@ class Entity {
69
193
  return Object.getOwnPropertyDescriptor(data, name);
70
194
  },
71
195
  ownKeys: (data) => {
72
- this.$referenceWake();
196
+ this.$check();
73
197
  return Object.getOwnPropertyNames(data).concat(Object.getOwnPropertyNames(this.$sdata));
74
198
  },
75
199
  };
76
200
  this.$dataStore = {};
77
201
  this.$sdata = {};
78
202
  this.$data = new Proxy(this.$dataStore, this.$dataHandler);
79
- if (guid != null) {
80
- const entity = this.$nymph.driver.getEntitySync({ class: this.constructor }, { type: '&', guid });
81
- if (entity) {
82
- this.guid = entity.guid;
83
- this.tags = entity.tags;
84
- this.cdate = entity.cdate;
85
- this.mdate = entity.mdate;
86
- this.$putData(entity.$getData(), entity.$getSData());
87
- }
88
- }
89
203
  return new Proxy(this, {
90
204
  has: (entity, name) => {
91
205
  if (typeof name !== 'string' ||
@@ -93,7 +207,7 @@ class Entity {
93
207
  name.substring(0, 1) === '$') {
94
208
  return name in entity;
95
209
  }
96
- this.$referenceWake();
210
+ this.$check();
97
211
  return name in entity.$data;
98
212
  },
99
213
  get: (entity, name) => {
@@ -101,11 +215,11 @@ class Entity {
101
215
  name in entity ||
102
216
  name.substring(0, 1) === '$') {
103
217
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
104
- this.$referenceWake();
218
+ this.$check();
105
219
  }
106
220
  return entity[name];
107
221
  }
108
- this.$referenceWake();
222
+ this.$check();
109
223
  if (name in entity.$data) {
110
224
  return entity.$data[name];
111
225
  }
@@ -116,12 +230,12 @@ class Entity {
116
230
  name in entity ||
117
231
  name.substring(0, 1) === '$') {
118
232
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
119
- this.$referenceWake();
233
+ this.$check();
120
234
  }
121
235
  entity[name] = value;
122
236
  }
123
237
  else {
124
- this.$referenceWake();
238
+ this.$check();
125
239
  entity.$data[name] = value;
126
240
  }
127
241
  return true;
@@ -131,7 +245,7 @@ class Entity {
131
245
  return delete entity[name];
132
246
  }
133
247
  else if (name in entity.$data) {
134
- this.$referenceWake();
248
+ this.$check();
135
249
  return delete entity.$data[name];
136
250
  }
137
251
  return true;
@@ -144,12 +258,12 @@ class Entity {
144
258
  name in entity ||
145
259
  name.substring(0, 1) === '$') {
146
260
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
147
- this.$referenceWake();
261
+ this.$check();
148
262
  }
149
263
  Object.defineProperty(entity, name, descriptor);
150
264
  }
151
265
  else {
152
- this.$referenceWake();
266
+ this.$check();
153
267
  Object.defineProperty(entity.$data, name, descriptor);
154
268
  }
155
269
  return true;
@@ -159,21 +273,28 @@ class Entity {
159
273
  name in entity ||
160
274
  name.substring(0, 1) === '$') {
161
275
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
162
- this.$referenceWake();
276
+ this.$check();
163
277
  }
164
278
  return Object.getOwnPropertyDescriptor(entity, name);
165
279
  }
166
280
  else {
167
- this.$referenceWake();
281
+ this.$check();
168
282
  return Object.getOwnPropertyDescriptor(entity.$data, name);
169
283
  }
170
284
  },
171
285
  ownKeys: (entity) => {
172
- this.$referenceWake();
286
+ this.$check();
173
287
  return Object.getOwnPropertyNames(entity).concat(Object.getOwnPropertyNames(entity.$data));
174
288
  },
175
289
  });
176
290
  }
291
+ /**
292
+ * Create or retrieve a new entity instance.
293
+ *
294
+ * Note that this will always return an entity, even if the GUID is not found.
295
+ *
296
+ * @param guid An optional GUID to retrieve.
297
+ */
177
298
  static async factory(guid) {
178
299
  const entity = new this();
179
300
  if (guid != null) {
@@ -186,14 +307,56 @@ class Entity {
186
307
  }
187
308
  return entity;
188
309
  }
189
- static factorySync(guid) {
190
- return new this(guid);
310
+ /**
311
+ * Create a new entity instance.
312
+ */
313
+ static factorySync() {
314
+ return new this();
191
315
  }
316
+ /**
317
+ * Create a new sleeping reference instance.
318
+ *
319
+ * Sleeping references won't retrieve their data from the database until they
320
+ * are readied with `$wake()` or a parent's `$wakeAll()`.
321
+ *
322
+ * @param reference The Nymph Entity Reference to use to wake.
323
+ * @returns The new instance.
324
+ */
192
325
  static factoryReference(reference) {
193
326
  const entity = new this();
194
327
  entity.$referenceSleep(reference);
195
328
  return entity;
196
329
  }
330
+ /**
331
+ * Get an array of strings that **must** be unique across the current etype.
332
+ *
333
+ * When you try to save another entity with any of the same unique strings,
334
+ * Nymph will throw an error.
335
+ *
336
+ * The default implementation of this static method instantiates the entity,
337
+ * assigns all of the given data, then calls `$getUniques` and returns its
338
+ * output. This can have a performance impact if a lot of extra processing
339
+ * happens during any of these steps. You can override this method to
340
+ * calculate the unique strings faster, but you must return the same strings
341
+ * that would be returned by `$getUniques`.
342
+ *
343
+ * @returns Resolves to an array of entity's unique constraint strings.
344
+ */
345
+ static async getUniques({ guid, cdate, mdate, tags, data, sdata, }) {
346
+ const entity = new this();
347
+ if (guid != null) {
348
+ entity.guid = guid;
349
+ }
350
+ if (cdate != null) {
351
+ entity.cdate = cdate;
352
+ }
353
+ if (mdate != null) {
354
+ entity.mdate = mdate;
355
+ }
356
+ entity.tags = tags;
357
+ entity.$putData(data, sdata);
358
+ return await entity.$getUniques();
359
+ }
197
360
  toJSON() {
198
361
  if (this.$isASleepingReference) {
199
362
  return this.$sleepingReference;
@@ -215,14 +378,14 @@ class Entity {
215
378
  return obj;
216
379
  }
217
380
  $addTag(...tags) {
218
- this.$referenceWake();
381
+ this.$check();
219
382
  if (tags.length < 1) {
220
383
  return;
221
384
  }
222
385
  this.tags = (0, utils_1.uniqueStrings)([...this.tags, ...tags]);
223
386
  }
224
387
  $arraySearch(array, strict = false) {
225
- this.$referenceWake();
388
+ this.$check();
226
389
  if (!Array.isArray(array)) {
227
390
  return -1;
228
391
  }
@@ -235,18 +398,18 @@ class Entity {
235
398
  return -1;
236
399
  }
237
400
  $clearCache() {
238
- this.$referenceWake();
401
+ this.$check();
239
402
  this.$putData(this.$getData(), this.$getSData());
240
403
  }
241
404
  $getClientEnabledMethods() {
242
405
  return this.$clientEnabledMethods;
243
406
  }
244
407
  async $delete() {
245
- this.$referenceWake();
408
+ this.$check();
246
409
  return await this.$nymph.deleteEntity(this);
247
410
  }
248
411
  $equals(object) {
249
- this.$referenceWake();
412
+ this.$check();
250
413
  if (!(object instanceof Entity)) {
251
414
  return false;
252
415
  }
@@ -255,22 +418,25 @@ class Entity {
255
418
  return false;
256
419
  }
257
420
  }
258
- if (object.constructor !== this.constructor) {
259
- return false;
260
- }
261
421
  if (object.cdate !== this.cdate) {
262
422
  return false;
263
423
  }
264
424
  if (object.mdate !== this.mdate) {
265
425
  return false;
266
426
  }
427
+ const obTags = [...object.tags].sort();
428
+ const myTags = [...this.tags].sort();
429
+ if (!(0, lodash_1.isEqual)(obTags, myTags)) {
430
+ return false;
431
+ }
267
432
  const obData = object.$getData(true);
268
433
  const myData = this.$getData(true);
269
434
  return (0, lodash_1.isEqual)(obData, myData);
270
435
  }
271
436
  $getData(includeSData = false) {
272
- this.$referenceWake();
437
+ this.$check();
273
438
  if (includeSData) {
439
+ // Access all the serialized properties to initialize them.
274
440
  for (const key in this.$sdata) {
275
441
  const _unused = this[key];
276
442
  }
@@ -278,24 +444,50 @@ class Entity {
278
444
  return (0, utils_1.entitiesToReferences)({ ...this.$dataStore });
279
445
  }
280
446
  $getSData() {
281
- this.$referenceWake();
447
+ this.$check();
282
448
  return this.$sdata;
283
449
  }
450
+ async $getUniques() {
451
+ return [];
452
+ }
284
453
  $getOriginalAcValues() {
285
- this.$referenceWake();
286
- return (this.$originalAcValues ?? {
287
- user: this.$data.user,
288
- group: this.$data.group,
289
- acUser: this.$data.acUser,
290
- acGroup: this.$data.acGroup,
291
- acOther: this.$data.acOther,
292
- acRead: this.$data.acRead,
293
- acWrite: this.$data.acWrite,
294
- acFull: this.$data.acFull,
295
- });
454
+ this.$check();
455
+ return this.$originalAcValues ?? this.$getCurrentAcValues();
456
+ }
457
+ $getCurrentAcValues() {
458
+ this.$check();
459
+ return {
460
+ user: this.$getAcUid(),
461
+ group: this.$getAcGid(),
462
+ acUser: this.$data.acUser ?? null,
463
+ acGroup: this.$data.acGroup ?? null,
464
+ acOther: this.$data.acOther ?? null,
465
+ acRead: this.$getAcReadIds(),
466
+ acWrite: this.$getAcWriteIds(),
467
+ acFull: this.$getAcFullIds(),
468
+ };
469
+ }
470
+ $getAcUid() {
471
+ return (this.$data.user?.guid ?? null);
472
+ }
473
+ $getAcGid() {
474
+ return (this.$data.group?.guid ?? null);
475
+ }
476
+ $getAcReadIds() {
477
+ return (this.$data.acRead?.map((entity) => entity.guid) ??
478
+ null);
479
+ }
480
+ $getAcWriteIds() {
481
+ return (this.$data.acWrite?.map((entity) => entity.guid) ??
482
+ null);
483
+ }
484
+ $getAcFullIds() {
485
+ return (this.$data.acFull?.map((entity) => entity.guid) ??
486
+ null);
296
487
  }
297
488
  $getValidatable() {
298
- this.$referenceWake();
489
+ this.$check();
490
+ // Access all the serialized properties to initialize them.
299
491
  for (const key in this.$sdata) {
300
492
  const _unused = this[key];
301
493
  }
@@ -308,11 +500,11 @@ class Entity {
308
500
  };
309
501
  }
310
502
  $getTags() {
311
- this.$referenceWake();
503
+ this.$check();
312
504
  return this.tags;
313
505
  }
314
506
  $hasTag(...tags) {
315
- this.$referenceWake();
507
+ this.$check();
316
508
  if (!tags.length) {
317
509
  return false;
318
510
  }
@@ -333,27 +525,34 @@ class Entity {
333
525
  if (this.guid || object.guid) {
334
526
  return this.guid === object.guid;
335
527
  }
336
- this.$referenceWake();
528
+ this.$check();
337
529
  if (typeof object.$getData !== 'function') {
338
530
  return false;
339
531
  }
340
532
  else {
533
+ const obTags = [...object.tags].sort();
534
+ const myTags = [...this.tags].sort();
535
+ if (!(0, lodash_1.isEqual)(obTags, myTags)) {
536
+ return false;
537
+ }
341
538
  const obData = object.$getData(true);
342
539
  const myData = this.$getData(true);
343
540
  return (0, lodash_1.isEqual)(obData, myData);
344
541
  }
345
542
  }
346
543
  $jsonAcceptData(input, allowConflict = false) {
347
- this.$referenceWake();
544
+ this.$check();
348
545
  if (this.guid != input.guid) {
349
546
  throw new errors_1.EntityConflictError('Tried to accept JSON input for the wrong entity.');
350
547
  }
548
+ // Accept the modified date.
351
549
  const mdate = input.mdate ?? 0;
352
550
  const thismdate = this.mdate ?? 0;
353
551
  if (mdate < thismdate && !allowConflict) {
354
552
  throw new errors_1.EntityConflictError('This entity is newer than JSON input.');
355
553
  }
356
554
  this.mdate = input.mdate;
555
+ // Accept the tags.
357
556
  const currentTags = this.$getTags();
358
557
  const protectedTags = (0, lodash_1.intersection)(this.$protectedTags, currentTags);
359
558
  let tags = (0, lodash_1.difference)(input.tags, this.$protectedTags);
@@ -362,6 +561,7 @@ class Entity {
362
561
  }
363
562
  this.$removeTag(...currentTags);
364
563
  this.$addTag(...protectedTags, ...tags);
564
+ // Accept the data.
365
565
  const data = input.data;
366
566
  const privateData = {};
367
567
  for (const name of this.$privateData) {
@@ -374,10 +574,21 @@ class Entity {
374
574
  }
375
575
  const protectedData = {};
376
576
  const protectedProps = [...this.$protectedData];
377
- if (this.$nymph.tilmeld &&
378
- !this.$nymph.tilmeld.gatekeeper('tilmeld/admin')) {
379
- protectedProps.push('user');
380
- protectedProps.push('group');
577
+ if (this.$nymph.tilmeld) {
578
+ protectedProps.push('acUser');
579
+ protectedProps.push('acGroup');
580
+ protectedProps.push('acOther');
581
+ protectedProps.push('acRead');
582
+ protectedProps.push('acWrite');
583
+ protectedProps.push('acFull');
584
+ if ((this.constructor.class !== 'User' &&
585
+ this.constructor.class !== 'Group') ||
586
+ !this.$nymph.tilmeld.currentUser ||
587
+ (!this.$nymph.tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
588
+ !this.$nymph.tilmeld.currentUser.abilities?.includes('system/admin'))) {
589
+ protectedProps.push('user');
590
+ protectedProps.push('group');
591
+ }
381
592
  }
382
593
  for (const name of protectedProps) {
383
594
  if (name in this.$data) {
@@ -407,20 +618,38 @@ class Entity {
407
618
  });
408
619
  }
409
620
  $jsonAcceptPatch(patch, allowConflict = false) {
410
- this.$referenceWake();
621
+ this.$check();
411
622
  if (this.guid != patch.guid) {
412
623
  throw new errors_1.EntityConflictError('Tried to accept JSON patch for the wrong entity.');
413
624
  }
625
+ // Accept the modified date.
414
626
  const mdate = patch.mdate ?? 0;
415
627
  const thismdate = this.mdate ?? 0;
416
628
  if (mdate < thismdate && !allowConflict) {
417
629
  throw new errors_1.EntityConflictError('This entity is newer than JSON patch.');
418
630
  }
419
631
  this.mdate = patch.mdate;
632
+ const protectedProps = [...this.$protectedData];
633
+ if (this.$nymph.tilmeld) {
634
+ protectedProps.push('acUser');
635
+ protectedProps.push('acGroup');
636
+ protectedProps.push('acOther');
637
+ protectedProps.push('acRead');
638
+ protectedProps.push('acWrite');
639
+ protectedProps.push('acFull');
640
+ if ((this.constructor.class !== 'User' &&
641
+ this.constructor.class !== 'Group') ||
642
+ !this.$nymph.tilmeld.currentUser ||
643
+ (!this.$nymph.tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
644
+ !this.$nymph.tilmeld.currentUser.abilities?.includes('system/admin'))) {
645
+ protectedProps.push('user');
646
+ protectedProps.push('group');
647
+ }
648
+ }
420
649
  for (const name in patch.set) {
421
650
  if ((this.$allowlistData != null &&
422
651
  this.$allowlistData.indexOf(name) === -1) ||
423
- this.$protectedData.indexOf(name) !== -1 ||
652
+ protectedProps.indexOf(name) !== -1 ||
424
653
  this.$privateData.indexOf(name) !== -1) {
425
654
  continue;
426
655
  }
@@ -429,7 +658,7 @@ class Entity {
429
658
  for (const name of patch.unset) {
430
659
  if ((this.$allowlistData != null &&
431
660
  this.$allowlistData.indexOf(name) === -1) ||
432
- this.$protectedData.indexOf(name) !== -1 ||
661
+ protectedProps.indexOf(name) !== -1 ||
433
662
  this.$privateData.indexOf(name) !== -1) {
434
663
  continue;
435
664
  }
@@ -453,7 +682,7 @@ class Entity {
453
682
  }
454
683
  }
455
684
  $putData(data, sdata) {
456
- this.$referenceWake();
685
+ this.$check();
457
686
  const mySdata = sdata ?? this.$getSData();
458
687
  for (const name in data) {
459
688
  delete mySdata[name];
@@ -465,10 +694,15 @@ class Entity {
465
694
  this.$dataStore[name] = (0, utils_1.referencesToEntities)(data[name], this.$nymph, this.$skipAc);
466
695
  }
467
696
  this.$sdata = mySdata;
697
+ // Set original AC values if not set..
468
698
  if (this.$originalAcValues == null) {
469
- this.$originalAcValues = this.$getOriginalAcValues();
699
+ this.$originalAcValues = this.$getCurrentAcValues();
470
700
  }
471
701
  }
702
+ /**
703
+ * Set up a sleeping reference.
704
+ * @param array $reference The reference to use to wake.
705
+ */
472
706
  $referenceSleep(reference) {
473
707
  if (reference.length !== 3 ||
474
708
  reference[0] !== 'nymph_entity_reference' ||
@@ -486,34 +720,108 @@ class Entity {
486
720
  this.guid = reference[1];
487
721
  this.$sleepingReference = reference;
488
722
  }
489
- $referenceWake() {
490
- if (!this.$isASleepingReference || this.$sleepingReference == null) {
491
- return true;
723
+ /**
724
+ * Check if this is a sleeping reference and throw an error if so.
725
+ */
726
+ $check() {
727
+ if (this.$isASleepingReference || this.$sleepingReference != null) {
728
+ throw new errors_1.EntityIsSleepingReferenceError('This entity is in a sleeping reference state. You must use .$wake() to wake it.');
492
729
  }
493
- const EntityClass = this.$nymph.getEntityClass(this.$sleepingReference[2]);
494
- const entity = this.$nymph.driver.getEntitySync({
495
- class: EntityClass,
496
- skipAc: this.$skipAc,
497
- }, { type: '&', guid: this.$sleepingReference[1] });
498
- if (entity == null) {
499
- return false;
730
+ }
731
+ /**
732
+ * Check if this is a sleeping reference.
733
+ */
734
+ $asleep() {
735
+ return this.$isASleepingReference || this.$sleepingReference != null;
736
+ }
737
+ /**
738
+ * Wake from a sleeping reference.
739
+ */
740
+ $wake() {
741
+ if (!this.$isASleepingReference || this.$sleepingReference == null) {
742
+ this.$wakePromise = null;
743
+ return Promise.resolve(this);
744
+ }
745
+ if (this.$sleepingReference?.[1] == null) {
746
+ throw new errors_1.InvalidStateError('Tried to wake a sleeping reference with no GUID.');
747
+ }
748
+ if (!this.$wakePromise) {
749
+ const EntityClass = this.$nymph.getEntityClass(this.$sleepingReference[2]);
750
+ this.$wakePromise = this.$nymph
751
+ .getEntity({
752
+ class: EntityClass,
753
+ skipAc: this.$skipAc,
754
+ }, { type: '&', guid: this.$sleepingReference[1] })
755
+ .then((entity) => {
756
+ if (entity == null || entity.guid == null) {
757
+ return Promise.reject(new errors_1.InvalidStateError('The sleeping reference could not be retrieved.'));
758
+ }
759
+ this.$isASleepingReference = false;
760
+ this.$sleepingReference = null;
761
+ this.guid = entity.guid;
762
+ this.tags = entity.tags;
763
+ this.cdate = entity.cdate;
764
+ this.mdate = entity.mdate;
765
+ this.$putData(entity.$getData(), entity.$getSData());
766
+ return this;
767
+ })
768
+ .finally(() => {
769
+ this.$wakePromise = null;
770
+ });
500
771
  }
501
- this.$isASleepingReference = false;
502
- this.$sleepingReference = null;
503
- this.guid = entity.guid;
504
- this.tags = entity.tags;
505
- this.cdate = entity.cdate;
506
- this.mdate = entity.mdate;
507
- this.$putData(entity.$getData(), entity.$getSData());
508
- return true;
772
+ return this.$wakePromise;
773
+ }
774
+ $wakeAll(level) {
775
+ return new Promise((resolve, reject) => {
776
+ // Run this once this entity is wake.
777
+ const wakeProps = () => {
778
+ let newLevel;
779
+ // If level is undefined, keep going forever, otherwise, stop once we've
780
+ // gone deep enough.
781
+ if (level !== undefined) {
782
+ newLevel = level - 1;
783
+ }
784
+ if (newLevel !== undefined && newLevel < 0) {
785
+ resolve(this);
786
+ return;
787
+ }
788
+ const promises = [];
789
+ // Go through data looking for entities to wake.
790
+ for (let [key, value] of Object.entries(this.$data)) {
791
+ if (value instanceof Entity && value.$isASleepingReference) {
792
+ promises.push(value.$wakeAll(newLevel));
793
+ }
794
+ else if (Array.isArray(value)) {
795
+ for (let i = 0; i < value.length; i++) {
796
+ if (value[i] instanceof Entity &&
797
+ value[i].$isASleepingReference) {
798
+ promises.push(value[i].$wakeAll(newLevel));
799
+ }
800
+ }
801
+ }
802
+ }
803
+ if (promises.length) {
804
+ Promise.all(promises).then(() => resolve(this), (errObj) => reject(errObj));
805
+ }
806
+ else {
807
+ resolve(this);
808
+ }
809
+ };
810
+ if (this.$isASleepingReference) {
811
+ this.$wake().then(wakeProps, (errObj) => reject(errObj));
812
+ }
813
+ else {
814
+ wakeProps();
815
+ }
816
+ });
509
817
  }
510
818
  async $refresh() {
511
- this.$referenceWake();
819
+ await this.$wake();
512
820
  if (this.guid == null) {
513
821
  return false;
514
822
  }
515
823
  const refresh = await this.$nymph.getEntity({
516
- class: this.constructor,
824
+ class: this.$nymph.getEntityClass(this.constructor),
517
825
  skipCache: true,
518
826
  skipAc: this.$skipAc,
519
827
  }, { type: '&', guid: this.guid });
@@ -527,11 +835,11 @@ class Entity {
527
835
  return true;
528
836
  }
529
837
  $removeTag(...tags) {
530
- this.$referenceWake();
838
+ this.$check();
531
839
  this.tags = (0, lodash_1.difference)(this.tags, tags);
532
840
  }
533
841
  async $save() {
534
- this.$referenceWake();
842
+ await this.$wake();
535
843
  return await this.$nymph.saveEntity(this);
536
844
  }
537
845
  $toReference() {
@@ -551,10 +859,44 @@ class Entity {
551
859
  this.$skipAc = !!skipAc;
552
860
  }
553
861
  }
554
- exports.default = Entity;
862
+ /**
863
+ * The instance of Nymph to use for queries.
864
+ */
555
865
  Entity.nymph = {};
866
+ /**
867
+ * A unique name for this type of entity used to separate its data from other
868
+ * types of entities in the database.
869
+ */
556
870
  Entity.ETYPE = 'entity';
871
+ /**
872
+ * The lookup name for this entity.
873
+ *
874
+ * This is used for reference arrays (and sleeping references) and client
875
+ * requests.
876
+ */
557
877
  Entity.class = 'Entity';
878
+ /**
879
+ * Whether this entity should publish changes to PubSub servers.
880
+ */
881
+ Entity.pubSubEnabled = true;
882
+ /**
883
+ * Whether this entity should be accessible on the frontend through the REST
884
+ * server.
885
+ *
886
+ * If this is false, any request from the client that attempts to use this
887
+ * entity will fail.
888
+ */
889
+ Entity.restEnabled = true;
890
+ /**
891
+ * Properties that will not be searchable from the frontend. If the frontend
892
+ * includes any of these properties in any of their clauses, they will be
893
+ * filtered out before the search is executed.
894
+ */
558
895
  Entity.searchRestrictedData = [];
896
+ /**
897
+ * The names of static methods allowed to be called by the frontend with
898
+ * serverCallStatic.
899
+ */
559
900
  Entity.clientEnabledStaticMethods = [];
901
+ exports.default = Entity;
560
902
  //# sourceMappingURL=Entity.js.map