@nymphjs/nymph 1.0.0-beta.1 → 1.0.0-beta.100

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 (96) hide show
  1. package/CHANGELOG.md +535 -0
  2. package/README.md +14 -22
  3. package/dist/Entity.d.ts +293 -10
  4. package/dist/Entity.js +571 -149
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Entity.test.js +67 -26
  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 +497 -13
  11. package/dist/Nymph.js +761 -188
  12. package/dist/Nymph.js.map +1 -1
  13. package/dist/Nymph.types.d.ts +95 -26
  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 +181 -43
  25. package/dist/driver/NymphDriver.js +532 -285
  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 +939 -219
  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/InvalidParametersError.js +1 -5
  46. package/dist/errors/InvalidParametersError.js.map +1 -1
  47. package/dist/errors/InvalidStateError.d.ts +3 -0
  48. package/dist/errors/InvalidStateError.js +7 -0
  49. package/dist/errors/InvalidStateError.js.map +1 -0
  50. package/dist/errors/NotConfiguredError.js +1 -5
  51. package/dist/errors/NotConfiguredError.js.map +1 -1
  52. package/dist/errors/QueryFailedError.js +2 -5
  53. package/dist/errors/QueryFailedError.js.map +1 -1
  54. package/dist/errors/UnableToConnectError.js +1 -5
  55. package/dist/errors/UnableToConnectError.js.map +1 -1
  56. package/dist/errors/index.d.ts +10 -8
  57. package/dist/errors/index.js +10 -24
  58. package/dist/errors/index.js.map +1 -1
  59. package/dist/index.d.ts +11 -11
  60. package/dist/index.js +14 -34
  61. package/dist/index.js.map +1 -1
  62. package/dist/nymphJoiProps.js +10 -15
  63. package/dist/nymphJoiProps.js.map +1 -1
  64. package/dist/testArtifacts.d.ts +19 -6
  65. package/dist/testArtifacts.js +32 -37
  66. package/dist/testArtifacts.js.map +1 -1
  67. package/dist/testMocks.d.ts +28 -11
  68. package/dist/testMocks.js +44 -28
  69. package/dist/testMocks.js.map +1 -1
  70. package/dist/utils.d.ts +4 -4
  71. package/dist/utils.js +40 -29
  72. package/dist/utils.js.map +1 -1
  73. package/jest.config.js +11 -2
  74. package/package.json +24 -20
  75. package/src/Entity.test.ts +63 -13
  76. package/src/Entity.ts +498 -156
  77. package/src/Entity.types.ts +72 -74
  78. package/src/Nymph.ts +904 -336
  79. package/src/Nymph.types.ts +179 -44
  80. package/src/conf/d.ts +25 -0
  81. package/src/conf/defaults.ts +34 -1
  82. package/src/conf/index.ts +2 -2
  83. package/src/driver/NymphDriver.ts +650 -460
  84. package/src/driver/index.ts +2 -2
  85. package/src/driver/testArtifacts.ts +966 -196
  86. package/src/errors/EntityUniqueConstraintError.ts +6 -0
  87. package/src/errors/InvalidStateError.ts +6 -0
  88. package/src/errors/index.ts +12 -8
  89. package/src/index.ts +11 -11
  90. package/src/nymphJoiProps.ts +2 -2
  91. package/src/testArtifacts.ts +20 -20
  92. package/src/testMocks.ts +85 -37
  93. package/src/type/locutus.d.ts +3 -0
  94. package/src/utils.ts +39 -18
  95. package/tsconfig.json +5 -3
  96. 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
  */
@@ -54,22 +60,49 @@ export default abstract class NymphDriver {
54
60
  */
55
61
  private putDataCounter = 0;
56
62
 
63
+ /**
64
+ * This is used internally by Nymph. Don't call it yourself.
65
+ *
66
+ * @returns A clone of this instance.
67
+ */
68
+ abstract clone(): NymphDriver;
69
+
70
+ /**
71
+ * Connect to the data store.
72
+ */
57
73
  abstract connect(): Promise<boolean>;
74
+
58
75
  abstract isConnected(): boolean;
76
+ protected abstract internalTransaction(name: string): Promise<any>;
59
77
  abstract startTransaction(name: string): Promise<Nymph>;
60
78
  abstract commit(name: string): Promise<boolean>;
61
79
  abstract rollback(name: string): Promise<boolean>;
62
80
  abstract inTransaction(): Promise<boolean>;
63
81
  abstract deleteEntityByID(
64
82
  guid: string,
65
- classConstructor: EntityConstructor
83
+ classConstructor: EntityConstructor,
66
84
  ): Promise<boolean>;
67
85
  abstract deleteEntityByID(guid: string, className?: string): Promise<boolean>;
68
86
  abstract deleteUID(name: string): Promise<boolean>;
87
+
88
+ /**
89
+ * Disconnect from the data store.
90
+ */
69
91
  abstract disconnect(): Promise<boolean>;
70
- protected abstract exportEntities(
71
- writeLine: (line: string) => void
72
- ): Promise<void>;
92
+
93
+ abstract needsMigration(): Promise<'json' | 'tokens' | false>;
94
+ abstract liveMigration(migrationType: 'tokenTables'): Promise<void>;
95
+
96
+ /**
97
+ * Returns a list of the etypes stored in the DB.
98
+ */
99
+ abstract getEtypes(): Promise<string[]>;
100
+
101
+ abstract exportDataIterator(): AsyncGenerator<
102
+ { type: 'comment' | 'uid' | 'entity'; content: string },
103
+ void,
104
+ undefined | false
105
+ >;
73
106
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
74
107
  options: Options<T> & { return: 'count' },
75
108
  ...selectors: Selector[]
@@ -79,39 +112,37 @@ export default abstract class NymphDriver {
79
112
  ...selectors: Selector[]
80
113
  ): Promise<string[]>;
81
114
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
82
- options?: Options<T>,
115
+ options: Options<T> & { return: 'object' },
83
116
  ...selectors: Selector[]
84
- ): Promise<ReturnType<T['factorySync']>[]>;
117
+ ): Promise<EntityObjectType<T>[]>;
85
118
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
86
119
  options?: Options<T>,
87
120
  ...selectors: Selector[]
88
- ): Promise<ReturnType<T['factorySync']>[] | string[] | number>;
89
- protected abstract getEntitiesSync<
90
- T extends EntityConstructor = EntityConstructor
91
- >(
92
- options: Options<T> & { return: 'count' },
93
- ...selectors: Selector[]
94
- ): number;
95
- protected abstract getEntitiesSync<
96
- T extends EntityConstructor = EntityConstructor
97
- >(
98
- options: Options<T> & { return: 'guid' },
99
- ...selectors: Selector[]
100
- ): string[];
101
- protected abstract getEntitiesSync<
102
- T extends EntityConstructor = EntityConstructor
103
- >(
104
- options?: Options<T>,
105
- ...selectors: Selector[]
106
- ): ReturnType<T['factorySync']>[];
107
- protected abstract getEntitiesSync<
108
- T extends EntityConstructor = EntityConstructor
109
- >(
121
+ ): Promise<EntityInstanceType<T>[]>;
122
+ abstract getEntities<T extends EntityConstructor = EntityConstructor>(
110
123
  options?: Options<T>,
111
124
  ...selectors: Selector[]
112
- ): ReturnType<T['factorySync']>[] | string[] | number;
125
+ ): Promise<
126
+ EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] | number
127
+ >;
113
128
  abstract getUID(name: string): Promise<number | null>;
