@nymphjs/nymph 1.0.0-beta.8 → 1.0.0-beta.81

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 (65) hide show
  1. package/CHANGELOG.md +382 -0
  2. package/README.md +9 -19
  3. package/dist/Entity.d.ts +269 -7
  4. package/dist/Entity.js +491 -105
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Entity.test.js +25 -3
  7. package/dist/Entity.test.js.map +1 -1
  8. package/dist/Entity.types.d.ts +225 -19
  9. package/dist/Nymph.d.ts +430 -11
  10. package/dist/Nymph.js +686 -168
  11. package/dist/Nymph.js.map +1 -1
  12. package/dist/Nymph.types.d.ts +65 -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 +30 -0
  16. package/dist/conf/defaults.js +27 -0
  17. package/dist/conf/defaults.js.map +1 -1
  18. package/dist/driver/NymphDriver.d.ts +136 -38
  19. package/dist/driver/NymphDriver.js +264 -131
  20. package/dist/driver/NymphDriver.js.map +1 -1
  21. package/dist/driver/testArtifacts.d.ts +1 -1
  22. package/dist/driver/testArtifacts.js +499 -51
  23. package/dist/driver/testArtifacts.js.map +1 -1
  24. package/dist/errors/EntityInvalidDataError.d.ts +6 -0
  25. package/dist/errors/EntityInvalidDataError.js +7 -1
  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 +1 -0
  34. package/dist/errors/QueryFailedError.js.map +1 -1
  35. package/dist/errors/index.d.ts +2 -0
  36. package/dist/errors/index.js +2 -0
  37. package/dist/errors/index.js.map +1 -1
  38. package/dist/testArtifacts.d.ts +17 -5
  39. package/dist/testArtifacts.js +31 -27
  40. package/dist/testArtifacts.js.map +1 -1
  41. package/dist/testMocks.d.ts +13 -10
  42. package/dist/testMocks.js +32 -13
  43. package/dist/testMocks.js.map +1 -1
  44. package/dist/utils.d.ts +1 -1
  45. package/dist/utils.js +21 -5
  46. package/dist/utils.js.map +1 -1
  47. package/package.json +16 -14
  48. package/src/Entity.test.ts +22 -5
  49. package/src/Entity.ts +423 -134
  50. package/src/Entity.types.ts +32 -66
  51. package/src/Nymph.ts +609 -176
  52. package/src/Nymph.types.ts +131 -26
  53. package/src/conf/d.ts +12 -0
  54. package/src/conf/defaults.ts +31 -0
  55. package/src/driver/NymphDriver.ts +289 -311
  56. package/src/driver/testArtifacts.ts +572 -181
  57. package/src/errors/EntityUniqueConstraintError.ts +6 -0
  58. package/src/errors/InvalidStateError.ts +6 -0
  59. package/src/errors/index.ts +4 -0
  60. package/src/nymphJoiProps.ts +2 -2
  61. package/src/testArtifacts.ts +18 -19
  62. package/src/testMocks.ts +55 -30
  63. package/src/utils.ts +22 -8
  64. package/tsconfig.json +3 -3
  65. package/typedoc.json +4 -0
