@nymphjs/nymph 1.0.0-beta.10 → 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 +483 -0
  2. package/README.md +10 -20
  3. package/dist/Entity.d.ts +293 -10
  4. package/dist/Entity.js +563 -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 +496 -12
  11. package/dist/Nymph.js +752 -179
  12. package/dist/Nymph.js.map +1 -1
  13. package/dist/Nymph.types.d.ts +77 -8
  14. package/dist/Nymph.types.js +3 -5
  15. package/dist/Nymph.types.js.map +1 -1
  16. package/dist/conf/d.d.ts +43 -0
  17. package/dist/conf/d.js +1 -2
  18. package/dist/conf/defaults.d.ts +1 -1
  19. package/dist/conf/defaults.js +26 -3
  20. package/dist/conf/defaults.js.map +1 -1
  21. package/dist/conf/index.d.ts +2 -2
  22. package/dist/conf/index.js +2 -8
  23. package/dist/conf/index.js.map +1 -1
  24. package/dist/driver/NymphDriver.d.ts +174 -40
  25. package/dist/driver/NymphDriver.js +469 -239
  26. package/dist/driver/NymphDriver.js.map +1 -1
  27. package/dist/driver/index.d.ts +2 -2
  28. package/dist/driver/index.js +3 -23
  29. package/dist/driver/index.js.map +1 -1
  30. package/dist/driver/testArtifacts.d.ts +2 -2
  31. package/dist/driver/testArtifacts.js +814 -92
  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 +27 -12
  68. package/dist/testMocks.js +40 -29
  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 +23 -19
  75. package/src/Entity.test.ts +50 -7
  76. package/src/Entity.ts +483 -147
  77. package/src/Entity.types.ts +72 -74
  78. package/src/Nymph.ts +868 -322
  79. package/src/Nymph.types.ts +146 -27
  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 +562 -414
  84. package/src/driver/index.ts +2 -2
  85. package/src/driver/testArtifacts.ts +954 -185
  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 +76 -35
  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
  */
@@ -67,13 +73,14 @@ export default abstract class NymphDriver {
67
73
  abstract connect(): Promise<boolean>;
68
74
 
69
75
  abstract isConnected(): boolean;
76
+ protected abstract internalTransaction(name: string): Promise<any>;
70
77
  abstract startTransaction(name: string): Promise<Nymph>;
71
78
  abstract commit(name: string): Promise<boolean>;
72
79
  abstract rollback(name: string): Promise<boolean>;
73
80
  abstract inTransaction(): Promise<boolean>;
74
81
  abstract deleteEntityByID(
75
82
  guid: string,
76
- classConstructor: EntityConstructor
83
+ classConstructor: EntityConstructor,
77
84
  ): Promise<boolean>;
78
85
  abstract deleteEntityByID(guid: string, className?: string): Promise<boolean>;
79
86
  abstract deleteUID(name: string): Promise<boolean>;
@@ -83,9 +90,19 @@ export default abstract class NymphDriver {
83
90
  */
84
91
  abstract disconnect(): Promise<boolean>;
85
92
 
86
- protected abstract exportEntities(
87
- writeLine: (line: string) => void
88
- ): Promise<void>;
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
+ >;
89
106
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
90
107
  options: Options<T> & { return: 'count' },
91
108
  ...selectors: Selector[]
@@ -95,39 +112,37 @@ export default abstract class NymphDriver {
95
112
  ...selectors: Selector[]
96
113
  ): Promise<string[]>;
97
114
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
98
- options?: Options<T>,
115
+ options: Options<T> & { return: 'object' },
99
116
  ...selectors: Selector[]
100
- ): Promise<ReturnType<T['factorySync']>[]>;
117
+ ): Promise<EntityObjectType<T>[]>;
101
118
  abstract getEntities<T extends EntityConstructor = EntityConstructor>(
102
119
  options?: Options<T>,
103
120
  ...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
- >(
121
+ ): Promise<EntityInstanceType<T>[]>;
122
+ abstract getEntities<T extends EntityConstructor = EntityConstructor>(
126
123
  options?: Options<T>,
127
124
  ...selectors: Selector[]
128
- ): ReturnType<T['factorySync']>[] | string[] | number;
125
+ ): Promise<
126
+ EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] | number
127
+ >;
129
128
  abstract getUID(name: string): Promise<number | null>;