114
- abstract import(filename: string): Promise<boolean>;
129
+ abstract importEntity(entity: {
130
+ guid: string;
131
+ cdate: number;
132
+ mdate: number;
133
+ tags: string[];
134
+ sdata: SerializedEntityData;
135
+ etype: string;
136
+ }): Promise<void>;
137
+ abstract importEntityTokens(entity: {
138
+ guid: string;
139
+ cdate: number;
140
+ mdate: number;
141
+ tags: string[];
142
+ sdata: SerializedEntityData;
143
+ etype: string;
144
+ }): Promise<void>;
145
+ abstract importUID(uid: { name: string; value: number }): Promise<void>;
115
146
  abstract newUID(name: string): Promise<number | null>;
116
147
  abstract renameUID(oldName: string, newName: string): Promise<boolean>;
117
148
  abstract saveEntity(entity: EntityInterface): Promise<boolean>;
@@ -131,7 +162,7 @@ export default abstract class NymphDriver {
131
162
  protected posixRegexMatch(
132
163
  pattern: string,
133
164
  subject: string,
134
- caseInsensitive = false
165
+ caseInsensitive = false,
135
166
  ) {
136
167
  const posixClasses = [
137
168
  // '[[:<:]]',
@@ -188,9 +219,9 @@ export default abstract class NymphDriver {
188
219
  if (!fhandle) {
189
220
  throw new InvalidParametersError('Provided filename is not writeable.');
190
221
  }
191
- await this.exportEntities((line: string) => {
192
- fs.writeSync(fhandle, `${line}\n`);
193
- });
222
+ for await (let entry of this.exportDataIterator()) {
223
+ fs.writeSync(fhandle, entry.content);
224
+ }
194
225
  fs.closeSync(fhandle);
195
226
  } catch (e: any) {
196
227
  return false;
@@ -199,130 +230,157 @@ export default abstract class NymphDriver {
199
230
  }
200
231
 
201
232
  public async exportPrint() {
202
- await this.exportEntities((line: string) => {
203
- console.log(line);
204
- });
233
+ for await (let entry of this.exportDataIterator()) {
234
+ console.log(entry.content);
235
+ }
205
236
  return true;
206
237
  }
207
238
 
208
- protected async importFromFile(
209
- filename: string,
210
- saveEntityCallback: (
211
- guid: string,
212
- tags: string[],
213
- sdata: SerializedEntityData,
214
- etype: string
215
- ) => Promise<void>,
216
- saveUIDCallback: (name: string, value: number) => Promise<void>,
217
- startTransactionCallback: (() => Promise<void>) | null = null,
218
- commitTransactionCallback: (() => Promise<void>) | null = null
239
+ public async importDataIterator(
240
+ lines: Iterable<string>,
241
+ transaction?: boolean,
219
242
  ) {
220
- let rl: ReadLines;
221
- try {
222
- rl = new ReadLines(filename);
223
- } catch (e: any) {
224
- throw new InvalidParametersError('Provided filename is unreadable.');
243
+ const first = lines[Symbol.iterator]().next();
244
+
245
+ if (first.done || first.value !== '#nex2') {
246
+ throw new Error('Tried to import a file that is not a NEX v2 file.');
225
247
  }
226
- let guid: string | null = null;
227
- let line: Buffer | false = false;
228
- let sdata: SerializedEntityData = {};
229
- let tags: string[] = [];
230
- let etype = '__undefined';
231
- if (startTransactionCallback) {
232
- await startTransactionCallback();
248
+
249
+ if (transaction) {
250
+ await this.internalTransaction('nymph-import');
233
251
  }
234
- while ((line = rl.next())) {
235
- const text = line.toString('utf8');
236
- if (text.match(/^\s*#/)) {
237
- continue;
238
- }
239
- const entityMatch = text.match(
240
- /^\s*{([0-9A-Fa-f]+)}<([-\w_]+)>\[([^\]]*)\]\s*$/
241
- );
242
- const propMatch = text.match(/^\s*([^=]+)\s*=\s*(.*\S)\s*$/);
243
- const uidMatch = text.match(/^\s*<([^>]+)>\[(\d+)\]\s*$/);
244
- if (uidMatch) {
245
- // Add the UID.
246
- await saveUIDCallback(uidMatch[1], Number(uidMatch[2]));
247
- } else if (entityMatch) {
248
- // Save the current entity.
249
- if (guid) {
250
- await saveEntityCallback(guid, tags, sdata, etype);
251
- guid = null;
252
- tags = [];
253
- sdata = {};
254
- etype = '__undefined';
252
+ try {
253
+ let guid: string | null = null;
254
+ let sdata: SerializedEntityData = {};
255
+ let tags: string[] = [];
256
+ let etype = '__undefined';
257
+ for (let line of lines) {
258
+ if (line.match(/^\s*#/)) {
259
+ continue;
255
260
  }
256
- // Record the new entity's info.
257
- guid = entityMatch[1];
258
- etype = entityMatch[2];
259
- tags = entityMatch[3].split(',');
260
- } else if (propMatch) {
261
- // Add the variable to the new entity.
262
- if (guid) {
263
- sdata[propMatch[1]] = propMatch[2];
261
+ const entityMatch = line.match(
262
+ /^\s*{([0-9A-Fa-f]+)}<([-\w_]+)>\[([^\]]*)\]\s*$/,
263
+ );
264
+ const propMatch = line.match(/^\s*([^=]+)\s*=\s*(.*\S)\s*$/);
265
+ const uidMatch = line.match(/^\s*<([^>]+)>\[(\d+)\]\s*$/);
266
+ if (uidMatch) {
267
+ // Add the UID.
268
+ await this.importUID({
269
+ name: uidMatch[1],
270
+ value: Number(uidMatch[2]),
271
+ });
272
+ } else if (entityMatch) {
273
+ // Save the current entity.
274
+ if (guid) {
275
+ const cdate = Number(JSON.parse(sdata.cdate));
276
+ delete sdata.cdate;
277
+ const mdate = Number(JSON.parse(sdata.mdate));
278
+ delete sdata.mdate;
279
+ await this.importEntity({ guid, cdate, mdate, tags, sdata, etype });
280
+ guid = null;
281
+ tags = [];
282
+ sdata = {};
283
+ etype = '__undefined';
284
+ }
285
+ // Record the new entity's info.
286
+ guid = entityMatch[1];
287
+ etype = entityMatch[2];
288
+ tags = entityMatch[3].split(',');
289
+ } else if (propMatch) {
290
+ // Add the variable to the new entity.
291
+ if (guid) {
292
+ sdata[propMatch[1]] = propMatch[2];
293
+ }
264
294
  }
295
+ // Clear the entity cache.
296
+ this.entityCache = {};
265
297
  }
266
- // Clear the entity cache.
267
- this.entityCache = {};
268
- }
269
- // Save the last entity.
270
- if (guid) {
271
- await saveEntityCallback(guid, tags, sdata, etype);
272
- }
273
- if (commitTransactionCallback) {
274
- await commitTransactionCallback();
298
+ // Save the last entity.
299
+ if (guid) {
300
+ const cdate = Number(JSON.parse(sdata.cdate));
301
+ delete sdata.cdate;
302
+ const mdate = Number(JSON.parse(sdata.mdate));
303
+ delete sdata.mdate;
304
+ await this.importEntity({ guid, cdate, mdate, tags, sdata, etype });
305
+ }
306
+ if (transaction) {
307
+ await this.commit('nymph-import');
308
+ }
309
+ } catch (e: any) {
310
+ await this.rollback('nymph-import');
311
+ throw e;
275
312
  }
276
313
  return true;
277
314
  }
278
315
 
316
+ public async importData(text: string, transaction?: boolean) {
317
+ return await this.importDataIterator(text.split('\n'), transaction);
318
+ }
319
+
320
+ public async import(filename: string, transaction?: boolean) {
321
+ let rl: ReadLines;
322
+ try {
323
+ rl = new ReadLines(filename);
324
+ } catch (e: any) {
325
+ throw new InvalidParametersError('Provided filename is unreadable.');
326
+ }
327
+
328
+ const lines = {
329
+ *[Symbol.iterator]() {
330
+ let line: false | Buffer;
331
+ while ((line = rl.next())) {
332
+ yield line.toString('utf8');
333
+ }
334
+ },
335
+ };
336
+
337
+ return await this.importDataIterator(lines, transaction);
338
+ }
339
+
279
340
  public checkData(
341
+ etype: string,
280
342
  data: EntityData,
281
343
  sdata: SerializedEntityData,
282
344
  selectors: Selector[],
283
345
  guid: string | null = null,
284
- tags: string[] | null = null
346
+ tags: string[] | null = null,
285
347
  ) {
286
- const formattedSelectors = this.formatSelectors(selectors);
348
+ try {
349
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
350
+ const formattedSelectors = this.formatSelectors(selectors).selectors;
287
351
 
288
- for (const curSelector of formattedSelectors) {
289
- const type = curSelector.type;
290
- const typeIsNot = type === '!&' || type === '!|';
291
- const typeIsOr = type === '|' || type === '!|';
292
- let pass = !typeIsOr;
293
- for (const k in curSelector) {
294
- const key = k as keyof Selector;
295
- const value = curSelector[key];
352
+ for (const curSelector of formattedSelectors) {
353
+ const type = curSelector.type;
354
+ const typeIsNot = type === '!&' || type === '!|';
355
+ const typeIsOr = type === '|' || type === '!|';
356
+ let pass = !typeIsOr;
357
+ for (const k in curSelector) {
358
+ const key = k as keyof Selector;
359
+ const value = curSelector[key];
296
360
 
297
- if (key === 'type' || value == null) {
298
- continue;
299
- }
361
+ if (key === 'type' || value == null) {
362
+ continue;
363
+ }
300
364
 
301
- const clauseNot = key.substr(0, 1) === '!';
302
- if (key === 'selector' || key === '!selector') {
303
- const tmpArr = (Array.isArray(value) ? value : [value]) as Selector[];
304
- pass = xor(
305
- this.checkData(data, sdata, tmpArr, guid, tags),
306
- xor(typeIsNot, clauseNot)
307
- );
308
- } else {
309
- if (key === 'qref' || key === '!qref') {
310
- throw new Error("Can't use checkData on qref clauses.");
365
+ const clauseNot = key.substring(0, 1) === '!';
366
+ if (key === 'selector' || key === '!selector') {
367
+ const tmpArr = (
368
+ Array.isArray(value) ? value : [value]
369
+ ) as Selector[];
370
+ pass = xor(
371
+ this.checkData(etype, data, sdata, tmpArr, guid, tags),
372
+ xor(typeIsNot, clauseNot),
373
+ );
311
374
  } else {
312
- // Check if it doesn't pass any for &, check if it
313
- // passes any for |.
314
- for (const curValue of value) {
315
- if (curValue == null) {
316
- continue;
317
- }
375
+ if (key === 'qref' || key === '!qref') {
376
+ throw new Error("Can't use checkData on qref clauses.");
377
+ } else {
378
+ // Check if it doesn't pass any for &, check if it passes any for |.
379
+ for (const curValue of value) {
380
+ if (curValue == null) {
381
+ continue;
382
+ }
318
383
 
319
- if (
320
- ((key === 'guid' || key === '!guid') && guid == null) ||
321
- ((key === 'tag' || key === '!tag') && tags == null)
322
- ) {
323
- // Skip because it has already been checked (by the query).
324
- pass = true;
325
- } else {
326
384
  const propName = (curValue as string[])[0];
327
385
 
328
386
  // Unserialize the data for this variable.
@@ -340,7 +398,7 @@ export default abstract class NymphDriver {
340
398
  case '!tag':
341
399
  pass = xor(
342
400
  (tags ?? []).indexOf(propName) !== -1,
343
- xor(typeIsNot, clauseNot)
401
+ xor(typeIsNot, clauseNot),
344
402
  );
345
403
  break;
346
404
  case 'defined':
@@ -351,7 +409,7 @@ export default abstract class NymphDriver {
351
409
  case '!truthy':
352
410
  pass = xor(
353
411
  propName in data && data[propName],
354
- xor(typeIsNot, clauseNot)
412
+ xor(typeIsNot, clauseNot),
355
413
  );
356
414
  break;
357
415
  case 'ref':
@@ -361,9 +419,9 @@ export default abstract class NymphDriver {
361
419
  propName in data &&
362
420
  this.entityReferenceSearch(
363
421
  data[propName],
364
- testRefValue
422
+ testRefValue,
365
423
  ),
366
- xor(typeIsNot, clauseNot)
424
+ xor(typeIsNot, clauseNot),
367
425
  );
368
426
  break;
369
427
  case 'equal':
@@ -375,7 +433,7 @@ export default abstract class NymphDriver {
375
433
  propName in data &&
376
434
  JSON.stringify(data[propName]) ===
377
435
  JSON.stringify(testEqualValue),
378
- xor(typeIsNot, clauseNot)
436
+ xor(typeIsNot, clauseNot),
379
437
  );
380
438
  break;
381
439
  case 'contain':
@@ -386,9 +444,9 @@ export default abstract class NymphDriver {
386
444
  pass = xor(
387
445
  propName in data &&
388
446
  JSON.stringify(data[propName]).indexOf(
389
- JSON.stringify(testContainValue)
447
+ JSON.stringify(testContainValue),
390
448
  ) !== -1,
391
- xor(typeIsNot, clauseNot)
449
+ xor(typeIsNot, clauseNot),
392
450
  );
393
451
  break;
394
452
  case 'like':
@@ -403,9 +461,27 @@ export default abstract class NymphDriver {
403
461
  escapeRegExp(testLikeValue)
404
462
  .replace('%', () => '.*')
405
463
  .replace('_', () => '.') +
406
- '$'
464
+ '$',
465
+ ).test(data[propName]),
466
+ xor(typeIsNot, clauseNot),
467
+ );
468
+ break;
469
+ case 'ilike':
470
+ case '!ilike':
471
+ const testILikeValue = (
472
+ curValue as [string, string]
473
+ )[1] as string;
474
+ pass = xor(
475
+ propName in data &&
476
+ new RegExp(
477
+ '^' +
478
+ escapeRegExp(testILikeValue)
479
+ .replace('%', () => '.*')
480
+ .replace('_', () => '.') +
481
+ '$',
482
+ 'i',
407
483
  ).test(data[propName]),
408
- xor(typeIsNot, clauseNot)
484
+ xor(typeIsNot, clauseNot),
409
485
  );
410
486
  break;
411
487
  case 'match':
@@ -417,7 +493,7 @@ export default abstract class NymphDriver {
417
493
  pass = xor(
418
494
  propName in data &&
419
495
  this.posixRegexMatch(testMatchValue, data[propName]),
420
- xor(typeIsNot, clauseNot)
496
+ xor(typeIsNot, clauseNot),
421
497
  );
422
498
  break;
423
499
  case 'imatch':
@@ -431,9 +507,28 @@ export default abstract class NymphDriver {
431
507
  this.posixRegexMatch(
432
508
  testIMatchValue,
433
509
  data[propName],
434
- true
510
+ true,
511
+ ),
512
+ xor(typeIsNot, clauseNot),
513
+ );
514
+ break;
515
+ case 'search':
516
+ case '!search':
517
+ const testSearchValue = (
518
+ curValue as [string, string]
519
+ )[1] as string;
520
+ const tokenString =
521
+ propName in data
522
+ ? (EntityClass.getFTSText(propName, data[propName]) ??
523
+ '')
524
+ : '';
525
+ pass = xor(
526
+ propName in data &&
527
+ this.tokenizer.searchString(
528
+ testSearchValue,
529
+ tokenString,
435
530
  ),
436
- xor(typeIsNot, clauseNot)
531
+ xor(typeIsNot, clauseNot),
437
532
  );
438
533
  break;
439
534
  case 'gt':
@@ -443,7 +538,7 @@ export default abstract class NymphDriver {
443
538
  )[1] as number;
444
539
  pass = xor(
445
540
  propName in data && data[propName] > testGTValue,
446
- xor(typeIsNot, clauseNot)
541
+ xor(typeIsNot, clauseNot),
447
542
  );
448
543
  break;
449
544
  case 'gte':
@@ -453,7 +548,7 @@ export default abstract class NymphDriver {
453
548
  )[1] as number;
454
549
  pass = xor(
455
550
  propName in data && data[propName] >= testGTEValue,
456
- xor(typeIsNot, clauseNot)
551
+ xor(typeIsNot, clauseNot),
457
552
  );
458
553
  break;
459
554
  case 'lt':
@@ -463,7 +558,7 @@ export default abstract class NymphDriver {
463
558
  )[1] as number;
464
559
  pass = xor(
465
560
  propName in data && data[propName] < testLTValue,
466
- xor(typeIsNot, clauseNot)
561
+ xor(typeIsNot, clauseNot),
467
562
  );
468
563
  break;
469
564
  case 'lte':
@@ -473,26 +568,33 @@ export default abstract class NymphDriver {
473
568
  )[1] as number;
474
569
  pass = xor(
475
570
  propName in data && data[propName] <= testLTEValue,
476
- xor(typeIsNot, clauseNot)
571
+ xor(typeIsNot, clauseNot),
477
572
  );
478
573
  break;
479
574
  }
480
- }
481
- if (!xor(typeIsOr, pass)) {
482
- break;
575
+
576
+ if (!xor(typeIsOr, pass)) {
577
+ break;
578
+ }
483
579
  }
484
580
  }
485
581
  }
582
+ if (!xor(typeIsOr, pass)) {
583
+ break;
584
+ }
486
585
  }
487
- if (!xor(typeIsOr, pass)) {
488
- break;
586
+ if (!pass) {
587
+ return false;
489
588
  }
490
589
  }
491
- if (!pass) {
492
- return false;
493
- }
590
+ return true;
591
+ } catch (e: any) {
592
+ this.nymph.config.debugError(
593
+ 'nymph',
594
+ `Failed to check entity data: ${e}`,
595
+ );
596
+ throw e;
494
597
  }
495
- return true;
496
598
  }
497
599
 
498
600
  /**
@@ -531,7 +633,7 @@ export default abstract class NymphDriver {
531
633
  | EntityInterface
532
634
  | EntityReference
533
635
  | string
534
- | (EntityInterface | EntityReference | string)[]
636
+ | (EntityInterface | EntityReference | string)[],
535
637
  ) {
536
638
  // Get the GUID, if the passed $entity is an object.
537
639
  const guids: string[] = [];
@@ -562,7 +664,7 @@ export default abstract class NymphDriver {
562
664
  return true;
563
665
  }
564
666
  // Search through arrays and objects looking for the reference.
565
- if (Array.isArray(value) || typeof value === 'object') {
667
+ if (value != null && (Array.isArray(value) || typeof value === 'object')) {
566
668
  for (const key in value) {
567
669
  if (this.entityReferenceSearch(value[key], guids)) {
568
670
  return true;
@@ -574,9 +676,13 @@ export default abstract class NymphDriver {
574
676
 
575
677
  public formatSelectors(
576
678
  selectors: Selector[],
577
- options: Options = {}
578
- ): FormattedSelector[] {
679
+ options: Options = {},
680
+ ): {
681
+ selectors: FormattedSelector[];
682
+ qrefs: [Options, ...FormattedSelector[]][];
683
+ } {
579
684
  const newSelectors: FormattedSelector[] = [];
685
+ const qrefs: [Options, ...FormattedSelector[]][] = [];
580
686
 
581
687
  for (const curSelector of selectors) {
582
688
  const newSelector: FormattedSelector = {
@@ -605,23 +711,27 @@ export default abstract class NymphDriver {
605
711
  for (let i = 0; i < tmpArr.length; i++) {
606
712
  const name = tmpArr[i][0];
607
713
  const [qrefOptions, ...qrefSelectors] = tmpArr[i][1];
608
- const QrefEntityClass =
609
- typeof qrefOptions.class === 'string'
610
- ? this.nymph.getEntityClass(qrefOptions.class)
611
- : qrefOptions.class ?? this.nymph.getEntityClass('Entity');
714
+ const QrefEntityClass = qrefOptions.class
715
+ ? this.nymph.getEntityClass(qrefOptions.class)
716
+ : this.nymph.getEntityClass('Entity');
612
717
  const newOptions = {
613
718
  ...qrefOptions,
614
719
  class: QrefEntityClass,
615
720
  source: options.source,
616
721
  };
617
722
  const newSelectors = this.formatSelectors(qrefSelectors, options);
618
- this.nymph.runQueryCallbacks(newOptions, newSelectors);
619
- formatArr[i] = [name, [newOptions, ...newSelectors]];
723
+ qrefs.push(
724
+ [newOptions, ...newSelectors.selectors],
725
+ ...newSelectors.qrefs,
726
+ );
727
+ formatArr[i] = [name, [newOptions, ...newSelectors.selectors]];
620
728
  }
621
729
  newSelector[key] = formatArr;
622
730
  } else if (key === 'selector' || key === '!selector') {
623
731
  const tmpArr = (Array.isArray(value) ? value : [value]) as Selector[];
624
- newSelector[key] = this.formatSelectors(tmpArr, options);
732
+ const newSelectors = this.formatSelectors(tmpArr, options);
733
+ newSelector[key] = newSelectors.selectors;
734
+ qrefs.push(...newSelectors.qrefs);
625
735
  } else if (!Array.isArray(value)) {
626
736
  // @ts-ignore: ts doesn't know what value is here.
627
737
  newSelector[key] = [[value]];
@@ -655,17 +765,17 @@ export default abstract class NymphDriver {
655
765
  newSelectors.push(newSelector);
656
766
  }
657
767
 
658
- return newSelectors;
768
+ return { selectors: newSelectors, qrefs };
659
769
  }
660
770
 
661
771
  protected iterateSelectorsForQuery(
662
772
  selectors: FormattedSelector[],
663
- callback: (
664
- key: string,
665
- value: any,
666
- typeIsOr: boolean,
667
- typeIsNot: boolean
668
- ) => string
773
+ callback: (data: {
774
+ key: string;
775
+ value: any;
776
+ typeIsOr: boolean;
777
+ typeIsNot: boolean;
778
+ }) => string,
669
779
  ) {
670
780
  const queryParts = [];
671
781
  for (const curSelector of selectors) {
@@ -678,7 +788,7 @@ export default abstract class NymphDriver {
678
788
  if (key === 'type') {
679
789
  continue;
680
790
  }
681
- let curQuery = callback(key, value, typeIsOr, typeIsNot);
791
+ let curQuery = callback({ key, value, typeIsOr, typeIsNot });
682
792
  if (curQuery) {
683
793
  if (curSelectorQuery) {
684
794
  curSelectorQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -694,71 +804,14 @@ export default abstract class NymphDriver {
694
804
  return queryParts;
695
805
  }
696
806
 
697
- /**
698
- * Get the first entity to match all options/selectors, synchronously.
699
- *
700
- * This should really only be used internally by the Entity class. It's a bad
701
- * idea to get entities synchronously. The Entity class uses this to load
702
- * sleeping references.
703
- *
704
- * @param options The options.
705
- * @param selectors Unlimited optional selectors to search for, or a single GUID. If none are given, all entities are searched for the given options.
706
- * @returns An entity, or null on failure or nothing found.
707
- */
708
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
709
- options: Options<T> & { return: 'count' },
710
- ...selectors: Selector[]
711
- ): number;
712
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
713
- options: Options<T> & { return: 'guid' },
714
- ...selectors: Selector[]
715
- ): string | null;
716
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
717
- options: Options<T>,
718
- ...selectors: Selector[]
719
- ): ReturnType<T['factorySync']> | null;
720
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
721
- options: Options<T> & { return: 'count' },
722
- guid: string
723
- ): number;
724
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
725
- options: Options<T> & { return: 'guid' },
726
- guid: string
727
- ): string | null;
728
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
729
- options: Options<T>,
730
- guid: string
731
- ): ReturnType<T['factorySync']> | null;
732
- public getEntitySync<T extends EntityConstructor = EntityConstructor>(
733
- options: Options<T> = {},
734
- ...selectors: Selector[] | string[]
735
- ): ReturnType<T['factorySync']> | string | number | null {
736
- // Set up options and selectors.
737
- if (typeof selectors[0] === 'string') {
738
- selectors = [{ type: '&', guid: selectors[0] }];
739
- }
740
- options.limit = 1;
741
- const entities = this.getEntitiesSync(
742
- options,
743
- ...(selectors as Selector[])
744
- );
745
- if (options.return === 'count') {
746
- return entities as unknown as number;
747
- }
748
- if (!entities || !entities.length) {
749
- return null;
750
- }
751
- return entities[0];
752
- }
753
-
754
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
807
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
755
808
  options: Options<T> & { return: 'count' },
756
809
  selectors: Selector[],
757
- performQueryCallback: (
758
- options: Options<T>,
759
- formattedSelectors: FormattedSelector[],
760
- etype: string
761
- ) => {
810
+ performQueryCallback: (data: {
811
+ options: Options<T>;
812
+ selectors: FormattedSelector[];
813
+ etype: string;
814
+ }) => {
762
815
  result: any;
763
816
  },
764
817
  rowFetchCallback: () => any,
@@ -773,16 +826,16 @@ export default abstract class NymphDriver {
773
826
  getDataNameAndSValueCallback: (row: any) => {
774
827
  name: string;
775
828
  svalue: string;
776
- }
777
- ): { result: any; process: () => number };
778
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
829
+ },
830
+ ): { result: any; process: () => number | Error };
831
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
779
832
  options: Options<T> & { return: 'guid' },
780
833
  selectors: Selector[],
781
- performQueryCallback: (
782
- options: Options<T>,
783
- formattedSelectors: FormattedSelector[],
784
- etype: string
785
- ) => {
834
+ performQueryCallback: (data: {
835
+ options: Options<T>;
836
+ selectors: FormattedSelector[];
837
+ etype: string;
838
+ }) => {
786
839
  result: any;
787
840
  },
788
841
  rowFetchCallback: () => any,
@@ -797,16 +850,40 @@ export default abstract class NymphDriver {
797
850
  getDataNameAndSValueCallback: (row: any) => {
798
851
  name: string;
799
852
  svalue: string;
800
- }
801
- ): { result: any; process: () => string[] };
802
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
853
+ },
854
+ ): { result: any; process: () => string[] | Error };
855
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
856
+ options: Options<T> & { return: 'object' },
857
+ selectors: Selector[],
858
+ performQueryCallback: (data: {
859
+ options: Options<T>;
860
+ selectors: FormattedSelector[];
861
+ etype: string;
862
+ }) => {
863
+ result: any;
864
+ },
865
+ rowFetchCallback: () => any,
866
+ freeResultCallback: () => void,
867
+ getCountCallback: (row: any) => number,
868
+ getGUIDCallback: (row: any) => string,
869
+ getTagsAndDatesCallback: (row: any) => {
870
+ tags: string[];
871
+ cdate: number;
872
+ mdate: number;
873
+ },
874
+ getDataNameAndSValueCallback: (row: any) => {
875
+ name: string;
876
+ svalue: string;
877
+ },
878
+ ): { result: any; process: () => EntityObjectType<T>[] | Error };
879
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
803
880
  options: Options<T>,
804
881
  selectors: Selector[],
805
- performQueryCallback: (
806
- options: Options<T>,
807
- formattedSelectors: FormattedSelector[],
808
- etype: string
809
- ) => {
882
+ performQueryCallback: (data: {
883
+ options: Options<T>;
884
+ selectors: FormattedSelector[];
885
+ etype: string;
886
+ }) => {
810
887
  result: any;
811
888
  },
812
889
  rowFetchCallback: () => any,
@@ -821,16 +898,19 @@ export default abstract class NymphDriver {
821
898
  getDataNameAndSValueCallback: (row: any) => {
822
899
  name: string;
823
900
  svalue: string;
824
- }
825
- ): { result: any; process: () => ReturnType<T['factorySync']>[] };
826
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
901
+ },
902
+ ): {
903
+ result: any;
904
+ process: () => EntityInstanceType<T>[] | Error;
905
+ };
906
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
827
907
  options: Options<T>,
828
908
  selectors: Selector[],
829
- performQueryCallback: (
830
- options: Options<T>,
831
- formattedSelectors: FormattedSelector[],
832
- etype: string
833
- ) => {
909
+ performQueryCallback: (data: {
910
+ options: Options<T>;
911
+ selectors: FormattedSelector[];
912
+ etype: string;
913
+ }) => {
834
914
  result: any;
835
915
  },
836
916
  rowFetchCallback: () => any,
@@ -845,10 +925,15 @@ export default abstract class NymphDriver {
845
925
  getDataNameAndSValueCallback: (row: any) => {
846
926
  name: string;
847
927
  svalue: string;
848
- }
928
+ },
849
929
  ): {
850
930
  result: any;
851
- process: () => ReturnType<T['factorySync']>[] | string[] | number;
931
+ process: () =>
932
+ | EntityInstanceType<T>[]
933
+ | EntityObjectType<T>[]
934
+ | string[]
935
+ | number
936
+ | Error;
852
937
  } {
853
938
  if (!this.isConnected()) {
854
939
  throw new UnableToConnectError('not connected to DB');
@@ -861,21 +946,29 @@ export default abstract class NymphDriver {
861
946
  ['&', '!&', '|', '!|'].indexOf(selector.type) === -1
862
947
  ) {
863
948
  throw new InvalidParametersError(
864
- 'Invalid query selector passed: ' + JSON.stringify(selector)
949
+ 'Invalid query selector passed: ' + JSON.stringify(selector),
865
950
  );
866
951
  }
867
952
  }
868
953
 
869
- let entities: ReturnType<T['factorySync']>[] | string[] = [];
954
+ let entities: EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] =
955
+ [];
870
956
  let count = 0;
871
957
  const EntityClass =
872
958
  options.class ?? (this.nymph.getEntityClass('Entity') as T);
873
959
  const etype = EntityClass.ETYPE;
874
960
 
961
+ if (options.source === 'client' && options.return === 'object') {
962
+ throw new InvalidParametersError(
963
+ 'Object return type not allowed from client.',
964
+ );
965
+ }
966
+
875
967
  // Check if the requested entity is cached.
876
968
  if (
877
969
  this.nymph.config.cache &&
878
970
  !options.skipCache &&
971
+ selectors.length &&
879
972
  'guid' in selectors[0] &&
880
973
  typeof selectors[0].guid === 'number'
881
974
  ) {
@@ -887,195 +980,297 @@ export default abstract class NymphDriver {
887
980
  (Object.keys(selectors[0]).length === 2 ||
888
981
  (Object.keys(selectors[0]).length === 3 && 'tag' in selectors[0]))
889
982
  ) {
890
- const entity = this.pullCache<ReturnType<T['factorySync']>>(
891
- selectors[0]['guid'],
892
- EntityClass.class,
893
- !!options.skipAc
894
- );
983
+ const cacheEntity = this.pullCache(selectors[0]['guid']);
984
+
895
985
  if (
896
- entity != null &&
897
- entity.guid != null &&
986
+ cacheEntity != null &&
987
+ cacheEntity.guid != null &&
898
988
  (!('tag' in selectors[0]) ||
899
- entity.hasTag(
900
- ...(Array.isArray(selectors[0].tag)
989
+ !(
990
+ Array.isArray(selectors[0].tag)
901
991
  ? selectors[0].tag
902
- : [selectors[0].tag])
903
- ))
992
+ : [selectors[0].tag]
993
+ ).find((tag) => tag == null || !cacheEntity.tags.includes(tag)))
904
994
  ) {
905
- const guid = entity.guid;
906
- return {
907
- result: Promise.resolve(null),
908
- process: () =>
909
- options.return === 'count'
910
- ? guid
911
- ? 1
912
- : 0
913
- : options.return === 'guid'
914
- ? [guid]
915
- : [entity],
916
- };
995
+ // Return the value from the cache.
996
+ const { guid, cdate, mdate, tags, data, sdata } = cacheEntity;
997
+
998
+ if (options.return === 'count') {
999
+ return {
1000
+ result: Promise.resolve(null),
1001
+ process: () => 1,
1002
+ };
1003
+ } else if (options.return === 'guid') {
1004
+ return {
1005
+ result: Promise.resolve(null),
1006
+ process: () => [guid],
1007
+ };
1008
+ } else if (options.return === 'object') {
1009
+ return {
1010
+ result: Promise.resolve(null),
1011
+ process: () => [
1012
+ {
1013
+ guid,
1014
+ cdate,
1015
+ mdate,
1016
+ tags,
1017
+ ...data,
1018
+ ...Object.fromEntries(
1019
+ Object.entries(sdata).map(([name, value]) => [
1020
+ name,
1021
+ JSON.parse(value),
1022
+ ]),
1023
+ ),
1024
+ } as EntityObjectType<T>,
1025
+ ],
1026
+ };
1027
+ } else {
1028
+ const entity = this.nymph
1029
+ .getEntityClass(EntityClass.class)
1030
+ .factorySync() as EntityInstanceType<T>;
1031
+ if (entity) {
1032
+ entity.$nymph = this.nymph;
1033
+ if (options.skipAc != null) {
1034
+ entity.$useSkipAc(!!options.skipAc);
1035
+ }
1036
+ entity.guid = guid;
1037
+ entity.cdate = cdate;
1038
+ entity.mdate = mdate;
1039
+ entity.tags = tags;
1040
+ entity.$putData(data, sdata, 'server');
1041
+ return {
1042
+ result: Promise.resolve(null),
1043
+ process: () => [entity],
1044
+ };
1045
+ }
1046
+
1047
+ return {
1048
+ result: Promise.resolve(null),
1049
+ process: () => [],
1050
+ };
1051
+ }
917
1052
  }
918
1053
  }
919
1054
  }
920
1055
 
921
- const formattedSelectors = this.formatSelectors(selectors, options);
922
- this.nymph.runQueryCallbacks(options, formattedSelectors);
923
- const { result } = performQueryCallback(options, formattedSelectors, etype);
1056
+ try {
1057
+ const formattedSelectors = this.formatSelectors(selectors, options);
1058
+ this.nymph.runQueryCallbacks(options, formattedSelectors.selectors);
1059
+ for (let i = 0; i < formattedSelectors.qrefs.length; i++) {
1060
+ const [options, ...selectors] = formattedSelectors.qrefs[i];
1061
+ this.nymph.runQueryCallbacks(options, selectors);
1062
+ formattedSelectors.qrefs[i] = [options, ...selectors];
1063
+ }
1064
+ const { result } = performQueryCallback({
1065
+ options,
1066
+ selectors: formattedSelectors.selectors,
1067
+ etype,
1068
+ });
924
1069
 
925
- return {
926
- result,
927
- process: () => {
928
- let row = rowFetchCallback();
929
- if (options.return === 'count') {
930
- while (row != null) {
931
- count += getCountCallback(row);
932
- row = rowFetchCallback();
933
- }
934
- } else if (options.return === 'guid') {
935
- while (row != null) {
936
- (entities as string[]).push(getGUIDCallback(row));
937
- row = rowFetchCallback();
938
- }
939
- } else {
940
- while (row != null) {
941
- const guid = getGUIDCallback(row);
942
- const tagsAndDates = getTagsAndDatesCallback(row);
943
- const tags = tagsAndDates.tags;
944
- const cdate = tagsAndDates.cdate;
945
- const mdate = tagsAndDates.mdate;
946
- let dataNameAndSValue = getDataNameAndSValueCallback(row);
947
- // Data.
948
- const data: EntityData = {};
949
- // Serialized data.
950
- const sdata: SerializedEntityData = {};
951
- if (dataNameAndSValue.name !== '') {
952
- // This do will keep going and adding the data until the
953
- // next entity is reached. $row will end on the next entity.
954
- do {
955
- dataNameAndSValue = getDataNameAndSValueCallback(row);
956
- sdata[dataNameAndSValue.name] = dataNameAndSValue.svalue;
957
- row = rowFetchCallback();
958
- } while (row != null && getGUIDCallback(row) === guid);
959
- } else {
960
- // Make sure that $row is incremented :)
1070
+ return {
1071
+ result,
1072
+ process: () => {
1073
+ let row = rowFetchCallback();
1074
+ if (options.return === 'count') {
1075
+ while (row != null) {
1076
+ count += getCountCallback(row);
961
1077
  row = rowFetchCallback();
962
1078
  }
963
- const entity = EntityClass.factorySync() as ReturnType<
964
- T['factorySync']
965
- >;
966
- entity.$nymph = this.nymph;
967
- if (options.skipAc != null) {
968
- entity.$useSkipAc(!!options.skipAc);
969
- }
970
- entity.guid = guid;
971
- entity.cdate = cdate;
972
- entity.mdate = mdate;
973
- entity.tags = tags;
974
- this.putDataCounter++;
975
- if (this.putDataCounter == 100) {
976
- throw new Error('Infinite loop detected in Entity loading.');
1079
+ } else if (options.return === 'guid') {
1080
+ while (row != null) {
1081
+ (entities as string[]).push(getGUIDCallback(row));
1082
+ row = rowFetchCallback();
977
1083
  }
978
- entity.$putData(data, sdata);
979
- this.putDataCounter--;
980
- if (this.nymph.config.cache) {
981
- this.pushCache(guid, cdate, mdate, tags, data, sdata);
1084
+ } else {
1085
+ while (row != null) {
1086
+ const guid = getGUIDCallback(row);
1087
+ const tagsAndDates = getTagsAndDatesCallback(row);
1088
+ const tags = tagsAndDates.tags;
1089
+ const cdate = tagsAndDates.cdate;
1090
+ const mdate = tagsAndDates.mdate;
1091
+ let dataNameAndSValue = getDataNameAndSValueCallback(row);
1092
+ // Data.
1093
+ const data: EntityData = {};
1094
+ // Serialized data.
1095
+ const sdata: SerializedEntityData = {};
1096
+ if (dataNameAndSValue.name !== '') {
1097
+ // This do will keep going and adding the data until the
1098
+ // next entity is reached. $row will end on the next entity.
1099
+ do {
1100
+ dataNameAndSValue = getDataNameAndSValueCallback(row);
1101
+ sdata[dataNameAndSValue.name] = dataNameAndSValue.svalue;
1102
+ row = rowFetchCallback();
1103
+ } while (row != null && getGUIDCallback(row) === guid);
1104
+ } else {
1105
+ // Make sure that $row is incremented :)
1106
+ row = rowFetchCallback();
1107
+ }
1108
+
1109
+ if (options.return === 'object') {
1110
+ (entities as EntityObjectType<T>[]).push({
1111
+ guid,
1112
+ cdate,
1113
+ mdate,
1114
+ tags,
1115
+ ...data,
1116
+ ...Object.fromEntries(
1117
+ Object.entries(sdata).map(([name, value]) => [
1118
+ name,
1119
+ JSON.parse(value),
1120
+ ]),
1121
+ ),
1122
+ } as EntityObjectType<T>);
1123
+ } else {
1124
+ const entity =
1125
+ EntityClass.factorySync() as EntityInstanceType<T>;
1126
+ entity.$nymph = this.nymph;
1127
+ if (options.skipAc != null) {
1128
+ entity.$useSkipAc(!!options.skipAc);
1129
+ }
1130
+ entity.guid = guid;
1131
+ entity.cdate = cdate;
1132
+ entity.mdate = mdate;
1133
+ entity.tags = tags;
1134
+ this.putDataCounter++;
1135
+ if (this.putDataCounter == 100) {
1136
+ throw new Error('Infinite loop detected in Entity loading.');
1137
+ }
1138
+ entity.$putData(data, sdata, 'server');
1139
+ this.putDataCounter--;
1140
+ entities.push(entity);
1141
+ }
1142
+ if (this.nymph.config.cache) {
1143
+ this.pushCache(guid, cdate, mdate, tags, data, sdata);
1144
+ }
982
1145
  }
983
- // @ts-ignore: ts doesn't know the return type here.
984
- entities.push(entity);
985
1146
  }
986
- }
987
1147
 
988
- freeResultCallback();
1148
+ freeResultCallback();
989
1149
 
990
- return options.return === 'count' ? count : entities;
991
- },
992
- };
1150
+ return options.return === 'count' ? count : entities;
1151
+ },
1152
+ };
1153
+ } catch (e: any) {
1154
+ return {
1155
+ result: Promise.resolve(e),
1156
+ process: () => {
1157
+ return e;
1158
+ },
1159
+ };
1160
+ }
993
1161
  }
994
1162
 
995
1163
  protected async saveEntityRowLike(
996
1164
  entity: EntityInterface,
997
- saveNewEntityCallback: (
998
- entity: EntityInterface,
999
- guid: string,
1000
- tags: string[],
1001
- data: EntityData,
1002
- sdata: SerializedEntityData,
1003
- cdate: number,
1004
- etype: string
1005
- ) => Promise<boolean>,
1006
- saveExistingEntityCallback: (
1007
- entity: EntityInterface,
1008
- guid: string,
1009
- tags: string[],
1010
- data: EntityData,
1011
- sdata: SerializedEntityData,
1012
- mdate: number,
1013
- etype: string
1014
- ) => Promise<boolean>,
1165
+ saveNewEntityCallback: (data: {
1166
+ entity: EntityInterface;
1167
+ guid: string;
1168
+ tags: string[];
1169
+ data: EntityData;
1170
+ sdata: SerializedEntityData;
1171
+ uniques: string[];
1172
+ cdate: number;
1173
+ etype: string;
1174
+ }) => Promise<boolean>,
1175
+ saveExistingEntityCallback: (data: {
1176
+ entity: EntityInterface;
1177
+ guid: string;
1178
+ tags: string[];
1179
+ data: EntityData;
1180
+ sdata: SerializedEntityData;
1181
+ uniques: string[];
1182
+ mdate: number;
1183
+ etype: string;
1184
+ }) => Promise<boolean>,
1015
1185
  startTransactionCallback: (() => Promise<void>) | null = null,
1016
1186
  commitTransactionCallback:
1017
1187
  | ((success: boolean) => Promise<boolean>)
1018
- | null = null
1188
+ | null = null,
1019
1189
  ) {
1190
+ const originalGuid = entity.guid;
1191
+ const originalCdate = entity.cdate;
1192
+ const originalMdate = entity.mdate;
1020
1193
  // Get a modified date.
1021
- const mdate = Date.now();
1194
+ let mdate = Date.now();
1195
+ if (entity.mdate != null) {
1196
+ if (this.nymph.config.updateMDate === false) {
1197
+ mdate = entity.mdate;
1198
+ } else if (typeof this.nymph.config.updateMDate === 'number') {
1199
+ mdate = entity.mdate + this.nymph.config.updateMDate;
1200
+ }
1201
+ }
1022
1202
  const tags = difference(entity.tags, ['']);
1023
1203
  const data = entity.$getData();
1024
1204
  const sdata = entity.$getSData();
1205
+ const uniques = await entity.$getUniques();
1025
1206
  const EntityClass = entity.constructor as EntityConstructor;
1026
1207
  const etype = EntityClass.ETYPE;
1027
1208
  if (startTransactionCallback) {
1028
1209
  await startTransactionCallback();
1029
1210
  }
1030
1211
  let success = false;
1031
- if (entity.guid == null) {
1032
- const cdate = mdate;
1033
- const newId = guid();
1034
- success = await saveNewEntityCallback(
1035
- entity,
1036
- newId,
1037
- tags,
1038
- data,
1039
- sdata,
1040
- cdate,
1041
- etype
1042
- );
1043
- if (success) {
1044
- entity.guid = newId;
1045
- entity.cdate = cdate;
1046
- entity.mdate = mdate;
1212
+ try {
1213
+ if (entity.guid == null) {
1214
+ const cdate = mdate;
1215
+ const newId = entity.$getGuaranteedGUID();
1216
+ success = await saveNewEntityCallback({
1217
+ entity,
1218
+ guid: newId,
1219
+ tags,
1220
+ data,
1221
+ sdata,
1222
+ uniques,
1223
+ cdate,
1224
+ etype,
1225
+ });
1226
+ if (success) {
1227
+ entity.guid = newId;
1228
+ entity.cdate = cdate;
1229
+ entity.mdate = mdate;
1230
+ }
1231
+ } else {
1232
+ // Removed any cached versions of this entity.
1233
+ if (this.nymph.config.cache) {
1234
+ this.cleanCache(entity.guid);
1235
+ }
1236
+ success = await saveExistingEntityCallback({
1237
+ entity,
1238
+ guid: entity.guid,
1239
+ tags,
1240
+ data,
1241
+ sdata,
1242
+ uniques,
1243
+ mdate,
1244
+ etype,
1245
+ });
1246
+ if (success) {
1247
+ entity.mdate = mdate;
1248
+ }
1047
1249
  }
1048
- } else {
1049
- // Removed any cached versions of this entity.
1050
- if (this.nymph.config.cache) {
1051
- this.cleanCache(entity.guid);
1250
+ if (commitTransactionCallback) {
1251
+ success = await commitTransactionCallback(success);
1052
1252
  }
1053
- success = await saveExistingEntityCallback(
1054
- entity,
1055
- entity.guid,
1056
- tags,
1057
- data,
1058
- sdata,
1059
- mdate,
1060
- etype
1061
- );
1062
- if (success) {
1063
- entity.mdate = mdate;
1253
+ // Cache the entity.
1254
+ if (success && this.nymph.config.cache) {
1255
+ this.pushCache(
1256
+ entity.guid as string,
1257
+ entity.cdate as number,
1258
+ entity.mdate as number,
1259
+ entity.tags,
1260
+ data,
1261
+ sdata,
1262
+ );
1064
1263
  }
1264
+ } catch (e: any) {
1265
+ entity.guid = originalGuid;
1266
+ entity.cdate = originalCdate;
1267
+ entity.mdate = originalMdate;
1268
+ throw e;
1065
1269
  }
1066
- if (commitTransactionCallback) {
1067
- success = await commitTransactionCallback(success);
1068
- }
1069
- // Cache the entity.
1070
- if (success && this.nymph.config.cache) {
1071
- this.pushCache(
1072
- entity.guid as string,
1073
- entity.cdate as number,
1074
- entity.mdate as number,
1075
- entity.tags,
1076
- data,
1077
- sdata
1078
- );
1270
+ if (!success) {
1271
+ entity.guid = originalGuid;
1272
+ entity.cdate = originalCdate;
1273
+ entity.mdate = originalMdate;
1079
1274
  }
1080
1275
  return success;
1081
1276
  }
@@ -1084,35 +1279,30 @@ export default abstract class NymphDriver {
1084
1279
  * Pull an entity from the cache.
1085
1280
  *
1086
1281
  * @param guid The entity's GUID.
1087
- * @param className The entity's class.
1088
- * @param useSkipAc Whether to tell the entity to use skip_ac.
1089
- * @returns The entity or null if it's not cached.
1282
+ * @returns The entity's data or null if it's not cached.
1090
1283
  */
1091
- protected pullCache<T extends EntityInterface = EntityInterface>(
1092
- guid: string,
1093
- className: string,
1094
- useSkipAc = false
1095
- ): T | null {
1284
+ protected pullCache(guid: string): {
1285
+ guid: string;
1286
+ cdate: number;
1287
+ mdate: number;
1288
+ tags: string[];
1289
+ data: EntityData;
1290
+ sdata: SerializedEntityData;
1291
+ } | null {
1096
1292
  // Increment the entity access count.
1097
1293
  if (!(guid in this.entityCount)) {
1098
1294
  this.entityCount[guid] = 0;
1099
1295
  }
1100
1296
  this.entityCount[guid]++;
1101
1297
  if (guid in this.entityCache) {
1102
- const entity = this.nymph.getEntityClass(className).factorySync() as T;
1103
- if (entity) {
1104
- entity.$nymph = this.nymph;
1105
- entity.$useSkipAc(!!useSkipAc);
1106
- entity.guid = guid;
1107
- entity.cdate = this.entityCache[guid]['cdate'];
1108
- entity.mdate = this.entityCache[guid]['mdate'];
1109
- entity.tags = this.entityCache[guid]['tags'];
1110
- entity.$putData(
1111
- this.entityCache[guid]['data'],
1112
- this.entityCache[guid]['sdata']
1113
- );
1114
- return entity;
1115
- }
1298
+ return {
1299
+ guid,
1300
+ cdate: this.entityCache[guid]['cdate'],
1301
+ mdate: this.entityCache[guid]['mdate'],
1302
+ tags: this.entityCache[guid]['tags'],
1303
+ data: this.entityCache[guid]['data'],
1304
+ sdata: this.entityCache[guid]['sdata'],
1305
+ };
1116
1306
  }
1117
1307
  return null;
1118
1308
  }
@@ -1133,7 +1323,7 @@ export default abstract class NymphDriver {
1133
1323
  mdate: number,
1134
1324
  tags: string[],
1135
1325
  data: EntityData,
1136
- sdata: SerializedEntityData
1326
+ sdata: SerializedEntityData,
1137
1327
  ) {
1138
1328
  if (guid == null) {
1139
1329
  return;
@@ -1149,11 +1339,11 @@ export default abstract class NymphDriver {
1149
1339
  }
1150
1340
  // Cache the entity.
1151
1341
  this.entityCache[guid] = {
1152
- cdate: cdate,
1153
- mdate: mdate,
1154
- tags: tags,
1155
- data: data,
1156
- sdata: sdata,
1342
+ cdate,
1343
+ mdate,
1344
+ tags,
1345
+ data,
1346
+ sdata,
1157
1347
  };
1158
1348
  while (
1159
1349
  this.nymph.config.cacheLimit &&
@@ -1162,7 +1352,7 @@ export default abstract class NymphDriver {
1162
1352
  // Find which entity has been accessed the least.
1163
1353
  const least =
1164
1354
  Object.entries(this.entityCount)
1165
- .sort(([guidA, countA], [guidB, countB]) => countA - countB)
1355
+ .sort(([_guidA, countA], [_guidB, countB]) => countA - countB)
1166
1356
  .pop()?.[0] ?? null;
1167
1357
  if (least == null) {
1168
1358
  // This should never happen.