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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +588 -0
  2. package/README.md +23 -21
  3. package/dist/Entity.d.ts +295 -12
  4. package/dist/Entity.js +560 -148
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Entity.test.js +52 -13
  7. package/dist/Entity.test.js.map +1 -1
  8. package/dist/Entity.types.d.ts +256 -23
  9. package/dist/Entity.types.js +1 -2
  10. package/dist/Nymph.d.ts +590 -13
  11. package/dist/Nymph.js +846 -180
  12. package/dist/Nymph.js.map +1 -1
  13. package/dist/Nymph.types.d.ts +131 -8
  14. package/dist/Nymph.types.js +47 -5
  15. package/dist/Nymph.types.js.map +1 -1
  16. package/dist/conf/defaults.d.ts +1 -1
  17. package/dist/conf/defaults.js +26 -3
  18. package/dist/conf/defaults.js.map +1 -1
  19. package/dist/conf/index.d.ts +50 -2
  20. package/dist/conf/index.js +2 -8
  21. package/dist/conf/index.js.map +1 -1
  22. package/dist/driver/NymphDriver.d.ts +205 -40
  23. package/dist/driver/NymphDriver.js +712 -238
  24. package/dist/driver/NymphDriver.js.map +1 -1
  25. package/dist/driver/index.d.ts +1 -2
  26. package/dist/driver/index.js +2 -23
  27. package/dist/driver/index.js.map +1 -1
  28. package/dist/driver/testArtifacts.d.ts +4 -4
  29. package/dist/driver/testArtifacts.js +1920 -1086
  30. package/dist/driver/testArtifacts.js.map +1 -1
  31. package/dist/errors/ClassNotAvailableError.js +1 -5
  32. package/dist/errors/ClassNotAvailableError.js.map +1 -1
  33. package/dist/errors/EntityConflictError.js +1 -5
  34. package/dist/errors/EntityConflictError.js.map +1 -1
  35. package/dist/errors/EntityInvalidDataError.d.ts +6 -0
  36. package/dist/errors/EntityInvalidDataError.js +8 -6
  37. package/dist/errors/EntityInvalidDataError.js.map +1 -1
  38. package/dist/errors/EntityIsSleepingReferenceError.js +1 -5
  39. package/dist/errors/EntityIsSleepingReferenceError.js.map +1 -1
  40. package/dist/errors/EntityUniqueConstraintError.d.ts +3 -0
  41. package/dist/errors/EntityUniqueConstraintError.js +7 -0
  42. package/dist/errors/EntityUniqueConstraintError.js.map +1 -0
  43. package/dist/errors/HttpError.d.ts +5 -0
  44. package/dist/errors/HttpError.js +11 -0
  45. package/dist/errors/HttpError.js.map +1 -0
  46. package/dist/errors/InvalidParametersError.js +1 -5
  47. package/dist/errors/InvalidParametersError.js.map +1 -1
  48. package/dist/errors/InvalidStateError.d.ts +3 -0
  49. package/dist/errors/InvalidStateError.js +7 -0
  50. package/dist/errors/InvalidStateError.js.map +1 -0
  51. package/dist/errors/NotConfiguredError.js +1 -5
  52. package/dist/errors/NotConfiguredError.js.map +1 -1
  53. package/dist/errors/QueryFailedError.d.ts +2 -1
  54. package/dist/errors/QueryFailedError.js +5 -6
  55. package/dist/errors/QueryFailedError.js.map +1 -1
  56. package/dist/errors/UnableToConnectError.js +1 -5
  57. package/dist/errors/UnableToConnectError.js.map +1 -1
  58. package/dist/errors/index.d.ts +11 -8
  59. package/dist/errors/index.js +11 -24
  60. package/dist/errors/index.js.map +1 -1
  61. package/dist/index.d.ts +11 -11
  62. package/dist/index.js +14 -34
  63. package/dist/index.js.map +1 -1
  64. package/dist/nymphJoiProps.d.ts +1 -1
  65. package/dist/nymphJoiProps.js +10 -15
  66. package/dist/nymphJoiProps.js.map +1 -1
  67. package/dist/testArtifacts.d.ts +19 -6
  68. package/dist/testArtifacts.js +32 -37
  69. package/dist/testArtifacts.js.map +1 -1
  70. package/dist/testMocks.d.ts +27 -12
  71. package/dist/testMocks.js +40 -29
  72. package/dist/testMocks.js.map +1 -1
  73. package/dist/utils.d.ts +4 -4
  74. package/dist/utils.js +40 -29
  75. package/dist/utils.js.map +1 -1
  76. package/jest.config.js +31 -5
  77. package/package.json +33 -23
  78. package/src/Entity.test.ts +50 -7
  79. package/src/Entity.ts +489 -150
  80. package/src/Entity.types.ts +72 -74
  81. package/src/Nymph.ts +991 -324
  82. package/src/Nymph.types.ts +201 -27
  83. package/src/conf/defaults.ts +34 -1
  84. package/src/conf/index.ts +50 -2
  85. package/src/driver/NymphDriver.ts +898 -413
  86. package/src/driver/index.ts +1 -3
  87. package/src/driver/testArtifacts.ts +2557 -1629
  88. package/src/errors/EntityUniqueConstraintError.ts +6 -0
  89. package/src/errors/HttpError.ts +12 -0
  90. package/src/errors/InvalidStateError.ts +6 -0
  91. package/src/errors/QueryFailedError.ts +3 -1
  92. package/src/errors/index.ts +14 -8
  93. package/src/index.ts +11 -11
  94. package/src/nymphJoiProps.ts +2 -2
  95. package/src/testArtifacts.ts +20 -20
  96. package/src/testMocks.ts +76 -35
  97. package/src/utils.ts +39 -18
  98. package/tsconfig.json +6 -3
  99. package/typedoc.json +4 -0
  100. package/dist/conf/d.d.ts +0 -6
  101. package/dist/conf/d.js +0 -3
  102. package/dist/conf/d.js.map +0 -1
  103. package/src/conf/d.ts +0 -24
@@ -1,34 +1,50 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const fs_1 = __importDefault(require("fs"));
7
- const lodash_1 = require("lodash");
8
- const n_readlines_1 = __importDefault(require("n-readlines"));
9
- const strtotime_1 = __importDefault(require("locutus/php/datetime/strtotime"));
10
- const guid_1 = require("@nymphjs/guid");
11
- const errors_1 = require("../errors");
12
- const utils_1 = require("../utils");
1
+ import fs from 'node:fs';
2
+ import { difference } from 'lodash-es';
3
+ import ReadLines from 'n-readlines';
4
+ import { strtotime } from 'locutus/php/datetime/index';
5
+ import { Tokenizer } from '@sciactive/tokenizer';
6
+ import { ClassNotAvailableError, InvalidParametersError, UnableToConnectError, } from '../errors/index.js';
7
+ import { xor } from '../utils.js';
8
+ // from: https://stackoverflow.com/a/6969486/664915
13
9
  function escapeRegExp(string) {
14
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
15
11
  }
