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

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