130
- 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>;
131
146
  abstract newUID(name: string): Promise<number | null>;
132
147
  abstract renameUID(oldName: string, newName: string): Promise<boolean>;
133
148
  abstract saveEntity(entity: EntityInterface): Promise<boolean>;
@@ -147,7 +162,7 @@ export default abstract class NymphDriver {
147
162
  protected posixRegexMatch(
148
163
  pattern: string,
149
164
  subject: string,
150
- caseInsensitive = false
165
+ caseInsensitive = false,
151
166
  ) {
152
167
  const posixClasses = [
153
168
  // '[[:<:]]',
@@ -204,9 +219,9 @@ export default abstract class NymphDriver {
204
219
  if (!fhandle) {
205
220
  throw new InvalidParametersError('Provided filename is not writeable.');
206
221
  }
207
- await this.exportEntities((line: string) => {
208
- fs.writeSync(fhandle, `${line}\n`);
209
- });
222
+ for await (let entry of this.exportDataIterator()) {
223
+ fs.writeSync(fhandle, entry.content);
224
+ }
210
225
  fs.closeSync(fhandle);
211
226
  } catch (e: any) {
212
227
  return false;
@@ -215,130 +230,157 @@ export default abstract class NymphDriver {
215
230
  }
216
231
 
217
232
  public async exportPrint() {
218
- await this.exportEntities((line: string) => {
219
- console.log(line);
220
- });
233
+ for await (let entry of this.exportDataIterator()) {
234
+ console.log(entry.content);
235
+ }
221
236
  return true;
222
237
  }
223
238
 
224
- protected async importFromFile(
225
- filename: string,
226
- saveEntityCallback: (
227
- guid: string,
228
- tags: string[],
229
- sdata: SerializedEntityData,
230
- etype: string
231
- ) => Promise<void>,
232
- saveUIDCallback: (name: string, value: number) => Promise<void>,
233
- startTransactionCallback: (() => Promise<void>) | null = null,
234
- commitTransactionCallback: (() => Promise<void>) | null = null
239
+ public async importDataIterator(
240
+ lines: Iterable<string>,
241
+ transaction?: boolean,
235
242
  ) {
236
- let rl: ReadLines;
237
- try {
238
- rl = new ReadLines(filename);
239
- } catch (e: any) {
240
- 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.');
241
247
  }
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();
248
+
249
+ if (transaction) {
250
+ await this.internalTransaction('nymph-import');
249
251
  }
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';
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;
271
260
  }
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];
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
+ }
280
294
  }
295
+ // Clear the entity cache.
296
+ this.entityCache = {};
281
297
  }
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();
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;
291
312
  }
292
313
  return true;
293
314
  }
294
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
+
295
340
  public checkData(
341
+ etype: string,
296
342
  data: EntityData,
297
343
  sdata: SerializedEntityData,
298
344
  selectors: Selector[],
299
345
  guid: string | null = null,
300
- tags: string[] | null = null
346
+ tags: string[] | null = null,
301
347
  ) {
302
- const formattedSelectors = this.formatSelectors(selectors).selectors;
348
+ try {
349
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
350
+ const formattedSelectors = this.formatSelectors(selectors).selectors;
303
351
 
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];
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];
312
360
 
313
- if (key === 'type' || value == null) {
314
- continue;
315
- }
361
+ if (key === 'type' || value == null) {
362
+ continue;
363
+ }
316
364
 
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.");
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
+ );
327
374
  } 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
- }
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
+ }
334
383
 
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
384
  const propName = (curValue as string[])[0];
343
385
 
344
386
  // Unserialize the data for this variable.