package/dist/Entity.js CHANGED
@@ -3,28 +3,221 @@ 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) {
8
- this.guid = null;
9
- this.cdate = null;
10
- this.mdate = null;
11
- this.tags = [];
12
- this.$isASleepingReference = false;
13
- this.$sleepingReference = null;
14
- this.$privateData = [];
15
- this.$protectedData = [];
16
- this.$protectedTags = [];
17
- this.$clientEnabledMethods = [];
18
- this.$skipAc = false;
19
- this.$originalAcValues = null;
76
+ /**
77
+ * The instance of Nymph to use for queries.
78
+ */
79
+ static nymph = {};
80
+ /**
81
+ * A unique name for this type of entity used to separate its data from other
82
+ * types of entities in the database.
83
+ */
84
+ static ETYPE = 'entity';
85
+ /**
86
+ * The lookup name for this entity.
87
+ *
88
+ * This is used for reference arrays (and sleeping references) and client
89
+ * requests.
90
+ */
91
+ static class = 'Entity';
92
+ $nymph;
93
+ guid = null;
94
+ cdate = null;
95
+ mdate = null;
96
+ tags = [];
97
+ /**
98
+ * The data proxy handler.
99
+ */
100
+ $dataHandler;
101
+ /**
102
+ * The actual data store.
103
+ */
104
+ $dataStore;
105
+ /**
106
+ * The actual sdata store.
107
+ */
108
+ $sdata;
109
+ /**
110
+ * The data proxy object.
111
+ */
112
+ $data;
113
+ /**
114
+ * Whether this instance is a sleeping reference.
115
+ */
116
+ $isASleepingReference = false;
117
+ /**
118
+ * The reference to use to wake.
119
+ */
120
+ $sleepingReference = null;
121
+ /**
122
+ * A promise that resolved when the entity's data is wake.
123
+ */
124
+ $wakePromise = null;
125
+ /**
126
+ * Properties that will not be serialized into JSON with toJSON(). This
127
+ * can be considered a denylist, because these properties will not be set
128
+ * with incoming JSON.
129
+ *
130
+ * Clients CAN still determine what is in these properties, unless they are
131
+ * also listed in searchRestrictedData.
132
+ */
133
+ $privateData = [];
134
+ /**
135
+ * Whether this entity should publish changes to PubSub servers.
136
+ */
137
+ static pubSubEnabled = true;
138
+ /**
139
+ * Whether this entity should be accessible on the frontend through the REST
140
+ * server.
141
+ *
142
+ * If this is false, any request from the client that attempts to use this
143
+ * entity will fail.
144
+ */
145
+ static restEnabled = true;
146
+ /**
147
+ * Properties that will not be searchable from the frontend. If the frontend
148
+ * includes any of these properties in any of their clauses, they will be
149
+ * filtered out before the search is executed.
150
+ */
151
+ static searchRestrictedData = [];
152
+ /**
153
+ * Properties that can only be modified by server side code. They will still
154
+ * be visible on the frontend, unlike $privateData, but any changes to them
155
+ * that come from the frontend will be ignored.
156
+ *
157
+ * In addition to what's listed here, all of the access control properties
158
+ * will be included when Tilmeld is being used. These are:
159
+ *
160
+ * - acUser
161
+ * - acGroup
162
+ * - acOther
163
+ * - acRead
164
+ * - acWrite
165
+ * - acFull
166
+ * - user
167
+ * - group
168
+ *
169
+ * You should modify these through client enabled methods or the $save method
170
+ * instead, for safety.
171
+ */
172
+ $protectedData = [];
173
+ /**
174
+ * If this is defined, then it lists the only properties that will be
175
+ * accepted from incoming JSON. Any other properties will be ignored.
176
+ *
177
+ * If you use an allowlist, you don't need to use protectedData, since you
178
+ * can simply leave those entries out of allowlistData.
179
+ */
180
+ $allowlistData;
181
+ /**
182
+ * Tags that can only be added/removed by server side code. They will still be
183
+ * visible on the frontend, but any changes to them that come from the
184
+ * frontend will be ignored.
185
+ */
186
+ $protectedTags = [];
187
+ /**
188
+ * If this is defined, then it lists the only tags that will be accepted from
189
+ * incoming JSON. Any other tags will be ignored.
190
+ */
191
+ $allowlistTags;
192
+ /**
193
+ * The names of methods allowed to be called by the frontend with serverCall.
194
+ */
195
+ $clientEnabledMethods = [];
196
+ /**
197
+ * The names of static methods allowed to be called by the frontend with
198
+ * serverCallStatic.
199
+ */
200
+ static clientEnabledStaticMethods = [];
201
+ /**
202
+ * Whether to use "skipAc" when accessing entity references.
203
+ */
204
+ $skipAc = false;
205
+ /**
206
+ * The AC properties' values when the entity was loaded.
207
+ */
208
+ $originalAcValues = null;
209
+ /**
210
+ * Initialize an entity.
211
+ */
212
+ constructor(..._rest) {
20
213
  this.$nymph = this.constructor.nymph;
21
214
  this.$dataHandler = {
22
215
  has: (data, name) => {
23
- this.$referenceWake();
216
+ this.$check();
24
217
  return name in data || name in this.$sdata;
25
218
  },
26
219
  get: (data, name) => {
27
- this.$referenceWake();
220
+ this.$check();
28
221
  if (this.$sdata.hasOwnProperty(name)) {
29
222
  data[name] = (0, utils_1.referencesToEntities)(JSON.parse(this.$sdata[name]), this.$nymph, this.$skipAc);
30
223
  delete this.$sdata[name];
@@ -35,7 +228,7 @@ class Entity {
35
228
  return undefined;
36
229
  },
37
230
  set: (data, name, value) => {
38
- this.$referenceWake();
231
+ this.$check();
39
232
  if (this.$sdata.hasOwnProperty(name)) {
40
233
  delete this.$sdata[name];
41
234
  }
@@ -43,7 +236,7 @@ class Entity {
43
236
  return true;
44
237
  },
45
238
  deleteProperty: (data, name) => {
46
- this.$referenceWake();
239
+ this.$check();
47
240
  if (this.$sdata.hasOwnProperty(name)) {
48
241
  return delete this.$sdata[name];
49
242
  }
@@ -53,7 +246,7 @@ class Entity {
53
246
  return true;
54
247
  },
55
248
  defineProperty: (data, name, descriptor) => {
56
- this.$referenceWake();
249
+ this.$check();
57
250
  if (this.$sdata.hasOwnProperty(name)) {
58
251
  delete this.$sdata[name];
59
252
  }
@@ -61,7 +254,7 @@ class Entity {
61
254
  return true;
62
255
  },
63
256
  getOwnPropertyDescriptor: (data, name) => {
64
- this.$referenceWake();
257
+ this.$check();
65
258
  if (this.$sdata.hasOwnProperty(name)) {
66
259
  data[name] = (0, utils_1.referencesToEntities)(JSON.parse(this.$sdata[name]), this.$nymph, this.$skipAc);
67
260
  delete this.$sdata[name];
@@ -69,23 +262,13 @@ class Entity {
69
262
  return Object.getOwnPropertyDescriptor(data, name);
70
263
  },
71
264
  ownKeys: (data) => {
72
- this.$referenceWake();
265
+ this.$check();
73
266
  return Object.getOwnPropertyNames(data).concat(Object.getOwnPropertyNames(this.$sdata));
74
267
  },
75
268
  };
76
269
  this.$dataStore = {};
77
270
  this.$sdata = {};
78
271
  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
272
  return new Proxy(this, {
90
273
  has: (entity, name) => {
91
274
  if (typeof name !== 'string' ||
@@ -93,7 +276,7 @@ class Entity {
93
276
  name.substring(0, 1) === '$') {
94
277
  return name in entity;
95
278
  }
96
- this.$referenceWake();
279
+ this.$check();
97
280
  return name in entity.$data;
98
281
  },
99
282
  get: (entity, name) => {
@@ -101,11 +284,11 @@ class Entity {
101
284
  name in entity ||
102
285
  name.substring(0, 1) === '$') {
103
286
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
104
- this.$referenceWake();
287
+ this.$check();
105
288
  }
106
289
  return entity[name];
107
290
  }
108
- this.$referenceWake();
291
+ this.$check();
109
292
  if (name in entity.$data) {
110
293
  return entity.$data[name];
111
294
  }
@@ -116,12 +299,12 @@ class Entity {
116
299
  name in entity ||
117
300
  name.substring(0, 1) === '$') {
118
301
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
119
- this.$referenceWake();
302
+ this.$check();
120
303
  }
121
304
  entity[name] = value;
122
305
  }
123
306
  else {
124
- this.$referenceWake();
307
+ this.$check();
125
308
  entity.$data[name] = value;
126
309
  }
127
310
  return true;
@@ -131,7 +314,7 @@ class Entity {
131
314
  return delete entity[name];
132
315
  }
133
316
  else if (name in entity.$data) {
134
- this.$referenceWake();
317
+ this.$check();
135
318
  return delete entity.$data[name];
136
319
  }
137
320
  return true;
@@ -144,12 +327,12 @@ class Entity {
144
327
  name in entity ||
145
328
  name.substring(0, 1) === '$') {
146
329
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
147
- this.$referenceWake();
330
+ this.$check();
148
331
  }
149
332
  Object.defineProperty(entity, name, descriptor);
150
333
  }
151
334
  else {
152
- this.$referenceWake();
335
+ this.$check();
153
336
  Object.defineProperty(entity.$data, name, descriptor);
154
337
  }
155
338
  return true;
@@ -159,21 +342,28 @@ class Entity {
159
342
  name in entity ||
160
343
  name.substring(0, 1) === '$') {
161
344
  if (name === 'tags' || name === 'cdate' || name === 'mdate') {
162
- this.$referenceWake();
345
+ this.$check();
163
346
  }
164
347
  return Object.getOwnPropertyDescriptor(entity, name);
165
348
  }
166
349
  else {
167
- this.$referenceWake();
350
+ this.$check();
168
351
  return Object.getOwnPropertyDescriptor(entity.$data, name);
169
352
  }
170
353
  },
171
354
  ownKeys: (entity) => {
172
- this.$referenceWake();
355
+ this.$check();
173
356
  return Object.getOwnPropertyNames(entity).concat(Object.getOwnPropertyNames(entity.$data));
174
357
  },
175
358
  });
176
359
  }
360
+ /**
361
+ * Create or retrieve a new entity instance.
362
+ *
363
+ * Note that this will always return an entity, even if the GUID is not found.
364
+ *
365
+ * @param guid An optional GUID to retrieve.
366
+ */
177
367
  static async factory(guid) {
178
368
  const entity = new this();
179
369
  if (guid != null) {
@@ -186,14 +376,56 @@ class Entity {
186
376
  }
187
377
  return entity;
188
378
  }
189
- static factorySync(guid) {
190
- return new this(guid);
379
+ /**
380
+ * Create a new entity instance.
381
+ */
382
+ static factorySync() {
383
+ return new this();
191
384
  }
385
+ /**
386
+ * Create a new sleeping reference instance.
387
+ *
388
+ * Sleeping references won't retrieve their data from the database until they
389
+ * are readied with `$wake()` or a parent's `$wakeAll()`.
390
+ *
391
+ * @param reference The Nymph Entity Reference to use to wake.
392
+ * @returns The new instance.
393
+ */
192
394
  static factoryReference(reference) {
193
395
  const entity = new this();
194
396
  entity.$referenceSleep(reference);
195
397
  return entity;
196
398
  }
399
+ /**
400
+ * Get an array of strings that **must** be unique across the current etype.
401
+ *
402
+ * When you try to save another entity with any of the same unique strings,
403
+ * Nymph will throw an error.
404
+ *
405
+ * The default implementation of this static method instantiates the entity,
406
+ * assigns all of the given data, then calls `$getUniques` and returns its
407
+ * output. This can have a performance impact if a lot of extra processing
408
+ * happens during any of these steps. You can override this method to
409
+ * calculate the unique strings faster, but you must return the same strings
410
+ * that would be returned by `$getUniques`.
411
+ *
412
+ * @returns Resolves to an array of entity's unique constraint strings.
413
+ */
414
+ static async getUniques({ guid, cdate, mdate, tags, data, sdata, }) {
415
+ const entity = new this();
416
+ if (guid != null) {
417
+ entity.guid = guid;
418
+ }
419
+ if (cdate != null) {
420
+ entity.cdate = cdate;
421
+ }
422
+ if (mdate != null) {
423
+ entity.mdate = mdate;
424
+ }
425
+ entity.tags = tags;
426
+ entity.$putData(data, sdata);
427
+ return await entity.$getUniques();
428
+ }
197
429
  toJSON() {
198
430
  if (this.$isASleepingReference) {
199
431
  return this.$sleepingReference;
@@ -215,14 +447,14 @@ class Entity {
215
447
  return obj;
216
448
  }
217
449
  $addTag(...tags) {
218
- this.$referenceWake();
450
+ this.$check();
219
451
  if (tags.length < 1) {
220
452
  return;
221
453
  }
222
454
  this.tags = (0, utils_1.uniqueStrings)([...this.tags, ...tags]);
223
455
  }
224
456
  $arraySearch(array, strict = false) {
225
- this.$referenceWake();
457
+ this.$check();
226
458
  if (!Array.isArray(array)) {
227
459
  return -1;
228
460
  }
@@ -235,18 +467,18 @@ class Entity {
235
467
  return -1;
236
468
  }
237
469
  $clearCache() {
238
- this.$referenceWake();
470
+ this.$check();
239
471
  this.$putData(this.$getData(), this.$getSData());
240
472
  }
241
473
  $getClientEnabledMethods() {
242
474
  return this.$clientEnabledMethods;
243
475
  }
244
476
  async $delete() {
245
- this.$referenceWake();
477
+ this.$check();
246
478
  return await this.$nymph.deleteEntity(this);
247
479
  }
248
480
  $equals(object) {
249
- this.$referenceWake();
481
+ this.$check();
250
482
  if (!(object instanceof Entity)) {
251
483
  return false;
252
484
  }
@@ -255,22 +487,25 @@ class Entity {
255
487
  return false;
256
488
  }
257
489
  }
258
- if (object.constructor !== this.constructor) {
259
- return false;
260
- }
261
490
  if (object.cdate !== this.cdate) {
262
491
  return false;
263
492
  }
264
493
  if (object.mdate !== this.mdate) {
265
494
  return false;
266
495
  }
496
+ const obTags = [...object.tags].sort();
497
+ const myTags = [...this.tags].sort();
498
+ if (!(0, lodash_1.isEqual)(obTags, myTags)) {
499
+ return false;
500
+ }
267
501
  const obData = object.$getData(true);
268
502
  const myData = this.$getData(true);
269
503
  return (0, lodash_1.isEqual)(obData, myData);
270
504
  }
271
505
  $getData(includeSData = false) {
272
- this.$referenceWake();
506
+ this.$check();
273
507
  if (includeSData) {
508
+ // Access all the serialized properties to initialize them.
274
509
  for (const key in this.$sdata) {
275
510
  const _unused = this[key];
276
511
  }
@@ -278,24 +513,64 @@ class Entity {
278
513
  return (0, utils_1.entitiesToReferences)({ ...this.$dataStore });
279
514
  }
280
515
  $getSData() {
281
- this.$referenceWake();
516
+ this.$check();
282
517
  return this.$sdata;
283
518
  }
519
+ async $getUniques() {
520
+ return [];
521
+ }
284
522
  $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
- });
523
+ this.$check();
524
+ return this.$originalAcValues ?? this.$getCurrentAcValues();
525
+ }
526
+ $getCurrentAcValues() {
527
+ this.$check();
528
+ return {
529
+ user: this.$getAcUid(),
530
+ group: this.$getAcGid(),
531
+ acUser: this.$data.acUser ?? null,
532
+ acGroup: this.$data.acGroup ?? null,
533
+ acOther: this.$data.acOther ?? null,
534
+ acRead: this.$getAcReadIds(),
535
+ acWrite: this.$getAcWriteIds(),
536
+ acFull: this.$getAcFullIds(),
537
+ };
538
+ }
539
+ $getAcUid() {
540
+ if ('user' in this.$sdata) {
541
+ const userValue = JSON.parse(this.$sdata.user);
542
+ if (Array.isArray(userValue) &&
543
+ userValue[0] === 'nymph_entity_reference') {
544
+ return userValue[1];
545
+ }
546
+ }
547
+ return (this.$data.user?.guid ?? null);
548
+ }
549
+ $getAcGid() {
550
+ if ('group' in this.$sdata) {
551
+ const groupValue = JSON.parse(this.$sdata.group);
552
+ if (Array.isArray(groupValue) &&
553
+ groupValue[0] === 'nymph_entity_reference') {
554
+ return groupValue[1];
555
+ }
556
+ }
557
+ return (this.$data.group?.guid ?? null);
558
+ }
559
+ $getAcReadIds() {
560
+ return (this.$data.acRead?.map((entity) => entity.guid) ??
561
+ null);
562
+ }
563
+ $getAcWriteIds() {
564
+ return (this.$data.acWrite?.map((entity) => entity.guid) ??
565
+ null);
566
+ }
567
+ $getAcFullIds() {
568
+ return (this.$data.acFull?.map((entity) => entity.guid) ??
569
+ null);
296
570
  }
297
571
  $getValidatable() {
298
- this.$referenceWake();
572
+ this.$check();
573
+ // Access all the serialized properties to initialize them.
299
574
  for (const key in this.$sdata) {
300
575
  const _unused = this[key];
301
576
  }
@@ -308,11 +583,11 @@ class Entity {
308
583
  };
309
584
  }
310
585
  $getTags() {
311
- this.$referenceWake();
586
+ this.$check();
312
587
  return this.tags;
313
588
  }
314
589
  $hasTag(...tags) {
315
- this.$referenceWake();
590
+ this.$check();
316
591
  if (!tags.length) {
317
592
  return false;
318
593
  }
@@ -333,27 +608,34 @@ class Entity {
333
608
  if (this.guid || object.guid) {
334
609
  return this.guid === object.guid;
335
610
  }
336
- this.$referenceWake();
611
+ this.$check();
337
612
  if (typeof object.$getData !== 'function') {
338
613
  return false;
339
614
  }
340
615
  else {
616
+ const obTags = [...object.tags].sort();
617
+ const myTags = [...this.tags].sort();
618
+ if (!(0, lodash_1.isEqual)(obTags, myTags)) {
619
+ return false;
620
+ }
341
621
  const obData = object.$getData(true);
342
622
  const myData = this.$getData(true);
343
623
  return (0, lodash_1.isEqual)(obData, myData);
344
624
  }
345
625
  }
346
626
  $jsonAcceptData(input, allowConflict = false) {
347
- this.$referenceWake();
627
+ this.$check();
348
628
  if (this.guid != input.guid) {
349
629
  throw new errors_1.EntityConflictError('Tried to accept JSON input for the wrong entity.');
350
630
  }
631
+ // Accept the modified date.
351
632
  const mdate = input.mdate ?? 0;
352
633
  const thismdate = this.mdate ?? 0;
353
634
  if (mdate < thismdate && !allowConflict) {
354
635
  throw new errors_1.EntityConflictError('This entity is newer than JSON input.');
355
636
  }
356
637
  this.mdate = input.mdate;
638
+ // Accept the tags.
357
639
  const currentTags = this.$getTags();
358
640
  const protectedTags = (0, lodash_1.intersection)(this.$protectedTags, currentTags);
359
641
  let tags = (0, lodash_1.difference)(input.tags, this.$protectedTags);
@@ -362,6 +644,7 @@ class Entity {
362
644
  }
363
645
  this.$removeTag(...currentTags);
364
646
  this.$addTag(...protectedTags, ...tags);
647
+ // Accept the data.
365
648
  const data = input.data;
366
649
  const privateData = {};
367
650
  for (const name of this.$privateData) {
@@ -374,10 +657,21 @@ class Entity {
374
657
  }
375
658
  const protectedData = {};
376
659
  const protectedProps = [...this.$protectedData];
377
- if (this.$nymph.tilmeld &&
378
- !this.$nymph.tilmeld.gatekeeper('tilmeld/admin')) {
379
- protectedProps.push('user');
380
- protectedProps.push('group');
660
+ if (this.$nymph.tilmeld) {
661
+ protectedProps.push('acUser');
662
+ protectedProps.push('acGroup');
663
+ protectedProps.push('acOther');
664
+ protectedProps.push('acRead');
665
+ protectedProps.push('acWrite');
666
+ protectedProps.push('acFull');
667
+ if ((this.constructor.class !== 'User' &&
668
+ this.constructor.class !== 'Group') ||
669
+ !this.$nymph.tilmeld.currentUser ||
670
+ (!this.$nymph.tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
671
+ !this.$nymph.tilmeld.currentUser.abilities?.includes('system/admin'))) {
672
+ protectedProps.push('user');
673
+ protectedProps.push('group');
674
+ }
381
675
  }
382
676
  for (const name of protectedProps) {
383
677
  if (name in this.$data) {
@@ -407,20 +701,38 @@ class Entity {
407
701
  });
408
702
  }
409
703
  $jsonAcceptPatch(patch, allowConflict = false) {
410
- this.$referenceWake();
704
+ this.$check();
411
705
  if (this.guid != patch.guid) {
412
706
  throw new errors_1.EntityConflictError('Tried to accept JSON patch for the wrong entity.');
413
707
  }
708
+ // Accept the modified date.
414
709
  const mdate = patch.mdate ?? 0;
415
710
  const thismdate = this.mdate ?? 0;
416
711
  if (mdate < thismdate && !allowConflict) {
417
712
  throw new errors_1.EntityConflictError('This entity is newer than JSON patch.');
418
713
  }
419
714
  this.mdate = patch.mdate;
715
+ const protectedProps = [...this.$protectedData];
716
+ if (this.$nymph.tilmeld) {
717
+ protectedProps.push('acUser');
718
+ protectedProps.push('acGroup');
719
+ protectedProps.push('acOther');
720
+ protectedProps.push('acRead');
721
+ protectedProps.push('acWrite');
722
+ protectedProps.push('acFull');
723
+ if ((this.constructor.class !== 'User' &&
724
+ this.constructor.class !== 'Group') ||
725
+ !this.$nymph.tilmeld.currentUser ||
726
+ (!this.$nymph.tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
727
+ !this.$nymph.tilmeld.currentUser.abilities?.includes('system/admin'))) {
728
+ protectedProps.push('user');
729
+ protectedProps.push('group');
730
+ }
731
+ }
420
732
  for (const name in patch.set) {
421
733
  if ((this.$allowlistData != null &&
422
734
  this.$allowlistData.indexOf(name) === -1) ||
423
- this.$protectedData.indexOf(name) !== -1 ||
735
+ protectedProps.indexOf(name) !== -1 ||
424
736
  this.$privateData.indexOf(name) !== -1) {
425
737
  continue;
426
738
  }
@@ -429,7 +741,7 @@ class Entity {
429
741
  for (const name of patch.unset) {
430
742
  if ((this.$allowlistData != null &&
431
743
  this.$allowlistData.indexOf(name) === -1) ||
432
- this.$protectedData.indexOf(name) !== -1 ||
744
+ protectedProps.indexOf(name) !== -1 ||
433
745
  this.$privateData.indexOf(name) !== -1) {
434
746
  continue;
435
747
  }
@@ -453,7 +765,7 @@ class Entity {
453
765
  }
454
766
  }
455
767
  $putData(data, sdata) {
456
- this.$referenceWake();
768
+ this.$check();
457
769
  const mySdata = sdata ?? this.$getSData();
458
770
  for (const name in data) {
459
771
  delete mySdata[name];
@@ -465,10 +777,15 @@ class Entity {
465
777
  this.$dataStore[name] = (0, utils_1.referencesToEntities)(data[name], this.$nymph, this.$skipAc);
466
778
  }
467
779
  this.$sdata = mySdata;
780
+ // Set original AC values if not set..
468
781
  if (this.$originalAcValues == null) {
469
- this.$originalAcValues = this.$getOriginalAcValues();
782
+ this.$originalAcValues = this.$getCurrentAcValues();
470
783
  }
471
784
  }
785
+ /**
786
+ * Set up a sleeping reference.
787
+ * @param array $reference The reference to use to wake.
788
+ */
472
789
  $referenceSleep(reference) {
473
790
  if (reference.length !== 3 ||
474
791
  reference[0] !== 'nymph_entity_reference' ||
@@ -486,34 +803,108 @@ class Entity {
486
803
  this.guid = reference[1];
487
804
  this.$sleepingReference = reference;
488
805
  }
489
- $referenceWake() {
490
- if (!this.$isASleepingReference || this.$sleepingReference == null) {
491
- return true;
806
+ /**
807
+ * Check if this is a sleeping reference and throw an error if so.
808
+ */
809
+ $check() {
810
+ if (this.$isASleepingReference || this.$sleepingReference != null) {
811
+ throw new errors_1.EntityIsSleepingReferenceError('This entity is in a sleeping reference state. You must use .$wake() to wake it.');
492
812
  }
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;
813
+ }
814
+ /**
815
+ * Check if this is a sleeping reference.
816
+ */
817
+ $asleep() {
818
+ return this.$isASleepingReference || this.$sleepingReference != null;
819
+ }
820
+ /**
821
+ * Wake from a sleeping reference.
822
+ */
823
+ $wake() {
824
+ if (!this.$isASleepingReference || this.$sleepingReference == null) {
825
+ this.$wakePromise = null;
826
+ return Promise.resolve(this);
827
+ }
828
+ if (this.$sleepingReference?.[1] == null) {
829
+ throw new errors_1.InvalidStateError('Tried to wake a sleeping reference with no GUID.');
830
+ }
831
+ if (!this.$wakePromise) {
832
+ const EntityClass = this.$nymph.getEntityClass(this.$sleepingReference[2]);
833
+ this.$wakePromise = this.$nymph
834
+ .getEntity({
835
+ class: EntityClass,
836
+ skipAc: this.$skipAc,
837
+ }, { type: '&', guid: this.$sleepingReference[1] })
838
+ .then((entity) => {
839
+ if (entity == null || entity.guid == null) {
840
+ return Promise.reject(new errors_1.InvalidStateError('The sleeping reference could not be retrieved.'));
841
+ }
842
+ this.$isASleepingReference = false;
843
+ this.$sleepingReference = null;
844
+ this.guid = entity.guid;
845
+ this.tags = entity.tags;
846
+ this.cdate = entity.cdate;
847
+ this.mdate = entity.mdate;
848
+ this.$putData(entity.$getData(), entity.$getSData());
849
+ return this;
850
+ })
851
+ .finally(() => {
852
+ this.$wakePromise = null;
853
+ });
500
854
  }
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;
855
+ return this.$wakePromise;
856
+ }
857
+ $wakeAll(level) {
858
+ return new Promise((resolve, reject) => {
859
+ // Run this once this entity is wake.
860
+ const wakeProps = () => {
861
+ let newLevel;
862
+ // If level is undefined, keep going forever, otherwise, stop once we've
863
+ // gone deep enough.
864
+ if (level !== undefined) {
865
+ newLevel = level - 1;
866
+ }
867
+ if (newLevel !== undefined && newLevel < 0) {
868
+ resolve(this);
869
+ return;
870
+ }
871
+ const promises = [];
872
+ // Go through data looking for entities to wake.
873
+ for (let [key, value] of Object.entries(this.$data)) {
874
+ if (value instanceof Entity && value.$isASleepingReference) {
875
+ promises.push(value.$wakeAll(newLevel));
876
+ }
877
+ else if (Array.isArray(value)) {
878
+ for (let i = 0; i < value.length; i++) {
879
+ if (value[i] instanceof Entity &&
880
+ value[i].$isASleepingReference) {
881
+ promises.push(value[i].$wakeAll(newLevel));
882
+ }
883
+ }
884
+ }
885
+ }
886
+ if (promises.length) {
887
+ Promise.all(promises).then(() => resolve(this), (errObj) => reject(errObj));
888
+ }
889
+ else {
890
+ resolve(this);
891
+ }
892
+ };
893
+ if (this.$isASleepingReference) {
894
+ this.$wake().then(wakeProps, (errObj) => reject(errObj));
895
+ }
896
+ else {
897
+ wakeProps();
898
+ }
899
+ });
509
900
  }
510
901
  async $refresh() {
511
- this.$referenceWake();
902
+ await this.$wake();
512
903
  if (this.guid == null) {
513
904
  return false;
514
905
  }
515
906
  const refresh = await this.$nymph.getEntity({
516
- class: this.constructor,
907
+ class: this.$nymph.getEntityClass(this.constructor),
517
908
  skipCache: true,
518
909
  skipAc: this.$skipAc,
519
910
  }, { type: '&', guid: this.guid });
@@ -527,11 +918,11 @@ class Entity {
527
918
  return true;
528
919
  }
529
920
  $removeTag(...tags) {
530
- this.$referenceWake();
921
+ this.$check();
531
922
  this.tags = (0, lodash_1.difference)(this.tags, tags);
532
923
  }
533
924
  async $save() {
534
- this.$referenceWake();
925
+ await this.$wake();
535
926
  return await this.$nymph.saveEntity(this);
536
927
  }
537
928
  $toReference() {
@@ -552,9 +943,4 @@ class Entity {
552
943
  }
553
944
  }
554
945
  exports.default = Entity;
555
- Entity.nymph = {};
556
- Entity.ETYPE = 'entity';
557
- Entity.class = 'Entity';
558
- Entity.searchRestrictedData = [];
559
- Entity.clientEnabledStaticMethods = [];
560
946
  //# sourceMappingURL=Entity.js.map