@nymphjs/nymph 1.0.0-beta.11 → 1.0.0-beta.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +540 -0
  2. package/README.md +23 -21
  3. package/dist/Entity.d.ts +293 -10
  4. package/dist/Entity.js +560 -148
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Entity.test.js +52 -13
  7. package/dist/Entity.test.js.map +1 -1
  8. package/dist/Entity.types.d.ts +256 -23
  9. package/dist/Entity.types.js +1 -2
  10. package/dist/Nymph.d.ts +586 -12
  11. package/dist/Nymph.js +844 -179
  12. package/dist/Nymph.js.map +1 -1
  13. package/dist/Nymph.types.d.ts +81 -8
  14. package/dist/Nymph.types.js +3 -5
  15. package/dist/Nymph.types.js.map +1 -1
  16. package/dist/conf/d.d.ts +43 -0
  17. package/dist/conf/d.js +1 -2
  18. package/dist/conf/defaults.d.ts +1 -1
  19. package/dist/conf/defaults.js +26 -3
  20. package/dist/conf/defaults.js.map +1 -1
  21. package/dist/conf/index.d.ts +2 -2
  22. package/dist/conf/index.js +2 -8
  23. package/dist/conf/index.js.map +1 -1
  24. package/dist/driver/NymphDriver.d.ts +201 -40
  25. package/dist/driver/NymphDriver.js +685 -239
  26. package/dist/driver/NymphDriver.js.map +1 -1
  27. package/dist/driver/index.d.ts +2 -2
  28. package/dist/driver/index.js +3 -23
  29. package/dist/driver/index.js.map +1 -1
  30. package/dist/driver/testArtifacts.d.ts +2 -2
  31. package/dist/driver/testArtifacts.js +909 -93
  32. package/dist/driver/testArtifacts.js.map +1 -1
  33. package/dist/errors/ClassNotAvailableError.js +1 -5
  34. package/dist/errors/ClassNotAvailableError.js.map +1 -1
  35. package/dist/errors/EntityConflictError.js +1 -5
  36. package/dist/errors/EntityConflictError.js.map +1 -1
  37. package/dist/errors/EntityInvalidDataError.d.ts +6 -0
  38. package/dist/errors/EntityInvalidDataError.js +8 -6
  39. package/dist/errors/EntityInvalidDataError.js.map +1 -1
  40. package/dist/errors/EntityIsSleepingReferenceError.js +1 -5
  41. package/dist/errors/EntityIsSleepingReferenceError.js.map +1 -1
  42. package/dist/errors/EntityUniqueConstraintError.d.ts +3 -0
  43. package/dist/errors/EntityUniqueConstraintError.js +7 -0
  44. package/dist/errors/EntityUniqueConstraintError.js.map +1 -0
  45. package/dist/errors/HttpError.d.ts +5 -0
  46. package/dist/errors/HttpError.js +11 -0
  47. package/dist/errors/HttpError.js.map +1 -0
  48. package/dist/errors/InvalidParametersError.js +1 -5
  49. package/dist/errors/InvalidParametersError.js.map +1 -1
  50. package/dist/errors/InvalidStateError.d.ts +3 -0
  51. package/dist/errors/InvalidStateError.js +7 -0
  52. package/dist/errors/InvalidStateError.js.map +1 -0
  53. package/dist/errors/NotConfiguredError.js +1 -5
  54. package/dist/errors/NotConfiguredError.js.map +1 -1
  55. package/dist/errors/QueryFailedError.d.ts +2 -1
  56. package/dist/errors/QueryFailedError.js +5 -6
  57. package/dist/errors/QueryFailedError.js.map +1 -1
  58. package/dist/errors/UnableToConnectError.js +1 -5
  59. package/dist/errors/UnableToConnectError.js.map +1 -1
  60. package/dist/errors/index.d.ts +11 -8
  61. package/dist/errors/index.js +11 -24
  62. package/dist/errors/index.js.map +1 -1
  63. package/dist/index.d.ts +11 -11
  64. package/dist/index.js +14 -34
  65. package/dist/index.js.map +1 -1
  66. package/dist/nymphJoiProps.d.ts +1 -1
  67. package/dist/nymphJoiProps.js +10 -15
  68. package/dist/nymphJoiProps.js.map +1 -1
  69. package/dist/testArtifacts.d.ts +19 -6
  70. package/dist/testArtifacts.js +32 -37
  71. package/dist/testArtifacts.js.map +1 -1
  72. package/dist/testMocks.d.ts +27 -12
  73. package/dist/testMocks.js +40 -29
  74. package/dist/testMocks.js.map +1 -1
  75. package/dist/utils.d.ts +4 -4
  76. package/dist/utils.js +40 -29
  77. package/dist/utils.js.map +1 -1
  78. package/jest.config.js +11 -2
  79. package/package.json +24 -20
  80. package/src/Entity.test.ts +50 -7
  81. package/src/Entity.ts +489 -150
  82. package/src/Entity.types.ts +72 -74
  83. package/src/Nymph.ts +985 -322
  84. package/src/Nymph.types.ts +150 -27
  85. package/src/conf/d.ts +25 -0
  86. package/src/conf/defaults.ts +34 -1
  87. package/src/conf/index.ts +2 -2
  88. package/src/driver/NymphDriver.ts +859 -412
  89. package/src/driver/index.ts +2 -2
  90. package/src/driver/testArtifacts.ts +1090 -186
  91. package/src/errors/EntityUniqueConstraintError.ts +6 -0
  92. package/src/errors/HttpError.ts +12 -0
  93. package/src/errors/InvalidStateError.ts +6 -0
  94. package/src/errors/QueryFailedError.ts +3 -1
  95. package/src/errors/index.ts +14 -8
  96. package/src/index.ts +11 -11
  97. package/src/nymphJoiProps.ts +2 -2
  98. package/src/testArtifacts.ts +20 -20
  99. package/src/testMocks.ts +76 -35
  100. package/src/type/locutus.d.ts +3 -0
  101. package/src/utils.ts +39 -18
  102. package/tsconfig.json +5 -3
  103. package/typedoc.json +4 -0
@@ -1,20 +1,24 @@
1
- import fs from 'fs';
2
- import { difference } from 'lodash';
1
+ import fs from 'node:fs';
2
+ import { difference } from 'lodash-es';
3
3
  import ReadLines from 'n-readlines';
4
- import strtotime from 'locutus/php/datetime/strtotime';
5
- import { guid } from '@nymphjs/guid';
4
+ import strtotime from 'locutus/php/datetime/strtotime.js';
5
+ import { Tokenizer } from '@sciactive/tokenizer';
6
6
 
7
- import type Nymph from '../Nymph';
8
- import {
7
+ import type Nymph from '../Nymph.js';
8
+ import type { Selector, Options, FormattedSelector } from '../Nymph.types.js';
9
+ import type { EntityInstanceType, EntityObjectType } from '../Entity.js';
10
+ import type {
9
11
  EntityConstructor,
10
12
  EntityData,
11
13
  EntityInterface,
12
14
  EntityReference,
13
15
  SerializedEntityData,
14
- } from '../Entity.types';
15
- import { InvalidParametersError, UnableToConnectError } from '../errors';
16
- import { Selector, Options, FormattedSelector } from '../Nymph.types';
17
- import { xor } from '../utils';
16
+ } from '../Entity.types.js';
17
+ import {
18
+ InvalidParametersError,
19
+ UnableToConnectError,
20
+ } from '../errors/index.js';
21
+ import { xor } from '../utils.js';
18
22
 
19
23
  // from: https://stackoverflow.com/a/6969486/664915