16
- class NymphDriver {
17
- constructor() {
18
- this.nymph = new Proxy({}, {
19
- get() {
20
- throw new Error('Attempted access of Nymph instance before driver initialization!');
21
- },
22
- });
23
- this.entityCache = {};
24
- this.entityCount = {};
25
- this.putDataCounter = 0;
26
- }
12
+ /**
13
+ * A Nymph database driver.
14
+ */
15
+ export default class NymphDriver {
16
+ nymph = new Proxy({}, {
17
+ get() {
18
+ throw new Error('Attempted access of Nymph instance before driver initialization!');
19
+ },
20
+ });
21
+ tokenizer = new Tokenizer();
22
+ /**
23
+ * A cache to make entity retrieval faster.
24
+ */
25
+ entityCache = {};
26
+ /**
27
+ * A counter for the entity cache to determine the most accessed entities.
28
+ */
29
+ entityCount = {};
30
+ /**
31
+ * Protect against infinite loops.
32
+ */
33
+ putDataCounter = 0;
34
+ /**
35
+ * Initialize the Nymph driver.
36
+ *
37
+ * This is meant to be called internally by Nymph. Don't call this directly.
38
+ *
39
+ * @param nymph The Nymph instance.
40
+ */
27
41
  init(nymph) {
28
42
  this.nymph = nymph;
29
43
  }
30
44
  posixRegexMatch(pattern, subject, caseInsensitive = false) {
31
45
  const posixClasses = [
46
+ // '[[:<:]]',
47
+ // '[[:>:]]',
32
48
  '[:alnum:]',
33
49
  '[:alpha:]',
34
50
  '[:ascii:]',
@@ -45,17 +61,21 @@ class NymphDriver {
45
61
  '[:xdigit:]',
46
62
  ];
47
63
  const jsClasses = [
64
+ // '\b(?=\w)',
65
+ // '(?<=\w)\b',
48
66
  '[A-Za-z0-9]',
49
67
  '[A-Za-z]',
50
68
  '[\x00-\x7F]',
51
- '[ \t]',
52
- '[\x00-\x1F\x7F]',
53
- '[0-9]',
54
- '[\x21-\x7E]',
69
+ '[ \t]', // '\s',
70
+ '[\x00-\x1F\x7F]', // '[\000\001\002\003\004\005\006\007\008\009\010\011\012\013\014'.
71
+ // '\015\016\017\018\019\020\021\022\023\024\025\026\027\028\029'.
72
+ // '\030\031\032\033\034\035\036\037\177]',
73
+ '[0-9]', // '\d',
74
+ '[\x21-\x7E]', // '[A-Za-z0-9!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}\~]',
55
75
  '[a-z]',
56
- '[\x20-\x7E]',
57
- '[!"#$%&\'()*+,-./:;<=>?@[\\\\\\]^_‘{|}~]',
58
- '[ \t\r\n\v\f]',
76
+ '[\x20-\x7E]', // '[A-Za-z0-9!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}\~]',
77
+ '[!"#$%&\'()*+,-./:;<=>?@[\\\\\\]^_‘{|}~]', // '[!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}\~]',
78
+ '[ \t\r\n\v\f]', // '[\t\n\x0B\f\r ]',
59
79
  '[A-Z]',
60
80
  '[A-Za-z0-9_]',
61
81
  '[0-9A-Fa-f]',
@@ -67,16 +87,21 @@ class NymphDriver {
67
87
  let re = new RegExp(newPattern, caseInsensitive ? 'mi' : 'm');
68
88
  return re.test(subject);
69
89
  }
90
+ checkIndexName(name) {
91
+ if (name.match(/[^a-z0-9_]/)) {
92
+ throw new Error('Index names can only contain lowercase letters, numbers, and underscores.');
93
+ }
94
+ }
70
95
  async export(filename) {
71
96
  try {
72
- const fhandle = fs_1.default.openSync(filename, 'w');
97
+ const fhandle = fs.openSync(filename, 'w');
73
98
  if (!fhandle) {
74
- throw new errors_1.InvalidParametersError('Provided filename is not writeable.');
99
+ throw new InvalidParametersError('Provided filename is not writeable.');
75
100
  }
76
- await this.exportEntities((line) => {
77
- fs_1.default.writeSync(fhandle, `${line}\n`);
78
- });
79
- fs_1.default.closeSync(fhandle);
101
+ for await (let entry of this.exportDataIterator()) {
102
+ fs.writeSync(fhandle, entry.content);
103
+ }
104
+ fs.closeSync(fhandle);
80
105
  }
81
106
  catch (e) {
82
107
  return false;
@@ -84,98 +109,159 @@ class NymphDriver {
84
109
  return true;
85
110
  }
86
111
  async exportPrint() {
87
- await this.exportEntities((line) => {
88
- console.log(line);
89
- });
112
+ for await (let entry of this.exportDataIterator()) {
113
+ console.log(entry.content);
114
+ }
90
115
  return true;
91
116
  }
92
- async importFromFile(filename, saveEntityCallback, saveUIDCallback, startTransactionCallback = null, commitTransactionCallback = null) {
93
- let rl;
94
- try {
95
- rl = new n_readlines_1.default(filename);
117
+ async importDataIterator(lines, options, transaction) {
118
+ const first = lines[Symbol.iterator]().next();
119
+ if (first.done || first.value !== '#nex2') {
120
+ throw new Error('Tried to import a file that is not a NEX v2 file.');
96
121
  }
97
- catch (e) {
98
- throw new errors_1.InvalidParametersError('Provided filename is unreadable.');
122
+ if (transaction) {
123
+ await this.internalTransaction('nymph-import');
99
124
  }
100
- let guid = null;
101
- let line = false;
102
- let sdata = {};
103
- let tags = [];
104
- let etype = '__undefined';
105
- if (startTransactionCallback) {
106
- await startTransactionCallback();
107
- }
108
- while ((line = rl.next())) {
109
- const text = line.toString('utf8');
110
- if (text.match(/^\s*#/)) {
111
- continue;
112
- }
113
- const entityMatch = text.match(/^\s*{([0-9A-Fa-f]+)}<([-\w_]+)>\[([^\]]*)\]\s*$/);
114
- const propMatch = text.match(/^\s*([^=]+)\s*=\s*(.*\S)\s*$/);
115
- const uidMatch = text.match(/^\s*<([^>]+)>\[(\d+)\]\s*$/);
116
- if (uidMatch) {
117
- await saveUIDCallback(uidMatch[1], Number(uidMatch[2]));
118
- }
119
- else if (entityMatch) {
120
- if (guid) {
121
- await saveEntityCallback(guid, tags, sdata, etype);
122
- guid = null;
123
- tags = [];
124
- sdata = {};
125
- etype = '__undefined';
125
+ try {
126
+ let guid = null;
127
+ let sdata = {};
128
+ let tags = [];
129
+ let etype = '__undefined';
130
+ for (let line of lines) {
131
+ if (line.match(/^\s*#/)) {
132
+ continue;
133
+ }
134
+ const entityMatch = line.match(/^\s*{([0-9A-Fa-f]+)}<([-\w_]+)>\[([^\]]*)\]\s*$/);
135
+ const propMatch = line.match(/^\s*([^=]+)\s*=\s*(.*\S)\s*$/);
136
+ const uidMatch = line.match(/^\s*<([^>]+)>\[(\d+)\]\s*$/);
137
+ if (uidMatch) {
138
+ // Add the UID.
139
+ await this.importUID({
140
+ name: uidMatch[1],
141
+ value: Number(uidMatch[2]),
142
+ });
126
143
  }
127
- guid = entityMatch[1];
128
- etype = entityMatch[2];
129
- tags = entityMatch[3].split(',');
144
+ else if (entityMatch) {
145
+ // Save the current entity.
146
+ if (guid) {
147
+ const cdate = Number(JSON.parse(sdata.cdate));
148
+ delete sdata.cdate;
149
+ const mdate = Number(JSON.parse(sdata.mdate));
150
+ delete sdata.mdate;
151
+ try {
152
+ await this.importEntity({
153
+ guid,
154
+ cdate,
155
+ mdate,
156
+ tags,
157
+ sdata,
158
+ etype,
159
+ });
160
+ }
161
+ catch (e) {
162
+ if (!(e instanceof ClassNotAvailableError) ||
163
+ !options?.ignoreUnknownETypes) {
164
+ throw e;
165
+ }
166
+ }
167
+ guid = null;
168
+ tags = [];
169
+ sdata = {};
170
+ etype = '__undefined';
171
+ }
172
+ // Record the new entity's info.
173
+ guid = entityMatch[1];
174
+ etype = entityMatch[2];
175
+ tags = entityMatch[3].split(',');
176
+ }
177
+ else if (propMatch) {
178
+ // Add the variable to the new entity.
179
+ if (guid) {
180
+ sdata[propMatch[1]] = propMatch[2];
181
+ }
182
+ }
183
+ // Clear the entity cache.
184
+ this.entityCache = {};
130
185
  }
131
- else if (propMatch) {
132
- if (guid) {
133
- sdata[propMatch[1]] = propMatch[2];
186
+ // Save the last entity.
187
+ if (guid) {
188
+ const cdate = Number(JSON.parse(sdata.cdate));
189
+ delete sdata.cdate;
190
+ const mdate = Number(JSON.parse(sdata.mdate));
191
+ delete sdata.mdate;
192
+ try {
193
+ await this.importEntity({ guid, cdate, mdate, tags, sdata, etype });
194
+ }
195
+ catch (e) {
196
+ if (!(e instanceof ClassNotAvailableError) ||
197
+ !options?.ignoreUnknownETypes) {
198
+ throw e;
199
+ }
134
200
  }
135
201
  }
136
- this.entityCache = {};
137
- }
138
- if (guid) {
139
- await saveEntityCallback(guid, tags, sdata, etype);
202
+ if (transaction) {
203
+ await this.commit('nymph-import');
204
+ }
140
205
  }
141
- if (commitTransactionCallback) {
142
- await commitTransactionCallback();
206
+ catch (e) {
207
+ await this.rollback('nymph-import');
208
+ throw e;
143
209
  }
144
210
  return true;
145
211
  }
146
- checkData(data, sdata, selectors, guid = null, tags = null) {
147
- const formattedSelectors = this.formatSelectors(selectors).selectors;
148
- for (const curSelector of formattedSelectors) {
149
- const type = curSelector.type;
150
- const typeIsNot = type === '!&' || type === '!|';
151
- const typeIsOr = type === '|' || type === '!|';
152
- let pass = !typeIsOr;
153
- for (const k in curSelector) {
154
- const key = k;
155
- const value = curSelector[key];
156
- if (key === 'type' || value == null) {
157
- continue;
158
- }
159
- const clauseNot = key.substr(0, 1) === '!';
160
- if (key === 'selector' || key === '!selector') {
161
- const tmpArr = (Array.isArray(value) ? value : [value]);
162
- pass = (0, utils_1.xor)(this.checkData(data, sdata, tmpArr, guid, tags), (0, utils_1.xor)(typeIsNot, clauseNot));
212
+ async importData(text, transaction) {
213
+ return await this.importDataIterator(text.split('\n'), undefined, transaction);
214
+ }
215
+ async import(filename, options, transaction) {
216
+ let rl;
217
+ try {
218
+ rl = new ReadLines(filename);
219
+ }
220
+ catch (e) {
221
+ throw new InvalidParametersError('Provided filename is unreadable.');
222
+ }
223
+ const lines = {
224
+ *[Symbol.iterator]() {
225
+ let line;
226
+ while ((line = rl.next())) {
227
+ yield line.toString('utf8');
163
228
  }
164
- else {
165
- if (key === 'qref' || key === '!qref') {
166
- throw new Error("Can't use checkData on qref clauses.");
229
+ },
230
+ };
231
+ return await this.importDataIterator(lines, options, transaction);
232
+ }
233
+ checkData(etype, data, sdata, selectors, guid = null, tags = null) {
234
+ try {
235
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
236
+ const formattedSelectors = this.formatSelectors(selectors).selectors;
237
+ for (const curSelector of formattedSelectors) {
238
+ const type = curSelector.type;
239
+ const typeIsNot = type === '!&' || type === '!|';
240
+ const typeIsOr = type === '|' || type === '!|';
241
+ let pass = !typeIsOr;
242
+ for (const k in curSelector) {
243
+ const key = k;
244
+ const value = curSelector[key];
245
+ if (key === 'type' || value == null) {
246
+ continue;
247
+ }
248
+ const clauseNot = key.substring(0, 1) === '!';
249
+ if (key === 'selector' || key === '!selector') {
250
+ const tmpArr = (Array.isArray(value) ? value : [value]);
251
+ pass = xor(this.checkData(etype, data, sdata, tmpArr, guid, tags), xor(typeIsNot, clauseNot));
167
252
  }
168
253
  else {
169
- for (const curValue of value) {
170
- if (curValue == null) {
171
- continue;
172
- }
173
- if (((key === 'guid' || key === '!guid') && guid == null) ||
174
- ((key === 'tag' || key === '!tag') && tags == null)) {
175
- pass = true;
176
- }
177
- else {
254
+ if (key === 'qref' || key === '!qref') {
255
+ throw new Error("Can't use checkData on qref clauses.");
256
+ }
257
+ else {
258
+ // Check if it doesn't pass any for &, check if it passes any for |.
259
+ for (const curValue of value) {
260
+ if (curValue == null) {
261
+ continue;
262
+ }
178
263
  const propName = curValue[0];
264
+ // Unserialize the data for this variable.
179
265
  if (propName in sdata) {
180
266
  data[propName] = JSON.parse(sdata[propName]);
181
267
  delete sdata[propName];
@@ -183,99 +269,130 @@ class NymphDriver {
183
269
  switch (key) {
184
270
  case 'guid':
185
271
  case '!guid':
186
- pass = (0, utils_1.xor)(guid == propName, (0, utils_1.xor)(typeIsNot, clauseNot));
272
+ pass = xor(guid == propName, xor(typeIsNot, clauseNot));
187
273
  break;
188
274
  case 'tag':
189
275
  case '!tag':
190
- pass = (0, utils_1.xor)((tags ?? []).indexOf(propName) !== -1, (0, utils_1.xor)(typeIsNot, clauseNot));
276
+ pass = xor((tags ?? []).indexOf(propName) !== -1, xor(typeIsNot, clauseNot));
191
277
  break;
192
278
  case 'defined':
193
279
  case '!defined':
194
- pass = (0, utils_1.xor)(propName in data, (0, utils_1.xor)(typeIsNot, clauseNot));
280
+ pass = xor(propName in data, xor(typeIsNot, clauseNot));
195
281
  break;
196
282
  case 'truthy':
197
283
  case '!truthy':
198
- pass = (0, utils_1.xor)(propName in data && data[propName], (0, utils_1.xor)(typeIsNot, clauseNot));
284
+ pass = xor(propName in data && data[propName], xor(typeIsNot, clauseNot));
199
285
  break;
200
286
  case 'ref':
201
287
  case '!ref':
202
288
  const testRefValue = curValue[1];
203
- pass = (0, utils_1.xor)(propName in data &&
204
- this.entityReferenceSearch(data[propName], testRefValue), (0, utils_1.xor)(typeIsNot, clauseNot));
289
+ pass = xor(propName in data &&
290
+ this.entityReferenceSearch(data[propName], testRefValue), xor(typeIsNot, clauseNot));
205
291
  break;
206
292
  case 'equal':
207
293
  case '!equal':
208
294
  const testEqualValue = curValue[1];
209
- pass = (0, utils_1.xor)(propName in data &&
295
+ pass = xor(propName in data &&
210
296
  JSON.stringify(data[propName]) ===
211
- JSON.stringify(testEqualValue), (0, utils_1.xor)(typeIsNot, clauseNot));
297
+ JSON.stringify(testEqualValue), xor(typeIsNot, clauseNot));
212
298
  break;
213
299
  case 'contain':
214
300
  case '!contain':
215
301
  const testContainValue = curValue[1];
216
- pass = (0, utils_1.xor)(propName in data &&
217
- JSON.stringify(data[propName]).indexOf(JSON.stringify(testContainValue)) !== -1, (0, utils_1.xor)(typeIsNot, clauseNot));
302
+ pass = xor(propName in data &&
303
+ JSON.stringify(data[propName]).indexOf(JSON.stringify(testContainValue)) !== -1, xor(typeIsNot, clauseNot));
218
304
  break;
219
305
  case 'like':
220
306
  case '!like':
221
307
  const testLikeValue = curValue[1];
222
- pass = (0, utils_1.xor)(propName in data &&
308
+ pass = xor(propName in data &&
223
309
  new RegExp('^' +
224
310
  escapeRegExp(testLikeValue)
225
311
  .replace('%', () => '.*')
226
312
  .replace('_', () => '.') +
227
- '$').test(data[propName]), (0, utils_1.xor)(typeIsNot, clauseNot));
313
+ '$').test(data[propName]), xor(typeIsNot, clauseNot));
314
+ break;
315
+ case 'ilike':
316
+ case '!ilike':
317
+ const testILikeValue = curValue[1];
318
+ pass = xor(propName in data &&
319
+ new RegExp('^' +
320
+ escapeRegExp(testILikeValue)
321
+ .replace('%', () => '.*')
322
+ .replace('_', () => '.') +
323
+ '$', 'i').test(data[propName]), xor(typeIsNot, clauseNot));
228
324
  break;
229
325
  case 'match':
230
326
  case '!match':
231
327
  const testMatchValue = curValue[1];
232
- pass = (0, utils_1.xor)(propName in data &&
233
- this.posixRegexMatch(testMatchValue, data[propName]), (0, utils_1.xor)(typeIsNot, clauseNot));
328
+ // Convert a POSIX regex to a JS regex.
329
+ pass = xor(propName in data &&
330
+ this.posixRegexMatch(testMatchValue, data[propName]), xor(typeIsNot, clauseNot));
234
331
  break;
235
332
  case 'imatch':
236
333
  case '!imatch':
237
334
  const testIMatchValue = curValue[1];
238
- pass = (0, utils_1.xor)(propName in data &&
239
- this.posixRegexMatch(testIMatchValue, data[propName], true), (0, utils_1.xor)(typeIsNot, clauseNot));
335
+ // Convert a POSIX regex to a JS regex.
336
+ pass = xor(propName in data &&
337
+ this.posixRegexMatch(testIMatchValue, data[propName], true), xor(typeIsNot, clauseNot));
338
+ break;
339
+ case 'search':
340
+ case '!search':
341
+ const testSearchValue = curValue[1];
342
+ const tokenString = propName in data
343
+ ? (EntityClass.getFTSText(propName, data[propName]) ??
344
+ '')
345
+ : '';
346
+ pass = xor(propName in data &&
347
+ this.tokenizer.searchString(testSearchValue, tokenString), xor(typeIsNot, clauseNot));
240
348
  break;
241
349
  case 'gt':
242
350
  case '!gt':
243
351
  const testGTValue = curValue[1];
244
- pass = (0, utils_1.xor)(propName in data && data[propName] > testGTValue, (0, utils_1.xor)(typeIsNot, clauseNot));
352
+ pass = xor(propName in data && data[propName] > testGTValue, xor(typeIsNot, clauseNot));
245
353
  break;
246
354
  case 'gte':
247
355
  case '!gte':
248
356
  const testGTEValue = curValue[1];
249
- pass = (0, utils_1.xor)(propName in data && data[propName] >= testGTEValue, (0, utils_1.xor)(typeIsNot, clauseNot));
357
+ pass = xor(propName in data && data[propName] >= testGTEValue, xor(typeIsNot, clauseNot));
250
358
  break;
251
359
  case 'lt':
252
360
  case '!lt':
253
361
  const testLTValue = curValue[1];
254
- pass = (0, utils_1.xor)(propName in data && data[propName] < testLTValue, (0, utils_1.xor)(typeIsNot, clauseNot));
362
+ pass = xor(propName in data && data[propName] < testLTValue, xor(typeIsNot, clauseNot));
255
363
  break;
256
364
  case 'lte':
257
365
  case '!lte':
258
366
  const testLTEValue = curValue[1];
259
- pass = (0, utils_1.xor)(propName in data && data[propName] <= testLTEValue, (0, utils_1.xor)(typeIsNot, clauseNot));
367
+ pass = xor(propName in data && data[propName] <= testLTEValue, xor(typeIsNot, clauseNot));
260
368
  break;
261
369
  }
262
- }
263
- if (!(0, utils_1.xor)(typeIsOr, pass)) {
264
- break;
370
+ if (!xor(typeIsOr, pass)) {
371
+ break;
372
+ }
265
373
  }
266
374
  }
267
375
  }
376
+ if (!xor(typeIsOr, pass)) {
377
+ break;
378
+ }
268
379
  }
269
- if (!(0, utils_1.xor)(typeIsOr, pass)) {
270
- break;
380
+ if (!pass) {
381
+ return false;
271
382
  }
272
383
  }
273
- if (!pass) {
274
- return false;
275
- }
384
+ return true;
385
+ }
386
+ catch (e) {
387
+ this.nymph.config.debugError('nymph', `Failed to check entity data: ${e}`);
388
+ throw e;
276
389
  }
277
- return true;
278
390
  }
391
+ /**
392
+ * Remove all copies of an entity from the cache.
393
+ *
394
+ * @param guid The GUID of the entity to remove.
395
+ */
279
396
  cleanCache(guid) {
280
397
  delete this.entityCache[guid];
281
398
  }
@@ -292,7 +409,15 @@ class NymphDriver {
292
409
  }
293
410
  return ret;
294
411
  }
412
+ /**
413
+ * Search through a value for an entity reference.
414
+ *
415
+ * @param value Any value to search.
416
+ * @param entity An entity, GUID, or array of either to search for.
417
+ * @returns True if the reference is found, false otherwise.
418
+ */
295
419
  entityReferenceSearch(value, entity) {
420
+ // Get the GUID, if the passed $entity is an object.
296
421
  const guids = [];
297
422
  if (Array.isArray(entity)) {
298
423
  if (entity[0] === 'nymph_entity_reference') {
@@ -323,7 +448,8 @@ class NymphDriver {
323
448
  guids.indexOf(value[1]) !== -1) {
324
449
  return true;
325
450
  }
326
- if (Array.isArray(value) || typeof value === 'object') {
451
+ // Search through arrays and objects looking for the reference.
452
+ if (value != null && (Array.isArray(value) || typeof value === 'object')) {
327
453
  for (const key in value) {
328
454
  if (this.entityReferenceSearch(value[key], guids)) {
329
455
  return true;
@@ -332,6 +458,175 @@ class NymphDriver {
332
458
  }
333
459
  return false;
334
460
  }
461
+ removeAndReturnACValues(etype, data, sdata) {
462
+ let user = null;
463
+ let group = null;
464
+ let acUser = null;
465
+ let acGroup = null;
466
+ let acOther = null;
467
+ let acRead = null;
468
+ let acWrite = null;
469
+ let acFull = null;
470
+ if (this.nymph.tilmeld != null) {
471
+ // Since Tilmeld is installed, we need to fill the AC vars.
472
+ const userValue = 'user' in sdata
473
+ ? JSON.parse(sdata.user)
474
+ : 'user' in data
475
+ ? data.user
476
+ : null;
477
+ if (Array.isArray(userValue) &&
478
+ userValue[0] === 'nymph_entity_reference' &&
479
+ userValue[2] === 'User' &&
480
+ typeof userValue[1] === 'string' &&
481
+ userValue[1].match(/^[0-9a-f]{24}$/i)) {
482
+ user = userValue[1];
483
+ }
484
+ const groupValue = 'group' in sdata
485
+ ? JSON.parse(sdata.group)
486
+ : 'group' in data
487
+ ? data.group
488
+ : null;
489
+ if (Array.isArray(groupValue) &&
490
+ groupValue[0] === 'nymph_entity_reference' &&
491
+ groupValue[2] === 'Group' &&
492
+ typeof groupValue[1] === 'string' &&
493
+ groupValue[1].match(/^[0-9a-f]{24}$/i)) {
494
+ group = groupValue[1];
495
+ }
496
+ if (etype !== 'tilmeld_user' && etype !== 'tilmeld_group') {
497
+ const acUserValue = 'acUser' in sdata
498
+ ? JSON.parse(sdata.acUser)
499
+ : 'acUser' in data
500
+ ? data.acUser
501
+ : null;
502
+ if (typeof acUserValue === 'number' &&
503
+ !isNaN(acUserValue) &&
504
+ acUserValue >= 0) {
505
+ acUser = acUserValue;
506
+ }
507
+ const acGroupValue = 'acGroup' in sdata
508
+ ? JSON.parse(sdata.acGroup)
509
+ : 'acGroup' in data
510
+ ? data.acGroup
511
+ : null;
512
+ if (typeof acGroupValue === 'number' &&
513
+ !isNaN(acGroupValue) &&
514
+ acGroupValue >= 0) {
515
+ acGroup = acGroupValue;
516
+ }
517
+ const acOtherValue = 'acOther' in sdata
518
+ ? JSON.parse(sdata.acOther)
519
+ : 'acOther' in data
520
+ ? data.acOther
521
+ : null;
522
+ if (typeof acOtherValue === 'number' &&
523
+ !isNaN(acOtherValue) &&
524
+ acOtherValue >= 0) {
525
+ acOther = acOtherValue;
526
+ }
527
+ const acReadValue = 'acRead' in sdata
528
+ ? JSON.parse(sdata.acRead)
529
+ : 'acRead' in data
530
+ ? data.acRead
531
+ : null;
532
+ if (Array.isArray(acReadValue)) {
533
+ const acReadArray = [];
534
+ for (let acReadEntry of acReadValue) {
535
+ if (typeof acReadEntry === 'string' &&
536
+ acReadEntry.match(/^[0-9a-f]{24}$/i)) {
537
+ acReadArray.push(acReadEntry);
538
+ }
539
+ else if (Array.isArray(acReadEntry) &&
540
+ acReadEntry[0] === 'nymph_entity_reference' &&
541
+ (acReadEntry[2] === 'User' || acReadEntry[2] === 'Group') &&
542
+ typeof acReadEntry[1] === 'string' &&
543
+ acReadEntry[1].match(/^[0-9a-f]{24}$/i)) {
544
+ acReadArray.push(acReadEntry[1]);
545
+ }
546
+ }
547
+ if (acReadArray.length) {
548
+ acRead = acReadArray;
549
+ }
550
+ }
551
+ const acWriteValue = 'acWrite' in sdata
552
+ ? JSON.parse(sdata.acWrite)
553
+ : 'acWrite' in data
554
+ ? data.acWrite
555
+ : null;
556
+ if (Array.isArray(acWriteValue)) {
557
+ const acWriteArray = [];
558
+ for (let acWriteEntry of acWriteValue) {
559
+ if (typeof acWriteEntry === 'string' &&
560
+ acWriteEntry.match(/^[0-9a-f]{24}$/i)) {
561
+ acWriteArray.push(acWriteEntry);
562
+ }
563
+ else if (Array.isArray(acWriteEntry) &&
564
+ acWriteEntry[0] === 'nymph_entity_reference' &&
565
+ (acWriteEntry[2] === 'User' || acWriteEntry[2] === 'Group') &&
566
+ typeof acWriteEntry[1] === 'string' &&
567
+ acWriteEntry[1].match(/^[0-9a-f]{24}$/i)) {
568
+ acWriteArray.push(acWriteEntry[1]);
569
+ }
570
+ }
571
+ if (acWriteArray.length) {
572
+ acWrite = acWriteArray;
573
+ }
574
+ }
575
+ const acFullValue = 'acFull' in sdata
576
+ ? JSON.parse(sdata.acFull)
577
+ : 'acFull' in data
578
+ ? data.acFull
579
+ : null;
580
+ if (Array.isArray(acFullValue)) {
581
+ const acFullArray = [];
582
+ for (let acFullEntry of acFullValue) {
583
+ if (typeof acFullEntry === 'string' &&
584
+ acFullEntry.match(/^[0-9a-f]{24}$/i)) {
585
+ acFullArray.push(acFullEntry);
586
+ }
587
+ else if (Array.isArray(acFullEntry) &&
588
+ acFullEntry[0] === 'nymph_entity_reference' &&
589
+ (acFullEntry[2] === 'User' || acFullEntry[2] === 'Group') &&
590
+ typeof acFullEntry[1] === 'string' &&
591
+ acFullEntry[1].match(/^[0-9a-f]{24}$/i)) {
592
+ acFullArray.push(acFullEntry[1]);
593
+ }
594
+ }
595
+ if (acFullArray.length) {
596
+ acFull = acFullArray;
597
+ }
598
+ }
599
+ }
600
+ // Delete all of the data, so it doesn't go into the database as rows in
601
+ // the data tables.
602
+ delete sdata.user;
603
+ delete data.user;
604
+ delete sdata.group;
605
+ delete data.group;
606
+ delete sdata.acUser;
607
+ delete data.acUser;
608
+ delete sdata.acGroup;
609
+ delete data.acGroup;
610
+ delete sdata.acOther;
611
+ delete data.acOther;
612
+ delete sdata.acRead;
613
+ delete data.acRead;
614
+ delete sdata.acWrite;
615
+ delete data.acWrite;
616
+ delete sdata.acFull;
617
+ delete data.acFull;
618
+ }
619
+ return {
620
+ user,
621
+ group,
622
+ acUser,
623
+ acGroup,
624
+ acOther,
625
+ acRead,
626
+ acWrite,
627
+ acFull,
628
+ };
629
+ }
335
630
  formatSelectors(selectors, options = {}) {
336
631
  const newSelectors = [];
337
632
  const qrefs = [];
@@ -356,9 +651,9 @@ class NymphDriver {
356
651
  for (let i = 0; i < tmpArr.length; i++) {
357
652
  const name = tmpArr[i][0];
358
653
  const [qrefOptions, ...qrefSelectors] = tmpArr[i][1];
359
- const QrefEntityClass = typeof qrefOptions.class === 'string'
654
+ const QrefEntityClass = qrefOptions.class
360
655
  ? this.nymph.getEntityClass(qrefOptions.class)
361
- : qrefOptions.class ?? this.nymph.getEntityClass('Entity');
656
+ : this.nymph.getEntityClass('Entity');
362
657
  const newOptions = {
363
658
  ...qrefOptions,
364
659
  class: QrefEntityClass,
@@ -377,24 +672,28 @@ class NymphDriver {
377
672
  qrefs.push(...newSelectors.qrefs);
378
673
  }
379
674
  else if (!Array.isArray(value)) {
675
+ // @ts-ignore: ts doesn't know what value is here.
380
676
  newSelector[key] = [[value]];
381
677
  }
382
678
  else if (!Array.isArray(value[0])) {
383
679
  if (value.length === 3 &&
384
680
  value[1] == null &&
385
681
  typeof value[2] === 'string') {
386
- newSelector[key] = [[value[0], (0, strtotime_1.default)(value[2]) * 1000]];
682
+ // @ts-ignore: ts doesn't know what value is here.
683
+ newSelector[key] = [[value[0], strtotime(value[2]) * 1000]];
387
684
  }
388
685
  else {
686
+ // @ts-ignore: ts doesn't know what value is here.
389
687
  newSelector[key] = [value];
390
688
  }
391
689
  }
392
690
  else {
691
+ // @ts-ignore: ts doesn't know what value is here.
393
692
  newSelector[key] = value.map((curValue) => {
394
693
  if (curValue.length === 3 &&
395
694
  curValue[1] == null &&
396
695
  typeof curValue[2] === 'string') {
397
- return [curValue[0], (0, strtotime_1.default)(curValue[2]) * 1000];
696
+ return [curValue[0], (strtotime(curValue[2]) || 0) * 1000];
398
697
  }
399
698
  return curValue;
400
699
  });
@@ -416,7 +715,7 @@ class NymphDriver {
416
715
  if (key === 'type') {
417
716
  continue;
418
717
  }
419
- let curQuery = callback(key, value, typeIsOr, typeIsNot);
718
+ let curQuery = callback({ key, value, typeIsOr, typeIsNot });
420
719
  if (curQuery) {
421
720
  if (curSelectorQuery) {
422
721
  curSelectorQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -430,73 +729,116 @@ class NymphDriver {
430
729
  }
431
730
  return queryParts;
432
731
  }
433
- getEntitySync(options = {}, ...selectors) {
434
- if (typeof selectors[0] === 'string') {
435
- selectors = [{ type: '&', guid: selectors[0] }];
436
- }
437
- options.limit = 1;
438
- const entities = this.getEntitiesSync(options, ...selectors);
439
- if (options.return === 'count') {
440
- return entities;
441
- }
442
- if (!entities || !entities.length) {
443
- return null;
444
- }
445
- return entities[0];
446
- }
447
- getEntitesRowLike(options, selectors, performQueryCallback, rowFetchCallback, freeResultCallback, getCountCallback, getGUIDCallback, getTagsAndDatesCallback, getDataNameAndSValueCallback) {
732
+ getEntitiesRowLike(options, selectors, performQueryCallback, rowFetchCallback, freeResultCallback, getCountCallback, getGUIDCallback, getTagsAndDatesCallback, getDataNameAndSValueCallback) {
448
733
  if (!this.isConnected()) {
449
- throw new errors_1.UnableToConnectError('not connected to DB');
734
+ throw new UnableToConnectError('not connected to DB');
450
735
  }
451
736
  for (const selector of selectors) {
452
737
  if (!selector ||
453
738
  Object.keys(selector).length === 1 ||
454
739
  !('type' in selector) ||
455
740
  ['&', '!&', '|', '!|'].indexOf(selector.type) === -1) {
456
- throw new errors_1.InvalidParametersError('Invalid query selector passed: ' + JSON.stringify(selector));
741
+ throw new InvalidParametersError('Invalid query selector passed: ' + JSON.stringify(selector));
457
742
  }
458
743
  }
459
744
  let entities = [];
460
745
  let count = 0;
461
746
  const EntityClass = options.class ?? this.nymph.getEntityClass('Entity');
462
747
  const etype = EntityClass.ETYPE;
748
+ if (options.source === 'client' && options.return === 'object') {
749
+ throw new InvalidParametersError('Object return type not allowed from client.');
750
+ }
751
+ // Check if the requested entity is cached.
463
752
  if (this.nymph.config.cache &&
464
753
  !options.skipCache &&
754
+ selectors.length &&
465
755
  'guid' in selectors[0] &&
466
756
  typeof selectors[0].guid === 'number') {
757
+ // Only safe to use the cache option with no other selectors than a GUID
758
+ // and tags.
467
759
  if (selectors.length === 1 &&
468
760
  selectors[0].type === '&' &&
469
761
  (Object.keys(selectors[0]).length === 2 ||
470
762
  (Object.keys(selectors[0]).length === 3 && 'tag' in selectors[0]))) {
471
- const entity = this.pullCache(selectors[0]['guid'], EntityClass.class, !!options.skipAc);
472
- if (entity != null &&
473
- entity.guid != null &&
763
+ const cacheEntity = this.pullCache(selectors[0]['guid']);
764
+ if (cacheEntity != null &&
765
+ cacheEntity.guid != null &&
474
766
  (!('tag' in selectors[0]) ||
475
- entity.hasTag(...(Array.isArray(selectors[0].tag)
767
+ !(Array.isArray(selectors[0].tag)
476
768
  ? selectors[0].tag
477
- : [selectors[0].tag])))) {
478
- const guid = entity.guid;
479
- return {
480
- result: Promise.resolve(null),
481
- process: () => options.return === 'count'
482
- ? guid
483
- ? 1
484
- : 0
485
- : options.return === 'guid'
486
- ? [guid]
487
- : [entity],
488
- };
769
+ : [selectors[0].tag]).find((tag) => tag == null || !cacheEntity.tags.includes(tag)))) {
770
+ // Return the value from the cache.
771
+ const { guid, cdate, mdate, tags, data, sdata } = cacheEntity;
772
+ if (options.return === 'count') {
773
+ return {
774
+ result: Promise.resolve(null),
775
+ process: () => 1,
776
+ };
777
+ }
778
+ else if (options.return === 'guid') {
779
+ return {
780
+ result: Promise.resolve(null),
781
+ process: () => [guid],
782
+ };
783
+ }
784
+ else if (options.return === 'object') {
785
+ return {
786
+ result: Promise.resolve(null),
787
+ process: () => [
788
+ {
789
+ guid,
790
+ cdate,
791
+ mdate,
792
+ tags,
793
+ ...data,
794
+ ...Object.fromEntries(Object.entries(sdata).map(([name, value]) => [
795
+ name,
796
+ JSON.parse(value),
797
+ ])),
798
+ },
799
+ ],
800
+ };
801
+ }
802
+ else {
803
+ const entity = this.nymph
804
+ .getEntityClass(EntityClass.class)
805
+ .factorySync();
806
+ if (entity) {
807
+ entity.$nymph = this.nymph;
808
+ if (options.skipAc != null) {
809
+ entity.$useSkipAc(!!options.skipAc);
810
+ }
811
+ entity.guid = guid;
812
+ entity.cdate = cdate;
813
+ entity.mdate = mdate;
814
+ entity.tags = tags;
815
+ entity.$putData(data, sdata, 'server');
816
+ return {
817
+ result: Promise.resolve(null),
818
+ process: () => [entity],
819
+ };
820
+ }
821
+ return {
822
+ result: Promise.resolve(null),
823
+ process: () => [],
824
+ };
825
+ }
489
826
  }
490
827
  }
491
828
  }
492
829
  try {
493
830
  const formattedSelectors = this.formatSelectors(selectors, options);
494
831
  this.nymph.runQueryCallbacks(options, formattedSelectors.selectors);
495
- for (let qref of formattedSelectors.qrefs) {
496
- const [options, ...selectors] = qref;
832
+ for (let i = 0; i < formattedSelectors.qrefs.length; i++) {
833
+ const [options, ...selectors] = formattedSelectors.qrefs[i];
497
834
  this.nymph.runQueryCallbacks(options, selectors);
835
+ formattedSelectors.qrefs[i] = [options, ...selectors];
498
836
  }
499
- const { result } = performQueryCallback(options, formattedSelectors.selectors, etype);
837
+ const { result } = performQueryCallback({
838
+ options,
839
+ selectors: formattedSelectors.selectors,
840
+ etype,
841
+ });
500
842
  return {
501
843
  result,
502
844
  process: () => {
@@ -521,9 +863,13 @@ class NymphDriver {
521
863
  const cdate = tagsAndDates.cdate;
522
864
  const mdate = tagsAndDates.mdate;
523
865
  let dataNameAndSValue = getDataNameAndSValueCallback(row);
866
+ // Data.
524
867
  const data = {};
868
+ // Serialized data.
525
869
  const sdata = {};
526
870
  if (dataNameAndSValue.name !== '') {
871
+ // This do will keep going and adding the data until the
872
+ // next entity is reached. $row will end on the next entity.
527
873
  do {
528
874
  dataNameAndSValue = getDataNameAndSValueCallback(row);
529
875
  sdata[dataNameAndSValue.name] = dataNameAndSValue.svalue;
@@ -531,27 +877,85 @@ class NymphDriver {
531
877
  } while (row != null && getGUIDCallback(row) === guid);
532
878
  }
533
879
  else {
880
+ // Make sure that $row is incremented :)
534
881
  row = rowFetchCallback();
535
882
  }
536
- const entity = EntityClass.factorySync();
537
- entity.$nymph = this.nymph;
538
- if (options.skipAc != null) {
539
- entity.$useSkipAc(!!options.skipAc);
883
+ if (this.nymph.tilmeld) {
884
+ if ('user' in tagsAndDates && tagsAndDates.user != null) {
885
+ data.user = [
886
+ 'nymph_entity_reference',
887
+ tagsAndDates.user,
888
+ 'User',
889
+ ];
890
+ delete sdata.user;
891
+ }
892
+ if ('group' in tagsAndDates && tagsAndDates.group != null) {
893
+ data.group = [
894
+ 'nymph_entity_reference',
895
+ tagsAndDates.group,
896
+ 'Group',
897
+ ];
898
+ delete sdata.group;
899
+ }
900
+ if ('acUser' in tagsAndDates && tagsAndDates.acUser != null) {
901
+ data.acUser = tagsAndDates.acUser;
902
+ delete sdata.acUser;
903
+ }
904
+ if ('acGroup' in tagsAndDates && tagsAndDates.acGroup != null) {
905
+ data.acGroup = tagsAndDates.acGroup;
906
+ delete sdata.acGroup;
907
+ }
908
+ if ('acOther' in tagsAndDates && tagsAndDates.acOther != null) {
909
+ data.acOther = tagsAndDates.acOther;
910
+ delete sdata.acOther;
911
+ }
912
+ if ('acRead' in tagsAndDates && tagsAndDates.acRead != null) {
913
+ data.acRead = tagsAndDates.acRead;
914
+ delete sdata.acRead;
915
+ }
916
+ if ('acWrite' in tagsAndDates && tagsAndDates.acWrite != null) {
917
+ data.acWrite = tagsAndDates.acWrite;
918
+ delete sdata.acWrite;
919
+ }
920
+ if ('acFull' in tagsAndDates && tagsAndDates.acFull != null) {
921
+ data.acFull = tagsAndDates.acFull;
922
+ delete sdata.acFull;
923
+ }
540
924
  }
541
- entity.guid = guid;
542
- entity.cdate = cdate;
543
- entity.mdate = mdate;
544
- entity.tags = tags;
545
- this.putDataCounter++;
546
- if (this.putDataCounter == 100) {
547
- throw new Error('Infinite loop detected in Entity loading.');
925
+ if (options.return === 'object') {
926
+ entities.push({
927
+ guid,
928
+ cdate,
929
+ mdate,
930
+ tags,
931
+ ...data,
932
+ ...Object.fromEntries(Object.entries(sdata).map(([name, value]) => [
933
+ name,
934
+ JSON.parse(value),
935
+ ])),
936
+ });
937
+ }
938
+ else {
939
+ const entity = EntityClass.factorySync();
940
+ entity.$nymph = this.nymph;
941
+ if (options.skipAc != null) {
942
+ entity.$useSkipAc(!!options.skipAc);
943
+ }
944
+ entity.guid = guid;
945
+ entity.cdate = cdate;
946
+ entity.mdate = mdate;
947
+ entity.tags = tags;
948
+ this.putDataCounter++;
949
+ if (this.putDataCounter == 100) {
950
+ throw new Error('Infinite loop detected in Entity loading.');
951
+ }
952
+ entity.$putData(data, sdata, 'server');
953
+ this.putDataCounter--;
954
+ entities.push(entity);
548
955
  }
549
- entity.$putData(data, sdata);
550
- this.putDataCounter--;
551
956
  if (this.nymph.config.cache) {
552
957
  this.pushCache(guid, cdate, mdate, tags, data, sdata);
553
958
  }
554
- entities.push(entity);
555
959
  }
556
960
  }
557
961
  freeResultCallback();
@@ -569,89 +973,160 @@ class NymphDriver {
569
973
  }
570
974
  }
571
975
  async saveEntityRowLike(entity, saveNewEntityCallback, saveExistingEntityCallback, startTransactionCallback = null, commitTransactionCallback = null) {
572
- const mdate = Date.now();
573
- const tags = (0, lodash_1.difference)(entity.tags, ['']);
976
+ const originalGuid = entity.guid;
977
+ const originalCdate = entity.cdate;
978
+ const originalMdate = entity.mdate;
979
+ // Get a modified date.
980
+ let mdate = Date.now();
981
+ if (entity.mdate != null) {
982
+ if (this.nymph.config.updateMDate === false) {
983
+ mdate = entity.mdate;
984
+ }
985
+ else if (typeof this.nymph.config.updateMDate === 'number') {
986
+ mdate = entity.mdate + this.nymph.config.updateMDate;
987
+ }
988
+ else if (entity.mdate >= mdate) {
989
+ // If we're saving too fast (within 1 millisecond), increment mdate to
990
+ // make sure it doesn't conflict in the DB.
991
+ mdate = entity.mdate + 1;
992
+ }
993
+ }
994
+ const tags = difference(entity.tags, ['']);
574
995
  const data = entity.$getData();
575
996
  const sdata = entity.$getSData();
997
+ const uniques = await entity.$getUniques();
576
998
  const EntityClass = entity.constructor;
577
999
  const etype = EntityClass.ETYPE;
578
1000
  if (startTransactionCallback) {
579
1001
  await startTransactionCallback();
580
1002
  }
581
1003
  let success = false;
582
- if (entity.guid == null) {
583
- const cdate = mdate;
584
- const newId = (0, guid_1.guid)();
585
- success = await saveNewEntityCallback(entity, newId, tags, data, sdata, cdate, etype);
586
- if (success) {
587
- entity.guid = newId;
588
- entity.cdate = cdate;
589
- entity.mdate = mdate;
1004
+ try {
1005
+ if (entity.guid == null) {
1006
+ const cdate = mdate;
1007
+ const newId = entity.$getGuaranteedGUID();
1008
+ success = await saveNewEntityCallback({
1009
+ entity,
1010
+ guid: newId,
1011
+ tags,
1012
+ data,
1013
+ sdata,
1014
+ uniques,
1015
+ cdate,
1016
+ etype,
1017
+ });
1018
+ if (success) {
1019
+ entity.guid = newId;
1020
+ entity.cdate = cdate;
1021
+ entity.mdate = mdate;
1022
+ }
590
1023
  }
591
- }
592
- else {
593
- if (this.nymph.config.cache) {
594
- this.cleanCache(entity.guid);
1024
+ else {
1025
+ // Removed any cached versions of this entity.
1026
+ if (this.nymph.config.cache) {
1027
+ this.cleanCache(entity.guid);
1028
+ }
1029
+ success = await saveExistingEntityCallback({
1030
+ entity,
1031
+ guid: entity.guid,
1032
+ tags,
1033
+ data,
1034
+ sdata,
1035
+ uniques,
1036
+ mdate,
1037
+ etype,
1038
+ });
1039
+ if (success) {
1040
+ entity.mdate = mdate;
1041
+ }
1042
+ }
1043
+ if (commitTransactionCallback) {
1044
+ success = await commitTransactionCallback(success);
595
1045
  }
596
- success = await saveExistingEntityCallback(entity, entity.guid, tags, data, sdata, mdate, etype);
597
- if (success) {
598
- entity.mdate = mdate;
1046
+ // Cache the entity.
1047
+ if (success && this.nymph.config.cache) {
1048
+ this.pushCache(entity.guid, entity.cdate, entity.mdate, entity.tags, data, sdata);
599
1049
  }
600
1050
  }
601
- if (commitTransactionCallback) {
602
- success = await commitTransactionCallback(success);
1051
+ catch (e) {
1052
+ entity.guid = originalGuid;
1053
+ entity.cdate = originalCdate;
1054
+ entity.mdate = originalMdate;
1055
+ throw e;
603
1056
  }
604
- if (success && this.nymph.config.cache) {
605
- this.pushCache(entity.guid, entity.cdate, entity.mdate, entity.tags, data, sdata);
1057
+ if (!success) {
1058
+ entity.guid = originalGuid;
1059
+ entity.cdate = originalCdate;
1060
+ entity.mdate = originalMdate;
606
1061
  }
607
1062
  return success;
608
1063
  }
609
- pullCache(guid, className, useSkipAc = false) {
1064
+ /**
1065
+ * Pull an entity from the cache.
1066
+ *
1067
+ * @param guid The entity's GUID.
1068
+ * @returns The entity's data or null if it's not cached.
1069
+ */
1070
+ pullCache(guid) {
1071
+ // Increment the entity access count.
610
1072
  if (!(guid in this.entityCount)) {
611
1073
  this.entityCount[guid] = 0;
612
1074
  }
613
1075
  this.entityCount[guid]++;
614
1076
  if (guid in this.entityCache) {
615
- const entity = this.nymph.getEntityClass(className).factorySync();
616
- if (entity) {
617
- entity.$nymph = this.nymph;
618
- entity.$useSkipAc(!!useSkipAc);
619
- entity.guid = guid;
620
- entity.cdate = this.entityCache[guid]['cdate'];
621
- entity.mdate = this.entityCache[guid]['mdate'];
622
- entity.tags = this.entityCache[guid]['tags'];
623
- entity.$putData(this.entityCache[guid]['data'], this.entityCache[guid]['sdata']);
624
- return entity;
625
- }
1077
+ return {
1078
+ guid,
1079
+ cdate: this.entityCache[guid]['cdate'],
1080
+ mdate: this.entityCache[guid]['mdate'],
1081
+ tags: this.entityCache[guid]['tags'],
1082
+ data: this.entityCache[guid]['data'],
1083
+ sdata: this.entityCache[guid]['sdata'],
1084
+ };
626
1085
  }
627
1086
  return null;
628
1087
  }
1088
+ /**
1089
+ * Push an entity onto the cache.
1090
+ *
1091
+ * @param guid The entity's GUID.
1092
+ * @param cdate The entity's cdate.
1093
+ * @param mdate The entity's mdate.
1094
+ * @param tags The entity's tags.
1095
+ * @param data The entity's data.
1096
+ * @param sdata The entity's sdata.
1097
+ */
629
1098
  pushCache(guid, cdate, mdate, tags, data, sdata) {
630
1099
  if (guid == null) {
631
1100
  return;
632
1101
  }
1102
+ // Increment the entity access count.
633
1103
  if (!(guid in this.entityCount)) {
634
1104
  this.entityCount[guid] = 0;
635
1105
  }
636
1106
  this.entityCount[guid]++;
1107
+ // Check the threshold.
637
1108
  if (this.entityCount[guid] < this.nymph.config.cacheThreshold) {
638
1109
  return;
639
1110
  }
1111
+ // Cache the entity.
640
1112
  this.entityCache[guid] = {
641
- cdate: cdate,
642
- mdate: mdate,
643
- tags: tags,
644
- data: data,
645
- sdata: sdata,
1113
+ cdate,
1114
+ mdate,
1115
+ tags,
1116
+ data,
1117
+ sdata,
646
1118
  };
647
1119
  while (this.nymph.config.cacheLimit &&
648
1120
  Object.keys(this.entityCache).length >= this.nymph.config.cacheLimit) {
1121
+ // Find which entity has been accessed the least.
649
1122
  const least = Object.entries(this.entityCount)
650
- .sort(([guidA, countA], [guidB, countB]) => countA - countB)
1123
+ .sort(([_guidA, countA], [_guidB, countB]) => countA - countB)
651
1124
  .pop()?.[0] ?? null;
652
1125
  if (least == null) {
1126
+ // This should never happen.
653
1127
  return;
654
1128
  }
1129
+ // Remove it.
655
1130
  delete this.entityCache[least];
656
1131
  delete this.entityCount[least];
657
1132
  }
@@ -665,5 +1140,4 @@ class NymphDriver {
665
1140
  return matches.map((match) => match.replace(re, '$1'));
666
1141
  }
667
1142
  }
668
- exports.default = NymphDriver;
669
1143
  //# sourceMappingURL=NymphDriver.js.map