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

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