20
24
  function escapeRegExp(string: string): string {
@@ -28,11 +32,13 @@ export default abstract class NymphDriver {
28
32
  protected nymph: Nymph = new Proxy({} as Nymph, {
29
33
  get() {
30
34
  throw new Error(
31
- 'Attempted access of Nymph instance before driver initialization!'
35
+ 'Attempted access of Nymph instance before driver initialization!',
32
36
  );
33
37
  },
34
38
  });
35
39
 
40
+ protected tokenizer = new Tokenizer();
41
+
36
42
  /**
37
43
  * A cache to make entity retrieval faster.
38
44
  */
@@ -67,13 +73,14 @@ export default abstract class NymphDriver {
67
73
  abstract connect(): Promise<boolean>;
68
74
 
69
75
  abstract isConnected(): boolean;
76
+ protected abstract internalTransaction(name: string): Promise<any>;
70
77
  abstract startTransaction(name: string): Promise<Nymph>;
71
78
  abstract commit(name: string): Promise<boolean>;
72
79
  abstract rollback(name: string): Promise<boolean>;
73
80
  abstract inTransaction(): Promise<boolean>;
74
81
  abstract deleteEntityByID(
75
82
  guid: string,
76
- classConstructor: EntityConstructor
83
+ classConstructor: EntityConstructor,
77
84
  ): Promise<boolean>;
78
85
  abstract deleteEntityByID(guid: string, className?: string): Promise<boolean>;
79
86
  abstract deleteUID(name: string): Promise<boolean>;
@@ -83,9 +90,40 @@ export default abstract class NymphDriver {
83
90
  */
84
91
  abstract disconnect(): Promise<boolean>;
85
92
 
86
- protected abstract exportEntities(
87
- writeLine: (line: string) => void
93
+ abstract needsMigration(): Promise<
94
+ 'json' | 'tokens' | 'tilmeldColumns' | false
95
+ >;
96
+ abstract liveMigration(
97
+ migrationType: 'tokenTables' | 'tilmeldColumns' | 'tilmeldRemoveOldRows',
88
98
  ): Promise<void>;
99
+
100
+ abstract getEtypes(): Promise<string[]>;
101
+ abstract getIndexes(etype: string): Promise<
102
+ {
103
+ scope: 'data' | 'references' | 'tokens';
104
+ name: string;
105
+ property: string;
106
+ }[]
107
+ >;
108
+ abstract addIndex(
109
+ etype: string,
110
+ definition: {
111
+ scope: 'data' | 'references' | 'tokens';
112
+ name: string;
113
+ property: string;
114
+ },
115
+ ): Promise<boolean>;
116
+ abstract deleteIndex(
117
+ etype: string,
118
+ scope: 'data' | 'references' | 'tokens',
119
+ name: string,
120
+ ): Promise<boolean>;
121
+
122
+ abstract exportDataIterator(): AsyncGenerator<
123
+ { type: 'comment' | 'uid' | 'entity'; content: string },
124
+ void,
125
+ undefined | false
126
+ >;
89
127
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
90
128
  options: Options<T> & { return: 'count' },
91
129
  ...selectors: Selector[]
@@ -95,39 +133,45 @@ export default abstract class NymphDriver {
95
133
  ...selectors: Selector[]
96
134
  ): Promise<string[]>;
97
135
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
98
- options?: Options<T>,
136
+ options: Options<T> & { return: 'object' },
99
137
  ...selectors: Selector[]
100
- ): Promise<ReturnType<T['factorySync']>[]>;
138
+ ): Promise<EntityObjectType<T>[]>;
101
139
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
102
140
  options?: Options<T>,
103
141
  ...selectors: Selector[]
104
- ): Promise<ReturnType<T['factorySync']>[] | string[] | number>;
105
- protected abstract getEntitiesSync<
106
- T extends EntityConstructor = EntityConstructor
107
- >(
108
- options: Options<T> & { return: 'count' },
109
- ...selectors: Selector[]
110
- ): number;
111
- protected abstract getEntitiesSync<
112
- T extends EntityConstructor = EntityConstructor
113
- >(
114
- options: Options<T> & { return: 'guid' },
115
- ...selectors: Selector[]
116
- ): string[];
117
- protected abstract getEntitiesSync<
118
- T extends EntityConstructor = EntityConstructor
119
- >(
120
- options?: Options<T>,
121
- ...selectors: Selector[]
122
- ): ReturnType<T['factorySync']>[];
123
- protected abstract getEntitiesSync<
124
- T extends EntityConstructor = EntityConstructor
125
- >(
142
+ ): Promise<EntityInstanceType<T>[]>;
143
+ abstract getEntities<T extends EntityConstructor = EntityConstructor>(
126
144
  options?: Options<T>,
127
145
  ...selectors: Selector[]
128
- ): ReturnType<T['factorySync']>[] | string[] | number;
146
+ ): Promise<
147
+ EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] | number
148
+ >;
129
149
  abstract getUID(name: string): Promise<number | null>;
130
- abstract import(filename: string): Promise<boolean>;
150
+ abstract importEntity(entity: {
151
+ guid: string;
152
+ cdate: number;
153
+ mdate: number;
154
+ tags: string[];
155
+ sdata: SerializedEntityData;
156
+ etype: string;
157
+ }): Promise<void>;
158
+ abstract importEntityTokens(entity: {
159
+ guid: string;
160
+ cdate: number;
161
+ mdate: number;
162
+ tags: string[];
163
+ sdata: SerializedEntityData;
164
+ etype: string;
165
+ }): Promise<void>;
166
+ abstract importEntityTilmeldAC(entity: {
167
+ guid: string;
168
+ cdate: number;
169
+ mdate: number;
170
+ tags: string[];
171
+ sdata: SerializedEntityData;
172
+ etype: string;
173
+ }): Promise<void>;
174
+ abstract importUID(uid: { name: string; value: number }): Promise<void>;
131
175
  abstract newUID(name: string): Promise<number | null>;
132
176
  abstract renameUID(oldName: string, newName: string): Promise<boolean>;
133
177
  abstract saveEntity(entity: EntityInterface): Promise<boolean>;