@@ -356,7 +398,7 @@ export default abstract class NymphDriver {
356
398
  case '!tag':
357
399
  pass = xor(
358
400
  (tags ?? []).indexOf(propName) !== -1,
359
- xor(typeIsNot, clauseNot)
401
+ xor(typeIsNot, clauseNot),
360
402
  );
361
403
  break;
362
404
  case 'defined':
@@ -367,7 +409,7 @@ export default abstract class NymphDriver {
367
409
  case '!truthy':
368
410
  pass = xor(
369
411
  propName in data && data[propName],
370
- xor(typeIsNot, clauseNot)
412
+ xor(typeIsNot, clauseNot),
371
413
  );
372
414
  break;
373
415
  case 'ref':
@@ -377,9 +419,9 @@ export default abstract class NymphDriver {
377
419
  propName in data &&
378
420
  this.entityReferenceSearch(
379
421
  data[propName],
380
- testRefValue
422
+ testRefValue,
381
423
  ),
382
- xor(typeIsNot, clauseNot)
424
+ xor(typeIsNot, clauseNot),
383
425
  );
384
426
  break;
385
427
  case 'equal':
@@ -391,7 +433,7 @@ export default abstract class NymphDriver {
391
433
  propName in data &&
392
434
  JSON.stringify(data[propName]) ===
393
435
  JSON.stringify(testEqualValue),
394
- xor(typeIsNot, clauseNot)
436
+ xor(typeIsNot, clauseNot),
395
437
  );
396
438
  break;
397
439
  case 'contain':
@@ -402,9 +444,9 @@ export default abstract class NymphDriver {
402
444
  pass = xor(
403
445
  propName in data &&
404
446
  JSON.stringify(data[propName]).indexOf(
405
- JSON.stringify(testContainValue)
447
+ JSON.stringify(testContainValue),
406
448
  ) !== -1,
407
- xor(typeIsNot, clauseNot)
449
+ xor(typeIsNot, clauseNot),
408
450
  );
409
451
  break;
410
452
  case 'like':
@@ -419,9 +461,27 @@ export default abstract class NymphDriver {
419
461
  escapeRegExp(testLikeValue)
420
462
  .replace('%', () => '.*')
421
463
  .replace('_', () => '.') +
422
- '$'
464
+ '$',
423
465
  ).test(data[propName]),
424
- xor(typeIsNot, clauseNot)
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',
483
+ ).test(data[propName]),
484
+ xor(typeIsNot, clauseNot),
425
485
  );
426
486
  break;
427
487
  case 'match':
@@ -433,7 +493,7 @@ export default abstract class NymphDriver {
433
493
  pass = xor(
434
494
  propName in data &&
435
495
  this.posixRegexMatch(testMatchValue, data[propName]),
436
- xor(typeIsNot, clauseNot)
496
+ xor(typeIsNot, clauseNot),
437
497
  );
438
498
  break;
439
499
  case 'imatch':
@@ -447,9 +507,28 @@ export default abstract class NymphDriver {
447
507
  this.posixRegexMatch(
448
508
  testIMatchValue,
449
509
  data[propName],
450
- true
510
+ true,
451
511
  ),
452
- xor(typeIsNot, clauseNot)
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,
530
+ ),
531
+ xor(typeIsNot, clauseNot),
453
532
  );
454
533
  break;
455
534
  case 'gt':
@@ -459,7 +538,7 @@ export default abstract class NymphDriver {
459
538
  )[1] as number;
460
539
  pass = xor(
461
540
  propName in data && data[propName] > testGTValue,
462
- xor(typeIsNot, clauseNot)
541
+ xor(typeIsNot, clauseNot),
463
542
  );
464
543
  break;
465
544
  case 'gte':
@@ -469,7 +548,7 @@ export default abstract class NymphDriver {
469
548
  )[1] as number;
470
549
  pass = xor(
471
550
  propName in data && data[propName] >= testGTEValue,
472
- xor(typeIsNot, clauseNot)
551
+ xor(typeIsNot, clauseNot),
473
552
  );
474
553
  break;
475
554
  case 'lt':
@@ -479,7 +558,7 @@ export default abstract class NymphDriver {
479
558
  )[1] as number;
480
559
  pass = xor(
481
560
  propName in data && data[propName] < testLTValue,
482
- xor(typeIsNot, clauseNot)
561
+ xor(typeIsNot, clauseNot),
483
562
  );
484
563
  break;
485
564
  case 'lte':
@@ -489,26 +568,33 @@ export default abstract class NymphDriver {
489
568
  )[1] as number;
490
569
  pass = xor(
491
570
  propName in data && data[propName] <= testLTEValue,
492
- xor(typeIsNot, clauseNot)
571
+ xor(typeIsNot, clauseNot),
493
572
  );
494
573
  break;
495
574
  }
