@nymphjs/nymph 1.0.0-beta.13 → 1.0.0-beta.131

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