@@ -147,7 +191,7 @@ export default abstract class NymphDriver {
147
191
  protected posixRegexMatch(
148
192
  pattern: string,
149
193
  subject: string,
150
- caseInsensitive = false
194
+ caseInsensitive = false,
151
195
  ) {
152
196
  const posixClasses = [
153
197
  // '[[:<:]]',
@@ -198,15 +242,23 @@ export default abstract class NymphDriver {
198
242
  return re.test(subject);
199
243
  }
200
244
 
245
+ public checkIndexName(name: string) {
246
+ if (name.match(/[^a-z0-9_]/)) {
247
+ throw new Error(
248
+ 'Index names can only contain lowercase letters, numbers, and underscores.',
249
+ );
250
+ }
251
+ }
252
+
201
253
  public async export(filename: string) {
202
254
  try {
203
255
  const fhandle = fs.openSync(filename, 'w');
204
256
  if (!fhandle) {
205
257
  throw new InvalidParametersError('Provided filename is not writeable.');
206
258
  }
207
- await this.exportEntities((line: string) => {
208
- fs.writeSync(fhandle, `${line}\n`);
209
- });
259
+ for await (let entry of this.exportDataIterator()) {
260
+ fs.writeSync(fhandle, entry.content);
261
+ }
210
262
  fs.closeSync(fhandle);
211
263
  } catch (e: any) {
212
264
  return false;
@@ -215,130 +267,157 @@ export default abstract class NymphDriver {
215
267
  }
216
268
 
217
269
  public async exportPrint() {
218
- await this.exportEntities((line: string) => {
219
- console.log(line);
220
- });
270
+ for await (let entry of this.exportDataIterator()) {
271
+ console.log(entry.content);
272
+ }
221
273
  return true;
222
274
  }
223
275
 
224
- protected async importFromFile(
225
- filename: string,
226
- saveEntityCallback: (
227
- guid: string,
228
- tags: string[],
229
- sdata: SerializedEntityData,
230
- etype: string
231
- ) => Promise<void>,
232
- saveUIDCallback: (name: string, value: number) => Promise<void>,
233
- startTransactionCallback: (() => Promise<void>) | null = null,
234
- commitTransactionCallback: (() => Promise<void>) | null = null
276
+ public async importDataIterator(
277
+ lines: Iterable<string>,
278
+ transaction?: boolean,
235
279
  ) {
236
- let rl: ReadLines;
237
- try {
238
- rl = new ReadLines(filename);
239
- } catch (e: any) {
240
- throw new InvalidParametersError('Provided filename is unreadable.');
280
+ const first = lines[Symbol.iterator]().next();
281
+
282
+ if (first.done || first.value !== '#nex2') {
283
+ throw new Error('Tried to import a file that is not a NEX v2 file.');
241
284
  }
242
- let guid: string | null = null;
243
- let line: Buffer | false = false;
244
- let sdata: SerializedEntityData = {};
245
- let tags: string[] = [];
246
- let etype = '__undefined';
247
- if (startTransactionCallback) {
248
- await startTransactionCallback();
285
+
286
+ if (transaction) {
287
+ await this.internalTransaction('nymph-import');
249
288
  }
250
- while ((line = rl.next())) {
251
- const text = line.toString('utf8');
252
- if (text.match(/^\s*#/)) {
253
- continue;
254
- }
255
- const entityMatch = text.match(
256
- /^\s*{([0-9A-Fa-f]+)}<([-\w_]+)>\[([^\]]*)\]\s*$/
257
- );
258
- const propMatch = text.match(/^\s*([^=]+)\s*=\s*(.*\S)\s*$/);
259
- const uidMatch = text.match(/^\s*<([^>]+)>\[(\d+)\]\s*$/);
260
- if (uidMatch) {
261
- // Add the UID.
262
- await saveUIDCallback(uidMatch[1], Number(uidMatch[2]));
263
- } else if (entityMatch) {
264
- // Save the current entity.
265
- if (guid) {
266
- await saveEntityCallback(guid, tags, sdata, etype);
267
- guid = null;
268
- tags = [];
269
- sdata = {};
270
- etype = '__undefined';
289
+ try {
290
+ let guid: string | null = null;
291
+ let sdata: SerializedEntityData = {};
292
+ let tags: string[] = [];
293
+ let etype = '__undefined';
294
+ for (let line of lines) {
295
+ if (line.match(/^\s*#/)) {
296
+ continue;
271
297
  }
272
- // Record the new entity's info.
273
- guid = entityMatch[1];
274
- etype = entityMatch[2];
275
- tags = entityMatch[3].split(',');
276
- } else if (propMatch) {
277
- // Add the variable to the new entity.
278
- if (guid) {
279
- sdata[propMatch[1]] = propMatch[2];
298
+ const entityMatch = line.match(
299
+ /^\s*{([0-9A-Fa-f]+)}<([-\w_]+)>\[([^\]]*)\]\s*$/,
300
+ );
301
+ const propMatch = line.match(/^\s*([^=]+)\s*=\s*(.*\S)\s*$/);
302
+ const uidMatch = line.match(/^\s*<([^>]+)>\[(\d+)\]\s*$/);
303
+ if (uidMatch) {
304
+ // Add the UID.
305
+ await this.importUID({
306
+ name: uidMatch[1],
307
+ value: Number(uidMatch[2]),
308
+ });
309
+ } else if (entityMatch) {
310
+ // Save the current entity.
311
+ if (guid) {
312
+ const cdate = Number(JSON.parse(sdata.cdate));
313
+ delete sdata.cdate;
314
+ const mdate = Number(JSON.parse(sdata.mdate));
315
+ delete sdata.mdate;
316
+ await this.importEntity({ guid, cdate, mdate, tags, sdata, etype });
317
+ guid = null;
318
+ tags = [];
319
+ sdata = {};
320
+ etype = '__undefined';
321
+ }
322
+ // Record the new entity's info.
323
+ guid = entityMatch[1];
324
+ etype = entityMatch[2];
325
+ tags = entityMatch[3].split(',');
326
+ } else if (propMatch) {
327
+ // Add the variable to the new entity.
328
+ if (guid) {
329
+ sdata[propMatch[1]] = propMatch[2];
330
+ }
280
331
  }
332
+ // Clear the entity cache.
333
+ this.entityCache = {};
281
334
  }
282
- // Clear the entity cache.
283
- this.entityCache = {};
284
- }
285
- // Save the last entity.
286
- if (guid) {
287
- await saveEntityCallback(guid, tags, sdata, etype);
288
- }
289
- if (commitTransactionCallback) {
290
- await commitTransactionCallback();
335
+ // Save the last entity.
336
+ if (guid) {
337
+ const cdate = Number(JSON.parse(sdata.cdate));
338
+ delete sdata.cdate;
339
+ const mdate = Number(JSON.parse(sdata.mdate));
340
+ delete sdata.mdate;
341
+ await this.importEntity({ guid, cdate, mdate, tags, sdata, etype });
342
+ }
343
+ if (transaction) {
344
+ await this.commit('nymph-import');
345
+ }
346
+ } catch (e: any) {
347
+ await this.rollback('nymph-import');
348
+ throw e;
291
349
  }
292
350
  return true;
293
351
  }
294
352
 
353
+ public async importData(text: string, transaction?: boolean) {
354
+ return await this.importDataIterator(text.split('\n'), transaction);
355
+ }
356
+
357
+ public async import(filename: string, transaction?: boolean) {
358
+ let rl: ReadLines;
359
+ try {
360
+ rl = new ReadLines(filename);
361
+ } catch (e: any) {
362
+ throw new InvalidParametersError('Provided filename is unreadable.');
363
+ }
364
+
365
+ const lines = {
366
+ *[Symbol.iterator]() {
367
+ let line: null | Buffer;
368
+ while ((line = rl.next())) {
369
+ yield line.toString('utf8');
370
+ }
371
+ },
372
+ };
373
+
374
+ return await this.importDataIterator(lines, transaction);
375
+ }
376
+
295
377
  public checkData(
378
+ etype: string,
296
379
  data: EntityData,
297
380
  sdata: SerializedEntityData,
298
381
  selectors: Selector[],
299
382
  guid: string | null = null,
300
- tags: string[] | null = null
383
+ tags: string[] | null = null,
301
384
  ) {
302
- const formattedSelectors = this.formatSelectors(selectors).selectors;
385
+ try {
386
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
387
+ const formattedSelectors = this.formatSelectors(selectors).selectors;
303
388
 
304
- for (const curSelector of formattedSelectors) {
305
- const type = curSelector.type;
306
- const typeIsNot = type === '!&' || type === '!|';
307
- const typeIsOr = type === '|' || type === '!|';
308
- let pass = !typeIsOr;
309
- for (const k in curSelector) {
310
- const key = k as keyof Selector;
311
- const value = curSelector[key];
389
+ for (const curSelector of formattedSelectors) {
390
+ const type = curSelector.type;
391
+ const typeIsNot = type === '!&' || type === '!|';
392
+ const typeIsOr = type === '|' || type === '!|';
393
+ let pass = !typeIsOr;
394
+ for (const k in curSelector) {
395
+ const key = k as keyof Selector;
396
+ const value = curSelector[key];
312
397
 
313
- if (key === 'type' || value == null) {
314
- continue;
315
- }
398
+ if (key === 'type' || value == null) {
399
+ continue;
400
+ }
316
401
 
317
- const clauseNot = key.substr(0, 1) === '!';
318
- if (key === 'selector' || key === '!selector') {
319
- const tmpArr = (Array.isArray(value) ? value : [value]) as Selector[];
320
- pass = xor(
321
- this.checkData(data, sdata, tmpArr, guid, tags),
322
- xor(typeIsNot, clauseNot)
323
- );
324
- } else {
325
- if (key === 'qref' || key === '!qref') {
326
- throw new Error("Can't use checkData on qref clauses.");
402
+ const clauseNot = key.substring(0, 1) === '!';
403
+ if (key === 'selector' || key === '!selector') {
404
+ const tmpArr = (
405
+ Array.isArray(value) ? value : [value]
406
+ ) as Selector[];
407
+ pass = xor(
408
+ this.checkData(etype, data, sdata, tmpArr, guid, tags),
409
+ xor(typeIsNot, clauseNot),
410
+ );
327
411
  } else {
328
- // Check if it doesn't pass any for &, check if it
329
- // passes any for |.
330
- for (const curValue of value) {
331
- if (curValue == null) {
332
- continue;
333
- }
412
+ if (key === 'qref' || key === '!qref') {
413
+ throw new Error("Can't use checkData on qref clauses.");
414
+ } else {
415
+ // Check if it doesn't pass any for &, check if it passes any for |.
416
+ for (const curValue of value) {
417
+ if (curValue == null) {
418
+ continue;
419
+ }
334
420
 
335
- if (
336
- ((key === 'guid' || key === '!guid') && guid == null) ||
337
- ((key === 'tag' || key === '!tag') && tags == null)
338
- ) {
339
- // Skip because it has already been checked (by the query).
340
- pass = true;
341
- } else {
342
421
  const propName = (curValue as string[])[0];
343
422
 
344
423
  // Unserialize the data for this variable.
@@ -356,7 +435,7 @@ export default abstract class NymphDriver {
356
435
  case '!tag':
357
436
  pass = xor(
358
437
  (tags ?? []).indexOf(propName) !== -1,
359
- xor(typeIsNot, clauseNot)
438
+ xor(typeIsNot, clauseNot),
360
439
  );
361
440
  break;
362
441
  case 'defined':
@@ -367,7 +446,7 @@ export default abstract class NymphDriver {
367
446
  case '!truthy':
368
447
  pass = xor(
369
448
  propName in data && data[propName],
370
- xor(typeIsNot, clauseNot)
449
+ xor(typeIsNot, clauseNot),
371
450
  );
372
451
  break;
373
452
  case 'ref':
@@ -377,9 +456,9 @@ export default abstract class NymphDriver {
377
456
  propName in data &&
378
457
  this.entityReferenceSearch(
379
458
  data[propName],
380
- testRefValue
459
+ testRefValue,
381
460
  ),
382
- xor(typeIsNot, clauseNot)
461
+ xor(typeIsNot, clauseNot),
383
462
  );
384
463
  break;
385
464
  case 'equal':
@@ -391,7 +470,7 @@ export default abstract class NymphDriver {
391
470
  propName in data &&
392
471
  JSON.stringify(data[propName]) ===
393
472
  JSON.stringify(testEqualValue),
394
- xor(typeIsNot, clauseNot)
473
+ xor(typeIsNot, clauseNot),
395
474
  );
396
475
  break;
397
476
  case 'contain':
@@ -402,9 +481,9 @@ export default abstract class NymphDriver {
402
481
  pass = xor(
403
482
  propName in data &&
404
483
  JSON.stringify(data[propName]).indexOf(
405
- JSON.stringify(testContainValue)
484
+ JSON.stringify(testContainValue),
406
485
  ) !== -1,
407
- xor(typeIsNot, clauseNot)
486
+ xor(typeIsNot, clauseNot),
408
487
  );
409
488
  break;
410
489
  case 'like':
@@ -419,9 +498,27 @@ export default abstract class NymphDriver {
419
498
  escapeRegExp(testLikeValue)
420
499
  .replace('%', () => '.*')
421
500
  .replace('_', () => '.') +
422
- '$'
501
+ '$',
502
+ ).test(data[propName]),
503
+ xor(typeIsNot, clauseNot),
504
+ );
505
+ break;
506
+ case 'ilike':
507
+ case '!ilike':
508
+ const testILikeValue = (
509
+ curValue as [string, string]
510
+ )[1] as string;
511
+ pass = xor(
512
+ propName in data &&
513
+ new RegExp(
514
+ '^' +
515
+ escapeRegExp(testILikeValue)
516
+ .replace('%', () => '.*')
517
+ .replace('_', () => '.') +
518
+ '$',
519
+ 'i',
423
520
  ).test(data[propName]),
424
- xor(typeIsNot, clauseNot)
521
+ xor(typeIsNot, clauseNot),
425
522
  );
426
523
  break;
427
524
  case 'match':
@@ -433,7 +530,7 @@ export default abstract class NymphDriver {
433
530
  pass = xor(
434
531
  propName in data &&
435
532
  this.posixRegexMatch(testMatchValue, data[propName]),
436
- xor(typeIsNot, clauseNot)
533
+ xor(typeIsNot, clauseNot),
437
534
  );
438
535
  break;
439
536
  case 'imatch':
@@ -447,9 +544,28 @@ export default abstract class NymphDriver {
447
544
  this.posixRegexMatch(
448
545
  testIMatchValue,
449
546
  data[propName],
450
- true
547
+ true,
451
548
  ),
452
- xor(typeIsNot, clauseNot)
549
+ xor(typeIsNot, clauseNot),
550
+ );
551
+ break;
552
+ case 'search':
553
+ case '!search':
554
+ const testSearchValue = (
555
+ curValue as [string, string]
556
+ )[1] as string;
557
+ const tokenString =
558
+ propName in data
559
+ ? (EntityClass.getFTSText(propName, data[propName]) ??
560
+ '')
561
+ : '';
562
+ pass = xor(
563
+ propName in data &&
564
+ this.tokenizer.searchString(
565
+ testSearchValue,
566
+ tokenString,
567
+ ),
568
+ xor(typeIsNot, clauseNot),
453
569
  );
454
570
  break;
455
571
  case 'gt':
@@ -459,7 +575,7 @@ export default abstract class NymphDriver {
459
575
  )[1] as number;
460
576
  pass = xor(
461
577
  propName in data && data[propName] > testGTValue,
462
- xor(typeIsNot, clauseNot)
578
+ xor(typeIsNot, clauseNot),
463
579
  );
464
580
  break;
465
581
  case 'gte':
@@ -469,7 +585,7 @@ export default abstract class NymphDriver {
469
585
  )[1] as number;
470
586
  pass = xor(
471
587
  propName in data && data[propName] >= testGTEValue,
472
- xor(typeIsNot, clauseNot)
588
+ xor(typeIsNot, clauseNot),
473
589
  );
474
590
  break;
475
591
  case 'lt':
@@ -479,7 +595,7 @@ export default abstract class NymphDriver {
479
595
  )[1] as number;
480
596
  pass = xor(
481
597
  propName in data && data[propName] < testLTValue,
482
- xor(typeIsNot, clauseNot)
598
+ xor(typeIsNot, clauseNot),
483
599
  );
484
600
  break;
485
601
  case 'lte':
@@ -489,26 +605,33 @@ export default abstract class NymphDriver {
489
605
  )[1] as number;
490
606
  pass = xor(
491
607
  propName in data && data[propName] <= testLTEValue,
492
- xor(typeIsNot, clauseNot)
608
+ xor(typeIsNot, clauseNot),
493
609
  );
494
610
  break;
495
611
  }
496
- }
497
- if (!xor(typeIsOr, pass)) {
498
- break;
612
+
613
+ if (!xor(typeIsOr, pass)) {
614
+ break;
615
+ }
499
616
  }
500
617
  }
501
618
  }
619
+ if (!xor(typeIsOr, pass)) {
620
+ break;
621
+ }
502
622
  }
503
- if (!xor(typeIsOr, pass)) {
504
- break;
623
+ if (!pass) {
624
+ return false;
505
625
  }
506
626
  }
507
- if (!pass) {
508
- return false;
509
- }
627
+ return true;
628
+ } catch (e: any) {
629
+ this.nymph.config.debugError(
630
+ 'nymph',
631
+ `Failed to check entity data: ${e}`,
632
+ );
633
+ throw e;
510
634
  }
511
- return true;
512
635
  }
513
636
 
514
637
  /**
@@ -547,7 +670,7 @@ export default abstract class NymphDriver {
547
670
  | EntityInterface
548
671
  | EntityReference
549
672
  | string
550
- | (EntityInterface | EntityReference | string)[]
673
+ | (EntityInterface | EntityReference | string)[],
551
674
  ) {
552
675
  // Get the GUID, if the passed $entity is an object.
553
676
  const guids: string[] = [];
@@ -578,7 +701,7 @@ export default abstract class NymphDriver {
578
701
  return true;
579
702
  }
580
703
  // Search through arrays and objects looking for the reference.
581
- if (Array.isArray(value) || typeof value === 'object') {
704
+ if (value != null && (Array.isArray(value) || typeof value === 'object')) {
582
705
  for (const key in value) {
583
706
  if (this.entityReferenceSearch(value[key], guids)) {
584
707
  return true;
@@ -588,9 +711,220 @@ export default abstract class NymphDriver {
588
711
  return false;
589
712
  }
590
713
 
714
+ protected removeAndReturnACValues(
715
+ etype: string,
716
+ data: EntityData,
717
+ sdata: SerializedEntityData,
718
+ ) {
719
+ let user: string | null = null;
720
+ let group: string | null = null;
721
+ let acUser: number | null = null;
722
+ let acGroup: number | null = null;
723
+ let acOther: number | null = null;
724
+ let acRead: string[] | null = null;
725
+ let acWrite: string[] | null = null;
726
+ let acFull: string[] | null = null;
727
+
728
+ if (this.nymph.tilmeld != null) {
729
+ // Since Tilmeld is installed, we need to fill the AC vars.
730
+ const userValue =
731
+ 'user' in sdata
732
+ ? JSON.parse(sdata.user)
733
+ : 'user' in data
734
+ ? data.user
735
+ : null;
736
+ if (
737
+ Array.isArray(userValue) &&
738
+ userValue[0] === 'nymph_entity_reference' &&
739
+ userValue[2] === 'User' &&
740
+ typeof userValue[1] === 'string' &&
741
+ userValue[1].match(/^[0-9a-f]{24}$/i)
742
+ ) {
743
+ user = userValue[1];
744
+ }
745
+
746
+ const groupValue =
747
+ 'group' in sdata
748
+ ? JSON.parse(sdata.group)
749
+ : 'group' in data
750
+ ? data.group
751
+ : null;
752
+ if (
753
+ Array.isArray(groupValue) &&
754
+ groupValue[0] === 'nymph_entity_reference' &&
755
+ groupValue[2] === 'Group' &&
756
+ typeof groupValue[1] === 'string' &&
757
+ groupValue[1].match(/^[0-9a-f]{24}$/i)
758
+ ) {
759
+ group = groupValue[1];
760
+ }
761
+
762
+ if (etype !== 'tilmeld_user' && etype !== 'tilmeld_group') {
763
+ const acUserValue =
764
+ 'acUser' in sdata
765
+ ? JSON.parse(sdata.acUser)
766
+ : 'acUser' in data
767
+ ? data.acUser
768
+ : null;
769
+ if (
770
+ typeof acUserValue === 'number' &&
771
+ !isNaN(acUserValue) &&
772
+ acUserValue >= 0
773
+ ) {
774
+ acUser = acUserValue;
775
+ }
776
+
777
+ const acGroupValue =
778
+ 'acGroup' in sdata
779
+ ? JSON.parse(sdata.acGroup)
780
+ : 'acGroup' in data
781
+ ? data.acGroup
782
+ : null;
783
+ if (
784
+ typeof acGroupValue === 'number' &&
785
+ !isNaN(acGroupValue) &&
786
+ acGroupValue >= 0
787
+ ) {
788
+ acGroup = acGroupValue;
789
+ }
790
+
791
+ const acOtherValue =
792
+ 'acOther' in sdata
793
+ ? JSON.parse(sdata.acOther)
794
+ : 'acOther' in data
795
+ ? data.acOther
796
+ : null;
797
+ if (
798
+ typeof acOtherValue === 'number' &&
799
+ !isNaN(acOtherValue) &&
800
+ acOtherValue >= 0
801
+ ) {
802
+ acOther = acOtherValue;
803
+ }
804
+
805
+ const acReadValue =
806
+ 'acRead' in sdata
807
+ ? JSON.parse(sdata.acRead)
808
+ : 'acRead' in data
809
+ ? data.acRead
810
+ : null;
811
+ if (Array.isArray(acReadValue)) {
812
+ const acReadArray = [];
813
+ for (let acReadEntry of acReadValue) {
814
+ if (
815
+ typeof acReadEntry === 'string' &&
816
+ acReadEntry.match(/^[0-9a-f]{24}$/i)
817
+ ) {
818
+ acReadArray.push(acReadEntry);
819
+ } else if (
820
+ Array.isArray(acReadEntry) &&
821
+ acReadEntry[0] === 'nymph_entity_reference' &&
822
+ (acReadEntry[2] === 'User' || acReadEntry[2] === 'Group') &&
823
+ typeof acReadEntry[1] === 'string' &&
824
+ acReadEntry[1].match(/^[0-9a-f]{24}$/i)
825
+ ) {
826
+ acReadArray.push(acReadEntry[1]);
827
+ }
828
+ }
829
+ if (acReadArray.length) {
830
+ acRead = acReadArray;
831
+ }
832
+ }
833
+
834
+ const acWriteValue =
835
+ 'acWrite' in sdata
836
+ ? JSON.parse(sdata.acWrite)
837
+ : 'acWrite' in data
838
+ ? data.acWrite
839
+ : null;
840
+ if (Array.isArray(acWriteValue)) {
841
+ const acWriteArray = [];
842
+ for (let acWriteEntry of acWriteValue) {
843
+ if (
844
+ typeof acWriteEntry === 'string' &&
845
+ acWriteEntry.match(/^[0-9a-f]{24}$/i)
846
+ ) {
847
+ acWriteArray.push(acWriteEntry);
848
+ } else if (
849
+ Array.isArray(acWriteEntry) &&
850
+ acWriteEntry[0] === 'nymph_entity_reference' &&
851
+ (acWriteEntry[2] === 'User' || acWriteEntry[2] === 'Group') &&
852
+ typeof acWriteEntry[1] === 'string' &&
853
+ acWriteEntry[1].match(/^[0-9a-f]{24}$/i)
854
+ ) {
855
+ acWriteArray.push(acWriteEntry[1]);
856
+ }
857
+ }
858
+ if (acWriteArray.length) {
859
+ acWrite = acWriteArray;
860
+ }
861
+ }
862
+
863
+ const acFullValue =
864
+ 'acFull' in sdata
865
+ ? JSON.parse(sdata.acFull)
866
+ : 'acFull' in data
867
+ ? data.acFull
868
+ : null;
869
+ if (Array.isArray(acFullValue)) {
870
+ const acFullArray = [];
871
+ for (let acFullEntry of acFullValue) {
872
+ if (
873
+ typeof acFullEntry === 'string' &&
874
+ acFullEntry.match(/^[0-9a-f]{24}$/i)
875
+ ) {
876
+ acFullArray.push(acFullEntry);
877
+ } else if (
878
+ Array.isArray(acFullEntry) &&
879
+ acFullEntry[0] === 'nymph_entity_reference' &&
880
+ (acFullEntry[2] === 'User' || acFullEntry[2] === 'Group') &&
881
+ typeof acFullEntry[1] === 'string' &&
882
+ acFullEntry[1].match(/^[0-9a-f]{24}$/i)
883
+ ) {
884
+ acFullArray.push(acFullEntry[1]);
885
+ }
886
+ }
887
+ if (acFullArray.length) {
888
+ acFull = acFullArray;
889
+ }
890
+ }
891
+ }
892
+
893
+ // Delete all of the data, so it doesn't go into the database as rows in
894
+ // the data tables.
895
+ delete sdata.user;
896
+ delete data.user;
897
+ delete sdata.group;
898
+ delete data.group;
899
+ delete sdata.acUser;
900
+ delete data.acUser;
901
+ delete sdata.acGroup;
902
+ delete data.acGroup;
903
+ delete sdata.acOther;
904
+ delete data.acOther;
905
+ delete sdata.acRead;
906
+ delete data.acRead;
907
+ delete sdata.acWrite;
908
+ delete data.acWrite;
909
+ delete sdata.acFull;
910
+ delete data.acFull;
911
+ }
912
+
913
+ return {
914
+ user,
915
+ group,
916
+ acUser,
917
+ acGroup,
918
+ acOther,
919
+ acRead,
920
+ acWrite,
921
+ acFull,
922
+ };
923
+ }
924
+
591
925
  public formatSelectors(
592
926
  selectors: Selector[],
593
- options: Options = {}
927
+ options: Options = {},
594
928
  ): {
595
929
  selectors: FormattedSelector[];
596
930
  qrefs: [Options, ...FormattedSelector[]][];
@@ -625,10 +959,9 @@ export default abstract class NymphDriver {
625
959
  for (let i = 0; i < tmpArr.length; i++) {
626
960
  const name = tmpArr[i][0];
627
961
  const [qrefOptions, ...qrefSelectors] = tmpArr[i][1];
628
- const QrefEntityClass =
629
- typeof qrefOptions.class === 'string'
630
- ? this.nymph.getEntityClass(qrefOptions.class)
631
- : qrefOptions.class ?? this.nymph.getEntityClass('Entity');
962
+ const QrefEntityClass = qrefOptions.class
963
+ ? this.nymph.getEntityClass(qrefOptions.class)
964
+ : this.nymph.getEntityClass('Entity');
632
965
  const newOptions = {
633
966
  ...qrefOptions,
634
967
  class: QrefEntityClass,
@@ -637,7 +970,7 @@ export default abstract class NymphDriver {
637
970
  const newSelectors = this.formatSelectors(qrefSelectors, options);
638
971
  qrefs.push(
639
972
  [newOptions, ...newSelectors.selectors],
640
- ...newSelectors.qrefs
973
+ ...newSelectors.qrefs,
641
974
  );
642
975
  formatArr[i] = [name, [newOptions, ...newSelectors.selectors]];
643
976
  }
@@ -685,12 +1018,12 @@ export default abstract class NymphDriver {
685
1018
 
686
1019
  protected iterateSelectorsForQuery(
687
1020
  selectors: FormattedSelector[],
688
- callback: (
689
- key: string,
690
- value: any,
691
- typeIsOr: boolean,
692
- typeIsNot: boolean
693
- ) => string
1021
+ callback: (data: {
1022
+ key: string;
1023
+ value: any;
1024
+ typeIsOr: boolean;
1025
+ typeIsNot: boolean;
1026
+ }) => string,
694
1027
  ) {
695
1028
  const queryParts = [];
696
1029
  for (const curSelector of selectors) {
@@ -703,7 +1036,7 @@ export default abstract class NymphDriver {
703
1036
  if (key === 'type') {
704
1037
  continue;
705
1038
  }
706
- let curQuery = callback(key, value, typeIsOr, typeIsNot);
1039
+ let curQuery = callback({ key, value, typeIsOr, typeIsNot });
707
1040
  if (curQuery) {
708
1041
  if (curSelectorQuery) {
709
1042
  curSelectorQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -719,71 +1052,14 @@ export default abstract class NymphDriver {
719
1052
  return queryParts;
720
1053
  }
721
1054
 
722
- /**
723
- * Get the first entity to match all options/selectors, synchronously.
724
- *
725
- * This should really only be used internally by the Entity class. It's a bad
726
- * idea to get entities synchronously. The Entity class uses this to load
727
- * sleeping references.
728
- *
729
- * @param options The options.
730
- * @param selectors Unlimited optional selectors to search for, or a single GUID. If none are given, all entities are searched for the given options.
731
- * @returns An entity, or null on failure or nothing found.
732
- */
733
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
734
- options: Options<T> & { return: 'count' },
735
- ...selectors: Selector[]
736
- ): number;
737
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
738
- options: Options<T> & { return: 'guid' },
739
- ...selectors: Selector[]
740
- ): string | null;
741
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
742
- options: Options<T>,
743
- ...selectors: Selector[]
744
- ): ReturnType<T['factorySync']> | null;
745
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
746
- options: Options<T> & { return: 'count' },
747
- guid: string
748
- ): number;
749
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
750
- options: Options<T> & { return: 'guid' },
751
- guid: string
752
- ): string | null;
753
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
754
- options: Options<T>,
755
- guid: string
756
- ): ReturnType<T['factorySync']> | null;
757
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
758
- options: Options<T> = {},
759
- ...selectors: Selector[] | string[]
760
- ): ReturnType<T['factorySync']> | string | number | null {
761
- // Set up options and selectors.
762
- if (typeof selectors[0] === 'string') {
763
- selectors = [{ type: '&', guid: selectors[0] }];
764
- }
765
- options.limit = 1;
766
- const entities = this.getEntitiesSync(
767
- options,
768
- ...(selectors as Selector[])
769
- );
770
- if (options.return === 'count') {
771
- return entities as unknown as number;
772
- }
773
- if (!entities || !entities.length) {
774
- return null;
775
- }
776
- return entities[0];
777
- }
778
-
779
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
1055
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
780
1056
  options: Options<T> & { return: 'count' },
781
1057
  selectors: Selector[],
782
- performQueryCallback: (
783
- options: Options<T>,
784
- formattedSelectors: FormattedSelector[],
785
- etype: string
786
- ) => {
1058
+ performQueryCallback: (data: {
1059
+ options: Options<T>;
1060
+ selectors: FormattedSelector[];
1061
+ etype: string;
1062
+ }) => {
787
1063
  result: any;
788
1064
  },
789
1065
  rowFetchCallback: () => any,
@@ -798,16 +1074,16 @@ export default abstract class NymphDriver {
798
1074
  getDataNameAndSValueCallback: (row: any) => {
799
1075
  name: string;
800
1076
  svalue: string;
801
- }
1077
+ },
802
1078
  ): { result: any; process: () => number | Error };
803
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
1079
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
804
1080
  options: Options<T> & { return: 'guid' },
805
1081
  selectors: Selector[],
806
- performQueryCallback: (
807
- options: Options<T>,
808
- formattedSelectors: FormattedSelector[],
809
- etype: string
810
- ) => {
1082
+ performQueryCallback: (data: {
1083
+ options: Options<T>;
1084
+ selectors: FormattedSelector[];
1085
+ etype: string;
1086
+ }) => {
811
1087
  result: any;
812
1088
  },
813
1089
  rowFetchCallback: () => any,
@@ -822,16 +1098,40 @@ export default abstract class NymphDriver {
822
1098
  getDataNameAndSValueCallback: (row: any) => {
823
1099
  name: string;
824
1100
  svalue: string;
825
- }
1101
+ },
826
1102
  ): { result: any; process: () => string[] | Error };
827
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
1103
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
1104
+ options: Options<T> & { return: 'object' },
1105
+ selectors: Selector[],
1106
+ performQueryCallback: (data: {
1107
+ options: Options<T>;
1108
+ selectors: FormattedSelector[];
1109
+ etype: string;
1110
+ }) => {
1111
+ result: any;
1112
+ },
1113
+ rowFetchCallback: () => any,
1114
+ freeResultCallback: () => void,
1115
+ getCountCallback: (row: any) => number,
1116
+ getGUIDCallback: (row: any) => string,
1117
+ getTagsAndDatesCallback: (row: any) => {
1118
+ tags: string[];
1119
+ cdate: number;
1120
+ mdate: number;
1121
+ },
1122
+ getDataNameAndSValueCallback: (row: any) => {
1123
+ name: string;
1124
+ svalue: string;
1125
+ },
1126
+ ): { result: any; process: () => EntityObjectType<T>[] | Error };
1127
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
828
1128
  options: Options<T>,
829
1129
  selectors: Selector[],
830
- performQueryCallback: (
831
- options: Options<T>,
832
- formattedSelectors: FormattedSelector[],
833
- etype: string
834
- ) => {
1130
+ performQueryCallback: (data: {
1131
+ options: Options<T>;
1132
+ selectors: FormattedSelector[];
1133
+ etype: string;
1134
+ }) => {
835
1135
  result: any;
836
1136
  },
837
1137
  rowFetchCallback: () => any,
@@ -846,16 +1146,19 @@ export default abstract class NymphDriver {
846
1146
  getDataNameAndSValueCallback: (row: any) => {
847
1147
  name: string;
848
1148
  svalue: string;
849
- }
850
- ): { result: any; process: () => ReturnType<T['factorySync']>[] | Error };
851
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
1149
+ },
1150
+ ): {
1151
+ result: any;
1152
+ process: () => EntityInstanceType<T>[] | Error;
1153
+ };
1154
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
852
1155
  options: Options<T>,
853
1156
  selectors: Selector[],
854
- performQueryCallback: (
855
- options: Options<T>,
856
- formattedSelectors: FormattedSelector[],
857
- etype: string
858
- ) => {
1157
+ performQueryCallback: (data: {
1158
+ options: Options<T>;
1159
+ selectors: FormattedSelector[];
1160
+ etype: string;
1161
+ }) => {
859
1162
  result: any;
860
1163
  },
861
1164
  rowFetchCallback: () => any,
@@ -866,14 +1169,27 @@ export default abstract class NymphDriver {
866
1169
  tags: string[];
867
1170
  cdate: number;
868
1171
  mdate: number;
1172
+ user?: string;
1173
+ group?: string;
1174
+ acUser?: number;
1175
+ acGroup?: number;
1176
+ acOther?: number;
1177
+ acRead?: string[];
1178
+ acWrite?: string[];
1179
+ acFull?: string[];
869
1180
  },
870
1181
  getDataNameAndSValueCallback: (row: any) => {
871
1182
  name: string;
872
1183
  svalue: string;
873
- }
1184
+ },
874
1185
  ): {
875
1186
  result: any;
876
- process: () => ReturnType<T['factorySync']>[] | string[] | number | Error;
1187
+ process: () =>
1188
+ | EntityInstanceType<T>[]
1189
+ | EntityObjectType<T>[]
1190
+ | string[]
1191
+ | number
1192
+ | Error;
877
1193
  } {
878
1194
  if (!this.isConnected()) {
879
1195
  throw new UnableToConnectError('not connected to DB');
@@ -886,21 +1202,29 @@ export default abstract class NymphDriver {
886
1202
  ['&', '!&', '|', '!|'].indexOf(selector.type) === -1
887
1203
  ) {
888
1204
  throw new InvalidParametersError(
889
- 'Invalid query selector passed: ' + JSON.stringify(selector)
1205
+ 'Invalid query selector passed: ' + JSON.stringify(selector),
890
1206
  );
891
1207
  }
892
1208
  }
893
1209
 
894
- let entities: ReturnType<T['factorySync']>[] | string[] = [];
1210
+ let entities: EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] =
1211
+ [];
895
1212
  let count = 0;
896
1213
  const EntityClass =
897
1214
  options.class ?? (this.nymph.getEntityClass('Entity') as T);
898
1215
  const etype = EntityClass.ETYPE;
899
1216
 
1217
+ if (options.source === 'client' && options.return === 'object') {
1218
+ throw new InvalidParametersError(
1219
+ 'Object return type not allowed from client.',
1220
+ );
1221
+ }
1222
+
900
1223
  // Check if the requested entity is cached.
901
1224
  if (
902
1225
  this.nymph.config.cache &&
903
1226
  !options.skipCache &&
1227
+ selectors.length &&
904
1228
  'guid' in selectors[0] &&
905
1229
  typeof selectors[0].guid === 'number'
906
1230
  ) {
@@ -912,33 +1236,75 @@ export default abstract class NymphDriver {
912
1236
  (Object.keys(selectors[0]).length === 2 ||
913
1237
  (Object.keys(selectors[0]).length === 3 && 'tag' in selectors[0]))
914
1238
  ) {
915
- const entity = this.pullCache<ReturnType<T['factorySync']>>(
916
- selectors[0]['guid'],
917
- EntityClass.class,
918
- !!options.skipAc
919
- );
1239
+ const cacheEntity = this.pullCache(selectors[0]['guid']);
1240
+
920
1241
  if (
921
- entity != null &&
922
- entity.guid != null &&
1242
+ cacheEntity != null &&
1243
+ cacheEntity.guid != null &&
923
1244
  (!('tag' in selectors[0]) ||
924
- entity.hasTag(
925
- ...(Array.isArray(selectors[0].tag)
1245
+ !(
1246
+ Array.isArray(selectors[0].tag)
926
1247
  ? selectors[0].tag
927
- : [selectors[0].tag])
928
- ))
1248
+ : [selectors[0].tag]
1249
+ ).find((tag) => tag == null || !cacheEntity.tags.includes(tag)))
929
1250
  ) {
930
- const guid = entity.guid;
931
- return {
932
- result: Promise.resolve(null),
933
- process: () =>
934
- options.return === 'count'
935
- ? guid
936
- ? 1
937
- : 0
938
- : options.return === 'guid'
939
- ? [guid]
940
- : [entity],
941
- };
1251
+ // Return the value from the cache.
1252
+ const { guid, cdate, mdate, tags, data, sdata } = cacheEntity;
1253
+
1254
+ if (options.return === 'count') {
1255
+ return {
1256
+ result: Promise.resolve(null),
1257
+ process: () => 1,
1258
+ };
1259
+ } else if (options.return === 'guid') {
1260
+ return {
1261
+ result: Promise.resolve(null),
1262
+ process: () => [guid],
1263
+ };
1264
+ } else if (options.return === 'object') {
1265
+ return {
1266
+ result: Promise.resolve(null),
1267
+ process: () => [
1268
+ {
1269
+ guid,
1270
+ cdate,
1271
+ mdate,
1272
+ tags,
1273
+ ...data,
1274
+ ...Object.fromEntries(
1275
+ Object.entries(sdata).map(([name, value]) => [
1276
+ name,
1277
+ JSON.parse(value),
1278
+ ]),
1279
+ ),
1280
+ } as EntityObjectType<T>,
1281
+ ],
1282
+ };
1283
+ } else {
1284
+ const entity = this.nymph
1285
+ .getEntityClass(EntityClass.class)
1286
+ .factorySync() as EntityInstanceType<T>;
1287
+ if (entity) {
1288
+ entity.$nymph = this.nymph;
1289
+ if (options.skipAc != null) {
1290
+ entity.$useSkipAc(!!options.skipAc);
1291
+ }
1292
+ entity.guid = guid;
1293
+ entity.cdate = cdate;
1294
+ entity.mdate = mdate;
1295
+ entity.tags = tags;
1296
+ entity.$putData(data, sdata, 'server');
1297
+ return {
1298
+ result: Promise.resolve(null),
1299
+ process: () => [entity],
1300
+ };
1301
+ }
1302
+
1303
+ return {
1304
+ result: Promise.resolve(null),
1305
+ process: () => [],
1306
+ };
1307
+ }
942
1308
  }
943
1309
  }
944
1310
  }
@@ -946,15 +1312,16 @@ export default abstract class NymphDriver {
946
1312
  try {
947
1313
  const formattedSelectors = this.formatSelectors(selectors, options);
948
1314
  this.nymph.runQueryCallbacks(options, formattedSelectors.selectors);
949
- for (let qref of formattedSelectors.qrefs) {
950
- const [options, ...selectors] = qref;
1315
+ for (let i = 0; i < formattedSelectors.qrefs.length; i++) {
1316
+ const [options, ...selectors] = formattedSelectors.qrefs[i];
951
1317
  this.nymph.runQueryCallbacks(options, selectors);
1318
+ formattedSelectors.qrefs[i] = [options, ...selectors];
952
1319
  }
953
- const { result } = performQueryCallback(
1320
+ const { result } = performQueryCallback({
954
1321
  options,
955
- formattedSelectors.selectors,
956
- etype
957
- );
1322
+ selectors: formattedSelectors.selectors,
1323
+ etype,
1324
+ });
958
1325
 
959
1326
  return {
960
1327
  result,
@@ -994,28 +1361,86 @@ export default abstract class NymphDriver {
994
1361
  // Make sure that $row is incremented :)
995
1362
  row = rowFetchCallback();
996
1363
  }
997
- const entity = EntityClass.factorySync() as ReturnType<
998
- T['factorySync']
999
- >;
1000
- entity.$nymph = this.nymph;
1001
- if (options.skipAc != null) {
1002
- entity.$useSkipAc(!!options.skipAc);
1364
+
1365
+ if (this.nymph.tilmeld) {
1366
+ if ('user' in tagsAndDates && tagsAndDates.user != null) {
1367
+ data.user = [
1368
+ 'nymph_entity_reference',
1369
+ tagsAndDates.user,
1370
+ 'User',
1371
+ ];
1372
+ delete sdata.user;
1373
+ }
1374
+ if ('group' in tagsAndDates && tagsAndDates.group != null) {
1375
+ data.group = [
1376
+ 'nymph_entity_reference',
1377
+ tagsAndDates.group,
1378
+ 'Group',
1379
+ ];
1380
+ delete sdata.group;
1381
+ }
1382
+ if ('acUser' in tagsAndDates && tagsAndDates.acUser != null) {
1383
+ data.acUser = tagsAndDates.acUser;
1384
+ delete sdata.acUser;
1385
+ }
1386
+ if ('acGroup' in tagsAndDates && tagsAndDates.acGroup != null) {
1387
+ data.acGroup = tagsAndDates.acGroup;
1388
+ delete sdata.acGroup;
1389
+ }
1390
+ if ('acOther' in tagsAndDates && tagsAndDates.acOther != null) {
1391
+ data.acOther = tagsAndDates.acOther;
1392
+ delete sdata.acOther;
1393
+ }
1394
+ if ('acRead' in tagsAndDates && tagsAndDates.acRead != null) {
1395
+ data.acRead = tagsAndDates.acRead;
1396
+ delete sdata.acRead;
1397
+ }
1398
+ if ('acWrite' in tagsAndDates && tagsAndDates.acWrite != null) {
1399
+ data.acWrite = tagsAndDates.acWrite;
1400
+ delete sdata.acWrite;
1401
+ }
1402
+ if ('acFull' in tagsAndDates && tagsAndDates.acFull != null) {
1403
+ data.acFull = tagsAndDates.acFull;
1404
+ delete sdata.acFull;
1405
+ }
1003
1406
  }
1004
- entity.guid = guid;
1005
- entity.cdate = cdate;
1006
- entity.mdate = mdate;
1007
- entity.tags = tags;
1008
- this.putDataCounter++;
1009
- if (this.putDataCounter == 100) {
1010
- throw new Error('Infinite loop detected in Entity loading.');
1407
+
1408
+ if (options.return === 'object') {
1409
+ (entities as EntityObjectType<T>[]).push({
1410
+ guid,
1411
+ cdate,
1412
+ mdate,
1413
+ tags,
1414
+ ...data,
1415
+ ...Object.fromEntries(
1416
+ Object.entries(sdata).map(([name, value]) => [
1417
+ name,
1418
+ JSON.parse(value),
1419
+ ]),
1420
+ ),
1421
+ } as EntityObjectType<T>);
1422
+ } else {
1423
+ const entity =
1424
+ EntityClass.factorySync() as EntityInstanceType<T>;
1425
+ entity.$nymph = this.nymph;
1426
+ if (options.skipAc != null) {
1427
+ entity.$useSkipAc(!!options.skipAc);
1428
+ }
1429
+ entity.guid = guid;
1430
+ entity.cdate = cdate;
1431
+ entity.mdate = mdate;
1432
+ entity.tags = tags;
1433
+ this.putDataCounter++;
1434
+ if (this.putDataCounter == 100) {
1435
+ throw new Error('Infinite loop detected in Entity loading.');
1436
+ }
1437
+ entity.$putData(data, sdata, 'server');
1438
+ this.putDataCounter--;
1439
+ entities.push(entity);
1011
1440
  }
1012
- entity.$putData(data, sdata);
1013
- this.putDataCounter--;
1014
1441
  if (this.nymph.config.cache) {
1015
1442
  this.pushCache(guid, cdate, mdate, tags, data, sdata);
1016
1443
  }
1017
- // @ts-ignore: ts doesn't know the return type here.
1018
- entities.push(entity);
1019
1444
  }
1020
1445
  }
1021
1446
 
@@ -1036,88 +1461,115 @@ export default abstract class NymphDriver {
1036
1461
 
1037
1462
  protected async saveEntityRowLike(
1038
1463
  entity: EntityInterface,
1039
- saveNewEntityCallback: (
1040
- entity: EntityInterface,
1041
- guid: string,
1042
- tags: string[],
1043
- data: EntityData,
1044
- sdata: SerializedEntityData,
1045
- cdate: number,
1046
- etype: string
1047
- ) => Promise<boolean>,
1048
- saveExistingEntityCallback: (
1049
- entity: EntityInterface,
1050
- guid: string,
1051
- tags: string[],
1052
- data: EntityData,
1053
- sdata: SerializedEntityData,
1054
- mdate: number,
1055
- etype: string
1056
- ) => Promise<boolean>,
1464
+ saveNewEntityCallback: (data: {
1465
+ entity: EntityInterface;
1466
+ guid: string;
1467
+ tags: string[];
1468
+ data: EntityData;
1469
+ sdata: SerializedEntityData;
1470
+ uniques: string[];
1471
+ cdate: number;
1472
+ etype: string;
1473
+ }) => Promise<boolean>,
1474
+ saveExistingEntityCallback: (data: {
1475
+ entity: EntityInterface;
1476
+ guid: string;
1477
+ tags: string[];
1478
+ data: EntityData;
1479
+ sdata: SerializedEntityData;
1480
+ uniques: string[];
1481
+ mdate: number;
1482
+ etype: string;
1483
+ }) => Promise<boolean>,
1057
1484
  startTransactionCallback: (() => Promise<void>) | null = null,
1058
1485
  commitTransactionCallback:
1059
1486
  | ((success: boolean) => Promise<boolean>)
1060
- | null = null
1487
+ | null = null,
1061
1488
  ) {
1489
+ const originalGuid = entity.guid;
1490
+ const originalCdate = entity.cdate;
1491
+ const originalMdate = entity.mdate;
1062
1492
  // Get a modified date.
1063
- const mdate = Date.now();
1493
+ let mdate = Date.now();
1494
+ if (entity.mdate != null) {
1495
+ if (this.nymph.config.updateMDate === false) {
1496
+ mdate = entity.mdate;
1497
+ } else if (typeof this.nymph.config.updateMDate === 'number') {
1498
+ mdate = entity.mdate + this.nymph.config.updateMDate;
1499
+ }
1500
+ }
1064
1501
  const tags = difference(entity.tags, ['']);
1065
1502
  const data = entity.$getData();
1066
1503
  const sdata = entity.$getSData();
1504
+ const uniques = await entity.$getUniques();
1067
1505
  const EntityClass = entity.constructor as EntityConstructor;
1068
1506
  const etype = EntityClass.ETYPE;
1069
1507
  if (startTransactionCallback) {
1070
1508
  await startTransactionCallback();
1071
1509
  }
1072
1510
  let success = false;
1073
- if (entity.guid == null) {
1074
- const cdate = mdate;
1075
- const newId = guid();
1076
- success = await saveNewEntityCallback(
1077
- entity,
1078
- newId,
1079
- tags,
1080
- data,
1081
- sdata,
1082
- cdate,
1083
- etype
1084
- );
1085
- if (success) {
1086
- entity.guid = newId;
1087
- entity.cdate = cdate;
1088
- entity.mdate = mdate;
1511
+ try {
1512
+ if (entity.guid == null) {
1513
+ const cdate = mdate;
1514
+ const newId = entity.$getGuaranteedGUID();
1515
+ success = await saveNewEntityCallback({
1516
+ entity,
1517
+ guid: newId,
1518
+ tags,
1519
+ data,
1520
+ sdata,
1521
+ uniques,
1522
+ cdate,
1523
+ etype,
1524
+ });
1525
+ if (success) {
1526
+ entity.guid = newId;
1527
+ entity.cdate = cdate;
1528
+ entity.mdate = mdate;
1529
+ }
1530
+ } else {
1531
+ // Removed any cached versions of this entity.
1532
+ if (this.nymph.config.cache) {
1533
+ this.cleanCache(entity.guid);
1534
+ }
1535
+ success = await saveExistingEntityCallback({
1536
+ entity,
1537
+ guid: entity.guid,
1538
+ tags,
1539
+ data,
1540
+ sdata,
1541
+ uniques,
1542
+ mdate,
1543
+ etype,
1544
+ });
1545
+ if (success) {
1546
+ entity.mdate = mdate;
1547
+ }
1089
1548
  }
1090
- } else {
1091
- // Removed any cached versions of this entity.
1092
- if (this.nymph.config.cache) {
1093
- this.cleanCache(entity.guid);
1549
+ if (commitTransactionCallback) {
1550
+ success = await commitTransactionCallback(success);
1094
1551
  }
1095
- success = await saveExistingEntityCallback(
1096
- entity,
1097
- entity.guid,
1098
- tags,
1099
- data,
1100
- sdata,
1101
- mdate,
1102
- etype
1103
- );
1104
- if (success) {
1105
- entity.mdate = mdate;
1552
+ // Cache the entity.
1553
+ if (success && this.nymph.config.cache) {
1554
+ this.pushCache(
1555
+ entity.guid as string,
1556
+ entity.cdate as number,
1557
+ entity.mdate as number,
1558
+ entity.tags,
1559
+ data,
1560
+ sdata,
1561
+ );
1106
1562
  }
1563
+ } catch (e: any) {
1564
+ entity.guid = originalGuid;
1565
+ entity.cdate = originalCdate;
1566
+ entity.mdate = originalMdate;
1567
+ throw e;
1107
1568
  }
1108
- if (commitTransactionCallback) {
1109
- success = await commitTransactionCallback(success);
1110
- }
1111
- // Cache the entity.
1112
- if (success && this.nymph.config.cache) {
1113
- this.pushCache(
1114
- entity.guid as string,
1115
- entity.cdate as number,
1116
- entity.mdate as number,
1117
- entity.tags,
1118
- data,
1119
- sdata
1120
- );
1569
+ if (!success) {
1570
+ entity.guid = originalGuid;
1571
+ entity.cdate = originalCdate;
1572
+ entity.mdate = originalMdate;
1121
1573
  }
1122
1574
  return success;
1123
1575
  }
@@ -1126,35 +1578,30 @@ export default abstract class NymphDriver {
1126
1578
  * Pull an entity from the cache.
1127
1579
  *
1128
1580
  * @param guid The entity's GUID.
1129
- * @param className The entity's class.
1130
- * @param useSkipAc Whether to tell the entity to use skip_ac.
1131
- * @returns The entity or null if it's not cached.
1581
+ * @returns The entity's data or null if it's not cached.
1132
1582
  */
1133
- protected pullCache<T extends EntityInterface = EntityInterface>(
1134
- guid: string,
1135
- className: string,
1136
- useSkipAc = false
1137
- ): T | null {
1583
+ protected pullCache(guid: string): {
1584
+ guid: string;
1585
+ cdate: number;
1586
+ mdate: number;
1587
+ tags: string[];
1588
+ data: EntityData;
1589
+ sdata: SerializedEntityData;
1590
+ } | null {
1138
1591
  // Increment the entity access count.
1139
1592
  if (!(guid in this.entityCount)) {
1140
1593
  this.entityCount[guid] = 0;
1141
1594
  }
1142
1595
  this.entityCount[guid]++;
1143
1596
  if (guid in this.entityCache) {
1144
- const entity = this.nymph.getEntityClass(className).factorySync() as T;
1145
- if (entity) {
1146
- entity.$nymph = this.nymph;
1147
- entity.$useSkipAc(!!useSkipAc);
1148
- entity.guid = guid;
1149
- entity.cdate = this.entityCache[guid]['cdate'];
1150
- entity.mdate = this.entityCache[guid]['mdate'];
1151
- entity.tags = this.entityCache[guid]['tags'];
1152
- entity.$putData(
1153
- this.entityCache[guid]['data'],
1154
- this.entityCache[guid]['sdata']
1155
- );
1156
- return entity;
1157
- }
1597
+ return {
1598
+ guid,
1599
+ cdate: this.entityCache[guid]['cdate'],
1600
+ mdate: this.entityCache[guid]['mdate'],
1601
+ tags: this.entityCache[guid]['tags'],
1602
+ data: this.entityCache[guid]['data'],
1603
+ sdata: this.entityCache[guid]['sdata'],
1604
+ };
1158
1605
  }
1159
1606
  return null;
1160
1607
  }
@@ -1175,7 +1622,7 @@ export default abstract class NymphDriver {
1175
1622
  mdate: number,
1176
1623
  tags: string[],
1177
1624
  data: EntityData,
1178
- sdata: SerializedEntityData
1625
+ sdata: SerializedEntityData,
1179
1626
  ) {
1180
1627
  if (guid == null) {
1181
1628
  return;
@@ -1191,11 +1638,11 @@ export default abstract class NymphDriver {
1191
1638
  }
1192
1639
  // Cache the entity.
1193
1640
  this.entityCache[guid] = {
1194
- cdate: cdate,
1195
- mdate: mdate,
1196
- tags: tags,
1197
- data: data,
1198
- sdata: sdata,
1641
+ cdate,
1642
+ mdate,
1643
+ tags,
1644
+ data,
1645
+ sdata,
1199
1646
  };
1200
1647
  while (
1201
1648
  this.nymph.config.cacheLimit &&
@@ -1204,7 +1651,7 @@ export default abstract class NymphDriver {
1204
1651
  // Find which entity has been accessed the least.
1205
1652
  const least =
1206
1653
  Object.entries(this.entityCount)
1207
- .sort(([guidA, countA], [guidB, countB]) => countA - countB)
1654
+ .sort(([_guidA, countA], [_guidB, countB]) => countA - countB)
1208
1655
  .pop()?.[0] ?? null;
1209
1656
  if (least == null) {
1210
1657
  // This should never happen.