496
- }
497
- if (!xor(typeIsOr, pass)) {
498
- break;
575
+
576
+ if (!xor(typeIsOr, pass)) {
577
+ break;
578
+ }
499
579
  }
500
580
  }
501
581
  }
582
+ if (!xor(typeIsOr, pass)) {
583
+ break;
584
+ }
502
585
  }
503
- if (!xor(typeIsOr, pass)) {
504
- break;
586
+ if (!pass) {
587
+ return false;
505
588
  }
506
589
  }
507
- if (!pass) {
508
- return false;
509
- }
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;
510
597
  }
511
- return true;
512
598
  }
513
599
 
514
600
  /**
@@ -547,7 +633,7 @@ export default abstract class NymphDriver {
547
633
  | EntityInterface
548
634
  | EntityReference
549
635
  | string
550
- | (EntityInterface | EntityReference | string)[]
636
+ | (EntityInterface | EntityReference | string)[],
551
637
  ) {
552
638
  // Get the GUID, if the passed $entity is an object.
553
639
  const guids: string[] = [];
@@ -578,7 +664,7 @@ export default abstract class NymphDriver {
578
664
  return true;
579
665
  }
580
666
  // Search through arrays and objects looking for the reference.
581
- if (Array.isArray(value) || typeof value === 'object') {
667
+ if (value != null && (Array.isArray(value) || typeof value === 'object')) {
582
668
  for (const key in value) {
583
669
  if (this.entityReferenceSearch(value[key], guids)) {
584
670
  return true;
@@ -590,7 +676,7 @@ export default abstract class NymphDriver {
590
676
 
591
677
  public formatSelectors(
592
678
  selectors: Selector[],
593
- options: Options = {}
679
+ options: Options = {},
594
680
  ): {
595
681
  selectors: FormattedSelector[];
596
682
  qrefs: [Options, ...FormattedSelector[]][];
@@ -625,10 +711,9 @@ export default abstract class NymphDriver {
625
711
  for (let i = 0; i < tmpArr.length; i++) {
626
712
  const name = tmpArr[i][0];
627
713
  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');
714
+ const QrefEntityClass = qrefOptions.class
715
+ ? this.nymph.getEntityClass(qrefOptions.class)
716
+ : this.nymph.getEntityClass('Entity');
632
717
  const newOptions = {
633
718
  ...qrefOptions,
634
719
  class: QrefEntityClass,
@@ -637,7 +722,7 @@ export default abstract class NymphDriver {
637
722
  const newSelectors = this.formatSelectors(qrefSelectors, options);
638
723
  qrefs.push(
639
724
  [newOptions, ...newSelectors.selectors],
640
- ...newSelectors.qrefs
725
+ ...newSelectors.qrefs,
641
726
  );
642
727
  formatArr[i] = [name, [newOptions, ...newSelectors.selectors]];
643
728
  }
@@ -685,12 +770,12 @@ export default abstract class NymphDriver {
685
770
 
686
771
  protected iterateSelectorsForQuery(
687
772
  selectors: FormattedSelector[],
688
- callback: (
689
- key: string,
690
- value: any,
691
- typeIsOr: boolean,
692
- typeIsNot: boolean
693
- ) => string
773
+ callback: (data: {
774
+ key: string;
775
+ value: any;
776
+ typeIsOr: boolean;
777
+ typeIsNot: boolean;
778
+ }) => string,
694
779
  ) {
695
780
  const queryParts = [];
696
781
  for (const curSelector of selectors) {
@@ -703,7 +788,7 @@ export default abstract class NymphDriver {
703
788
  if (key === 'type') {
704
789
  continue;
705
790
  }
706
- let curQuery = callback(key, value, typeIsOr, typeIsNot);
791
+ let curQuery = callback({ key, value, typeIsOr, typeIsNot });
707
792
  if (curQuery) {
708
793
  if (curSelectorQuery) {
709
794
  curSelectorQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -719,71 +804,14 @@ export default abstract class NymphDriver {
719
804
  return queryParts;
720
805
  }
721
806
 
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>(
807
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
780
808
  options: Options<T> & { return: 'count' },
781
809
  selectors: Selector[],
782
- performQueryCallback: (
783
- options: Options<T>,
784
- formattedSelectors: FormattedSelector[],
785
- etype: string
786
- ) => {
810
+ performQueryCallback: (data: {
811
+ options: Options<T>;
812
+ selectors: FormattedSelector[];
813
+ etype: string;
814
+ }) => {
787
815
  result: any;
788
816
  },
789
817
  rowFetchCallback: () => any,
@@ -798,16 +826,16 @@ export default abstract class NymphDriver {
798
826
  getDataNameAndSValueCallback: (row: any) => {
799
827
  name: string;
800
828
  svalue: string;
801
- }
829
+ },
802
830
  ): { result: any; process: () => number | Error };
803
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
831
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
804
832
  options: Options<T> & { return: 'guid' },
805
833
  selectors: Selector[],
806
- performQueryCallback: (
807
- options: Options<T>,
808
- formattedSelectors: FormattedSelector[],
809
- etype: string
810
- ) => {
834
+ performQueryCallback: (data: {
835
+ options: Options<T>;
836
+ selectors: FormattedSelector[];
837
+ etype: string;
838
+ }) => {
811
839
  result: any;
812
840
  },
813
841
  rowFetchCallback: () => any,
@@ -822,16 +850,40 @@ export default abstract class NymphDriver {
822
850
  getDataNameAndSValueCallback: (row: any) => {
823
851
  name: string;
824
852
  svalue: string;
825
- }
853
+ },
826
854
  ): { result: any; process: () => string[] | Error };
827
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
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>(
828
880
  options: Options<T>,
829
881
  selectors: Selector[],
830
- performQueryCallback: (
831
- options: Options<T>,
832
- formattedSelectors: FormattedSelector[],
833
- etype: string
834
- ) => {
882
+ performQueryCallback: (data: {
883
+ options: Options<T>;
884
+ selectors: FormattedSelector[];
885
+ etype: string;
886
+ }) => {
835
887
  result: any;
836
888
  },
837
889
  rowFetchCallback: () => any,
@@ -846,16 +898,19 @@ export default abstract class NymphDriver {
846
898
  getDataNameAndSValueCallback: (row: any) => {
847
899
  name: string;
848
900
  svalue: string;
849
- }
850
- ): { result: any; process: () => ReturnType<T['factorySync']>[] | Error };
851
- protected getEntitesRowLike<T extends EntityConstructor = EntityConstructor>(
901
+ },
902
+ ): {
903
+ result: any;
904
+ process: () => EntityInstanceType<T>[] | Error;
905
+ };
906
+ protected getEntitiesRowLike<T extends EntityConstructor = EntityConstructor>(
852
907
  options: Options<T>,
853
908
  selectors: Selector[],
854
- performQueryCallback: (
855
- options: Options<T>,
856
- formattedSelectors: FormattedSelector[],
857
- etype: string
858
- ) => {
909
+ performQueryCallback: (data: {
910
+ options: Options<T>;
911
+ selectors: FormattedSelector[];
912
+ etype: string;
913
+ }) => {
859
914
  result: any;
860
915
  },
861
916
  rowFetchCallback: () => any,
@@ -870,10 +925,15 @@ export default abstract class NymphDriver {
870
925
  getDataNameAndSValueCallback: (row: any) => {
871
926
  name: string;
872
927
  svalue: string;
873
- }
928
+ },
874
929
  ): {
875
930
  result: any;
876
- process: () => ReturnType<T['factorySync']>[] | string[] | number | Error;
931
+ process: () =>
932
+ | EntityInstanceType<T>[]
933
+ | EntityObjectType<T>[]
934
+ | string[]
935
+ | number
936
+ | Error;
877
937
  } {
878
938
  if (!this.isConnected()) {
879
939
  throw new UnableToConnectError('not connected to DB');
@@ -886,21 +946,29 @@ export default abstract class NymphDriver {
886
946
  ['&', '!&', '|', '!|'].indexOf(selector.type) === -1
887
947
  ) {
888
948
  throw new InvalidParametersError(
889
- 'Invalid query selector passed: ' + JSON.stringify(selector)
949
+ 'Invalid query selector passed: ' + JSON.stringify(selector),
890
950
  );
891
951
  }
892
952
  }
893
953
 
894
- let entities: ReturnType<T['factorySync']>[] | string[] = [];
954
+ let entities: EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] =
955
+ [];
895
956
  let count = 0;
896
957
  const EntityClass =
897
958
  options.class ?? (this.nymph.getEntityClass('Entity') as T);
898
959
  const etype = EntityClass.ETYPE;
899
960
 
961
+ if (options.source === 'client' && options.return === 'object') {
962
+ throw new InvalidParametersError(
963
+ 'Object return type not allowed from client.',
964
+ );
965
+ }
966
+
900
967
  // Check if the requested entity is cached.
901
968
  if (
902
969
  this.nymph.config.cache &&
903
970
  !options.skipCache &&
971
+ selectors.length &&
904
972
  'guid' in selectors[0] &&
905
973
  typeof selectors[0].guid === 'number'
906
974
  ) {
@@ -912,33 +980,75 @@ export default abstract class NymphDriver {
912
980
  (Object.keys(selectors[0]).length === 2 ||
913
981
  (Object.keys(selectors[0]).length === 3 && 'tag' in selectors[0]))
914
982
  ) {
915
- const entity = this.pullCache<ReturnType<T['factorySync']>>(
916
- selectors[0]['guid'],
917
- EntityClass.class,
918
- !!options.skipAc
919
- );
983
+ const cacheEntity = this.pullCache(selectors[0]['guid']);
984
+
920
985
  if (
921
- entity != null &&
922
- entity.guid != null &&
986
+ cacheEntity != null &&
987
+ cacheEntity.guid != null &&
923
988
  (!('tag' in selectors[0]) ||
924
- entity.hasTag(
925
- ...(Array.isArray(selectors[0].tag)
989
+ !(
990
+ Array.isArray(selectors[0].tag)
926
991
  ? selectors[0].tag
927
- : [selectors[0].tag])
928
- ))
992
+ : [selectors[0].tag]
993
+ ).find((tag) => tag == null || !cacheEntity.tags.includes(tag)))
929
994
  ) {
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
- };
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
+ }
942
1052
  }
943
1053
  }
944
1054
  }
@@ -946,15 +1056,16 @@ export default abstract class NymphDriver {
946
1056
  try {
947
1057
  const formattedSelectors = this.formatSelectors(selectors, options);
948
1058
  this.nymph.runQueryCallbacks(options, formattedSelectors.selectors);
949
- for (let qref of formattedSelectors.qrefs) {
950
- const [options, ...selectors] = qref;
1059
+ for (let i = 0; i < formattedSelectors.qrefs.length; i++) {
1060
+ const [options, ...selectors] = formattedSelectors.qrefs[i];
951
1061
  this.nymph.runQueryCallbacks(options, selectors);
1062
+ formattedSelectors.qrefs[i] = [options, ...selectors];
952
1063
  }
953
- const { result } = performQueryCallback(
1064
+ const { result } = performQueryCallback({
954
1065
  options,
955
- formattedSelectors.selectors,
956
- etype
957
- );
1066
+ selectors: formattedSelectors.selectors,
1067
+ etype,
1068
+ });
958
1069
 
959
1070
  return {
960
1071
  result,
@@ -994,28 +1105,43 @@ export default abstract class NymphDriver {
994
1105
  // Make sure that $row is incremented :)
995
1106
  row = rowFetchCallback();
996
1107
  }
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);
1003
- }
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.');
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);
1011
1141
  }
1012
- entity.$putData(data, sdata);
1013
- this.putDataCounter--;
1014
1142
  if (this.nymph.config.cache) {
1015
1143
  this.pushCache(guid, cdate, mdate, tags, data, sdata);
1016
1144
  }
1017
- // @ts-ignore: ts doesn't know the return type here.
1018
- entities.push(entity);
1019
1145
  }
1020
1146
  }
1021
1147
 
@@ -1036,88 +1162,115 @@ export default abstract class NymphDriver {
1036
1162
 
1037
1163
  protected async saveEntityRowLike(
1038
1164
  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>,
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>,
1057
1185
  startTransactionCallback: (() => Promise<void>) | null = null,
1058
1186
  commitTransactionCallback:
1059
1187
  | ((success: boolean) => Promise<boolean>)
1060
- | null = null
1188
+ | null = null,
1061
1189
  ) {
1190
+ const originalGuid = entity.guid;
1191
+ const originalCdate = entity.cdate;
1192
+ const originalMdate = entity.mdate;
1062
1193
  // Get a modified date.
1063
- 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
+ }
1064
1202
  const tags = difference(entity.tags, ['']);
1065
1203
  const data = entity.$getData();
1066
1204
  const sdata = entity.$getSData();
1205
+ const uniques = await entity.$getUniques();
1067
1206
  const EntityClass = entity.constructor as EntityConstructor;
1068
1207
  const etype = EntityClass.ETYPE;
1069
1208
  if (startTransactionCallback) {
1070
1209
  await startTransactionCallback();
1071
1210
  }
1072
1211
  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;
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
+ }
1089
1249
  }
1090
- } else {
1091
- // Removed any cached versions of this entity.
1092
- if (this.nymph.config.cache) {
1093
- this.cleanCache(entity.guid);
1250
+ if (commitTransactionCallback) {
1251
+ success = await commitTransactionCallback(success);
1094
1252
  }
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;
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
+ );
1106
1263
  }
1264
+ } catch (e: any) {
1265
+ entity.guid = originalGuid;
1266
+ entity.cdate = originalCdate;
1267
+ entity.mdate = originalMdate;
1268
+ throw e;
1107
1269
  }
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
- );
1270
+ if (!success) {
1271
+ entity.guid = originalGuid;
1272
+ entity.cdate = originalCdate;
1273
+ entity.mdate = originalMdate;
1121
1274
  }
1122
1275
  return success;
1123
1276
  }
@@ -1126,35 +1279,30 @@ export default abstract class NymphDriver {
1126
1279
  * Pull an entity from the cache.
1127
1280
  *
1128
1281
  * @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.
1282
+ * @returns The entity's data or null if it's not cached.
1132
1283
  */
1133
- protected pullCache<T extends EntityInterface = EntityInterface>(
1134
- guid: string,
1135
- className: string,
1136
- useSkipAc = false
1137
- ): 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 {
1138
1292
  // Increment the entity access count.
1139
1293
  if (!(guid in this.entityCount)) {
1140
1294
  this.entityCount[guid] = 0;
1141
1295
  }
1142
1296
  this.entityCount[guid]++;
1143
1297
  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
- }
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
+ };
1158
1306
  }
1159
1307
  return null;
1160
1308
  }
@@ -1175,7 +1323,7 @@ export default abstract class NymphDriver {
1175
1323
  mdate: number,
1176
1324
  tags: string[],
1177
1325
  data: EntityData,
1178
- sdata: SerializedEntityData
1326
+ sdata: SerializedEntityData,
1179
1327
  ) {
1180
1328
  if (guid == null) {
1181
1329
  return;
@@ -1191,11 +1339,11 @@ export default abstract class NymphDriver {
1191
1339
  }
1192
1340
  // Cache the entity.
1193
1341
  this.entityCache[guid] = {
1194
- cdate: cdate,
1195
- mdate: mdate,
1196
- tags: tags,
1197
- data: data,
1198
- sdata: sdata,
1342
+ cdate,
1343
+ mdate,
1344
+ tags,
1345
+ data,
1346
+ sdata,
1199
1347
  };
1200
1348
  while (
1201
1349
  this.nymph.config.cacheLimit &&
@@ -1204,7 +1352,7 @@ export default abstract class NymphDriver {
1204
1352
  // Find which entity has been accessed the least.
1205
1353
  const least =
1206
1354
  Object.entries(this.entityCount)
1207
- .sort(([guidA, countA], [guidB, countB]) => countA - countB)
1355
+ .sort(([_guidA, countA], [_guidB, countB]) => countA - countB)
1208
1356
  .pop()?.[0] ?? null;
1209
1357
  if (least == null) {
1210
1358
  // This should never happen.