@nymphjs/driver-mysql 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.
@@ -1,21 +1,30 @@
1
- import cp from 'child_process';
2
- // @ts-ignore: types are wonky with @vlasky/mysql.
3
- import { default as MySQLType } from '@types/mysql';
4
- // @ts-ignore: replace with mysql once https://github.com/mysqljs/mysql/pull/2233 is merged.
5
- import vlaskyMysql from '@vlasky/mysql';
1
+ import { Pool, PoolOptions, PoolConnection } from 'mysql2';
2
+ import mysql from 'mysql2';
3
+ import SqlString from 'sqlstring';
4
+ import type {
5
+ SearchTerm,
6
+ SearchOrTerm,
7
+ SearchNotTerm,
8
+ SearchSeriesTerm,
9
+ } from '@sciactive/tokenizer';
6
10
  import {
7
11
  NymphDriver,
8
- EntityConstructor,
9
- EntityData,
10
- EntityInterface,
11
- SerializedEntityData,
12
+ type EntityConstructor,
13
+ type EntityData,
14
+ type EntityObjectType,
15
+ type EntityInterface,
16
+ type EntityInstanceType,
17
+ type SerializedEntityData,
18
+ type FormattedSelector,
19
+ type Options,
20
+ type Selector,
21
+ EntityConflictError,
22
+ EntityInvalidDataError,
23
+ EntityUniqueConstraintError,
12
24
  InvalidParametersError,
13
25
  NotConfiguredError,
14
26
  QueryFailedError,
15
27
  UnableToConnectError,
16
- FormattedSelector,
17
- Options,
18
- Selector,
19
28
  xor,
20
29
  } from '@nymphjs/nymph';
21
30
  import { makeTableSuffix } from '@nymphjs/guid';
@@ -23,29 +32,28 @@ import { makeTableSuffix } from '@nymphjs/guid';
23
32
  import {
24
33
  MySQLDriverConfig,
25
34
  MySQLDriverConfigDefaults as defaults,
26
- } from './conf';
35
+ } from './conf/index.js';
27
36
 
28
37
  type MySQLDriverTransaction = {
29
- connection: MySQLType.PoolConnection | null;
38
+ connection: PoolConnection | null;
30
39
  count: number;
40
+ latestPromise: Promise<any>;
31
41
  };
32
42
 
33
- const mysql = vlaskyMysql as typeof MySQLType;
34
-
35
43
  /**
36
44
  * The MySQL Nymph database driver.
37
45
  */
38
46
  export default class MySQLDriver extends NymphDriver {
39
47
  public config: MySQLDriverConfig;
40
- private mysqlConfig: MySQLType.PoolConfig;
48
+ private mysqlConfig: PoolOptions;
41
49
  protected prefix: string;
42
50
  protected connected: boolean = false;
43
51
  // @ts-ignore: this is assigned in connect(), which is called by the constructor.
44
- protected link: MySQLType.Pool;
52
+ protected link: Pool;
45
53
  protected transaction: MySQLDriverTransaction | null = null;
46
54
 
47
55
  static escape(input: string) {
48
- return mysql.escapeId(input);
56
+ return SqlString.escapeId(input, true);
49
57
  }
50
58
 
51
59
  static escapeValue(input: string) {
@@ -54,8 +62,8 @@ export default class MySQLDriver extends NymphDriver {
54
62
 
55
63
  constructor(
56
64
  config: Partial<MySQLDriverConfig>,
57
- link?: MySQLType.Pool,
58
- transaction?: MySQLDriverTransaction
65
+ link?: Pool,
66
+ transaction?: MySQLDriverTransaction,
59
67
  ) {
60
68
  super();
61
69
  this.config = { ...defaults, ...config };
@@ -90,16 +98,16 @@ export default class MySQLDriver extends NymphDriver {
90
98
  return new MySQLDriver(
91
99
  this.config,
92
100
  this.link,
93
- this.transaction ?? undefined
101
+ this.transaction ?? undefined,
94
102
  );
95
103
  }
96
104
 
97
- private getConnection(): Promise<MySQLType.PoolConnection> {
105
+ private getConnection(): Promise<PoolConnection> {
98
106
  if (this.transaction != null && this.transaction.connection != null) {
99
107
  return Promise.resolve(this.transaction.connection);
100
108
  }
101
109
  return new Promise((resolve, reject) =>
102
- this.link.getConnection((err, con) => (err ? reject(err) : resolve(con)))
110
+ this.link.getConnection((err, con) => (err ? reject(err) : resolve(con))),
103
111
  );
104
112
  }
105
113
 
@@ -112,16 +120,11 @@ export default class MySQLDriver extends NymphDriver {
112
120
  // If we think we're connected, try pinging the server.
113
121
  try {
114
122
  if (this.connected) {
115
- const connection: MySQLType.PoolConnection = await new Promise(
123
+ const connection: PoolConnection = await new Promise(
116
124
  (resolve, reject) =>
117
125
  this.link.getConnection((err, con) =>
118
- err ? reject(err) : resolve(con)
119
- )
120
- );
121
- await new Promise((resolve) =>
122
- connection.ping(() => {
123
- resolve(0);
124
- })
126
+ err ? reject(err) : resolve(con),
127
+ ),
125
128
  );
126
129
  connection.release();
127
130
  }
@@ -132,7 +135,10 @@ export default class MySQLDriver extends NymphDriver {
132
135
  // Connecting, selecting database
133
136
  if (!this.connected) {
134
137
  try {
135
- this.link = mysql.createPool(this.mysqlConfig);
138
+ this.link = mysql.createPool({
139
+ ...this.mysqlConfig,
140
+ charset: 'utf8mb4_bin',
141
+ });
136
142
  this.connected = true;
137
143
  } catch (e: any) {
138
144
  if (
@@ -142,7 +148,7 @@ export default class MySQLDriver extends NymphDriver {
142
148
  this.mysqlConfig.database === 'nymph'
143
149
  ) {
144
150
  throw new NotConfiguredError(
145
- "It seems the config hasn't been set up correctly."
151
+ "It seems the config hasn't been set up correctly.",
146
152
  );
147
153
  } else {
148
154
  throw new UnableToConnectError('Could not connect: ' + e?.message);
@@ -166,6 +172,9 @@ export default class MySQLDriver extends NymphDriver {
166
172
  }
167
173
 
168
174
  public async inTransaction() {
175
+ if (this.transaction && this.transaction.count === 0) {
176
+ this.transaction = null;
177
+ }
169
178
  return !!this.transaction;
170
179
  }
171
180
 
@@ -178,106 +187,271 @@ export default class MySQLDriver extends NymphDriver {
178
187
  return this.connected;
179
188
  }
180
189
 
181
- /**
182
- * Create entity tables in the database.
183
- *
184
- * @param etype The entity type to create a table for. If this is blank, the default tables are created.
185
- * @returns True on success, false on failure.
186
- */
187
- private createTables(etype: string | null = null) {
188
- this.queryRunSync('SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";');
189
- let foreignKeyDataTableGuid = '';
190
- let foreignKeyDataComparisonsTableGuid = '';
191
- let foreignKeyReferencesTableGuid = '';
192
- if (this.config.foreignKeys) {
193
- foreignKeyDataTableGuid = ` REFERENCES ${MySQLDriver.escape(
194
- `${this.prefix}entities_${etype}`
195
- )}(\`guid\`) ON DELETE CASCADE`;
196
- foreignKeyDataComparisonsTableGuid = ` REFERENCES ${MySQLDriver.escape(
197
- `${this.prefix}entities_${etype}`
198
- )}(\`guid\`) ON DELETE CASCADE`;
199
- foreignKeyReferencesTableGuid = ` REFERENCES ${MySQLDriver.escape(
200
- `${this.prefix}entities_${etype}`
201
- )}(\`guid\`) ON DELETE CASCADE`;
202
- }
203
- if (etype != null) {
204
- // Create the entity table.
205
- this.queryRunSync(
206
- `CREATE TABLE IF NOT EXISTS ${MySQLDriver.escape(
207
- `${this.prefix}entities_${etype}`
208
- )} (
190
+ private async createEntitiesTable(etype: string) {
191
+ // Create the entity table.
192
+ await this.queryRun(
193
+ `CREATE TABLE IF NOT EXISTS ${MySQLDriver.escape(
194
+ `${this.prefix}entities_${etype}`,
195
+ )} (
209
196
  \`guid\` BINARY(12) NOT NULL,
210
197
  \`tags\` LONGTEXT,
211
198
  \`cdate\` DOUBLE PRECISION NOT NULL,
212
199
  \`mdate\` DOUBLE PRECISION NOT NULL,
200
+ \`user\` BINARY(12),
201
+ \`group\` BINARY(12),
202
+ \`acUser\` TINYINT UNSIGNED,
203
+ \`acGroup\` TINYINT UNSIGNED,
204
+ \`acOther\` TINYINT UNSIGNED,
205
+ \`acRead\` LONGTEXT,
206
+ \`acWrite\` LONGTEXT,
207
+ \`acFull\` LONGTEXT,
213
208
  PRIMARY KEY (\`guid\`),
209
+ INDEX \`id_guid\` USING HASH (\`guid\`),
214
210
  INDEX \`id_cdate\` USING BTREE (\`cdate\`),
215
211
  INDEX \`id_mdate\` USING BTREE (\`mdate\`),
216
- FULLTEXT \`id_tags\` (\`tags\`)
217
- ) ENGINE ${this.config.engine}
218
- CHARACTER SET utf8 COLLATE utf8_bin;`
219
- );
220
- // Create the data table.
221
- this.queryRunSync(
222
- `CREATE TABLE IF NOT EXISTS ${MySQLDriver.escape(
223
- `${this.prefix}data_${etype}`
224
- )} (
225
- \`guid\` BINARY(12) NOT NULL${foreignKeyDataTableGuid},
226
- \`name\` TEXT NOT NULL,
227
- \`value\` LONGTEXT NOT NULL,
228
- PRIMARY KEY (\`guid\`,\`name\`(255)),
229
- INDEX \`id_name\` USING HASH (\`name\`(255)),
230
- INDEX \`id_name_value\` USING BTREE (\`name\`(255), \`value\`(512))
212
+ FULLTEXT \`id_tags\` (\`tags\`),
213
+ INDEX \`id_user_acUser\` USING BTREE (\`user\`, \`acUser\`),
214
+ INDEX \`id_group_acGroup\` USING BTREE (\`group\`, \`acGroup\`),
215
+ INDEX \`id_acUser\` USING BTREE (\`acUser\`),
216
+ INDEX \`id_acGroup\` USING BTREE (\`acGroup\`),
217
+ INDEX \`id_acOther\` USING BTREE (\`acOther\`),
218
+ FULLTEXT \`id_acRead\` (\`acRead\`),
219
+ FULLTEXT \`id_acWrite\` (\`acWrite\`),
220
+ FULLTEXT \`id_acFull\` (\`acFull\`)
231
221
  ) ENGINE ${this.config.engine}
232
- CHARACTER SET utf8 COLLATE utf8_bin;`
233
- );
234
- // Create the data comparisons table.
235
- this.queryRunSync(
236
- `CREATE TABLE IF NOT EXISTS ${MySQLDriver.escape(
237
- `${this.prefix}comparisons_${etype}`
238
- )} (
239
- \`guid\` BINARY(12) NOT NULL${foreignKeyDataComparisonsTableGuid},
222
+ CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`,
223
+ );
224
+ }
225
+
226
+ private async addTilmeldColumnsAndIndexes(etype: string) {
227
+ await this.queryRun(
228
+ `ALTER TABLE ${MySQLDriver.escape(
229
+ `${this.prefix}entities_${etype}`,
230
+ )} ADD COLUMN \`user\` BINARY(12);`,
231
+ );
232
+ await this.queryRun(
233
+ `ALTER TABLE ${MySQLDriver.escape(
234
+ `${this.prefix}entities_${etype}`,
235
+ )} ADD COLUMN \`group\` BINARY(12);`,
236
+ );
237
+ await this.queryRun(
238
+ `ALTER TABLE ${MySQLDriver.escape(
239
+ `${this.prefix}entities_${etype}`,
240
+ )} ADD COLUMN \`acUser\` TINYINT UNSIGNED;`,
241
+ );
242
+ await this.queryRun(
243
+ `ALTER TABLE ${MySQLDriver.escape(
244
+ `${this.prefix}entities_${etype}`,
245
+ )} ADD COLUMN \`acGroup\` TINYINT UNSIGNED;`,
246
+ );
247
+ await this.queryRun(
248
+ `ALTER TABLE ${MySQLDriver.escape(
249
+ `${this.prefix}entities_${etype}`,
250
+ )} ADD COLUMN \`acOther\` TINYINT UNSIGNED;`,
251
+ );
252
+ await this.queryRun(
253
+ `ALTER TABLE ${MySQLDriver.escape(
254
+ `${this.prefix}entities_${etype}`,
255
+ )} ADD COLUMN \`acRead\` LONGTEXT;`,
256
+ );
257
+ await this.queryRun(
258
+ `ALTER TABLE ${MySQLDriver.escape(
259
+ `${this.prefix}entities_${etype}`,
260
+ )} ADD COLUMN \`acWrite\` LONGTEXT;`,
261
+ );
262
+ await this.queryRun(
263
+ `ALTER TABLE ${MySQLDriver.escape(
264
+ `${this.prefix}entities_${etype}`,
265
+ )} ADD COLUMN \`acFull\` LONGTEXT;`,
266
+ );
267
+ await this.createEntitiesTilmeldIndexes(etype);
268
+ }
269
+
270
+ private async createEntitiesTilmeldIndexes(etype: string) {
271
+ await this.queryRun(
272
+ `CREATE INDEX \`id_user_acUser\` ON ${MySQLDriver.escape(
273
+ `${this.prefix}entities_${etype}`,
274
+ )} (\`user\`, \`acUser\`) USING BTREE;`,
275
+ );
276
+ await this.queryRun(
277
+ `CREATE INDEX \`id_group_acGroup\` ON ${MySQLDriver.escape(
278
+ `${this.prefix}entities_${etype}`,
279
+ )} (\`group\`, \`acGroup\`) USING BTREE;`,
280
+ );
281
+ await this.queryRun(
282
+ `CREATE INDEX \`id_acUser\` ON ${MySQLDriver.escape(
283
+ `${this.prefix}entities_${etype}`,
284
+ )} (\`acUser\`) USING BTREE;`,
285
+ );
286
+ await this.queryRun(
287
+ `CREATE INDEX \`id_acGroup\` ON ${MySQLDriver.escape(
288
+ `${this.prefix}entities_${etype}`,
289
+ )} (\`acGroup\`) USING BTREE;`,
290
+ );
291
+ await this.queryRun(
292
+ `CREATE INDEX \`id_acOther\` ON ${MySQLDriver.escape(
293
+ `${this.prefix}entities_${etype}`,
294
+ )} (\`acOther\`) USING BTREE;`,
295
+ );
296
+ await this.queryRun(
297
+ `CREATE FULLTEXT INDEX \`id_acRead\` ON ${MySQLDriver.escape(
298
+ `${this.prefix}entities_${etype}`,
299
+ )} (\`acRead\`);`,
300
+ );
301
+ await this.queryRun(
302
+ `CREATE FULLTEXT INDEX \`id_acWrite\` ON ${MySQLDriver.escape(
303
+ `${this.prefix}entities_${etype}`,
304
+ )} (\`acWrite\`);`,
305
+ );
306
+ await this.queryRun(
307
+ `CREATE FULLTEXT INDEX \`id_acFull\` ON ${MySQLDriver.escape(
308
+ `${this.prefix}entities_${etype}`,
309
+ )} (\`acFull\`);`,
310
+ );
311
+ }
312
+
313
+ private async createDataTable(etype: string) {
314
+ let foreignKey = '';
315
+ if (this.config.foreignKeys) {
316
+ foreignKey = ` REFERENCES ${MySQLDriver.escape(
317
+ `${this.prefix}entities_${etype}`,
318
+ )}(\`guid\`) ON DELETE CASCADE`;
319
+ }
320
+ // Create the data table.
321
+ await this.queryRun(
322
+ `CREATE TABLE IF NOT EXISTS ${MySQLDriver.escape(
323
+ `${this.prefix}data_${etype}`,
324
+ )} (
325
+ \`guid\` BINARY(12) NOT NULL${foreignKey},
240
326
  \`name\` TEXT NOT NULL,
241
- \`truthy\` BOOLEAN,
327
+ \`value\` CHAR(1) NOT NULL,
328
+ \`json\` JSON,
242
329
  \`string\` LONGTEXT,
243
330
  \`number\` DOUBLE,
331
+ \`truthy\` BOOLEAN,
244
332
  PRIMARY KEY (\`guid\`, \`name\`(255)),
245
- INDEX \`id_name\` USING HASH (\`name\`(255))
333
+ INDEX \`id_name_guid\` USING BTREE (\`name\`(255), \`guid\`),
334
+ INDEX \`id_name_string_guid\` USING BTREE (\`name\`(255), \`string\`(500), \`guid\`),
335
+ INDEX \`id_name_number_guid\` USING BTREE (\`name\`(255), \`number\`, \`guid\`),
336
+ INDEX \`id_name_truthy_guid\` USING BTREE (\`name\`(255), \`truthy\`, \`guid\`),
337
+ INDEX \`id_name_json_guid\` USING BTREE (\`name\`(255), (CAST(\`json\` AS CHAR(500)) COLLATE utf8mb4_bin), \`guid\`)
246
338
  ) ENGINE ${this.config.engine}
247
- CHARACTER SET utf8 COLLATE utf8_bin;`
248
- );
249
- // Create the references table.
250
- this.queryRunSync(
251
- `CREATE TABLE IF NOT EXISTS ${MySQLDriver.escape(
252
- `${this.prefix}references_${etype}`
253
- )} (
254
- \`guid\` BINARY(12) NOT NULL${foreignKeyReferencesTableGuid},
339
+ CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`,
340
+ );
341
+ }
342
+
343
+ private async createReferencesTable(etype: string) {
344
+ let foreignKey = '';
345
+ if (this.config.foreignKeys) {
346
+ foreignKey = ` REFERENCES ${MySQLDriver.escape(
347
+ `${this.prefix}entities_${etype}`,
348
+ )}(\`guid\`) ON DELETE CASCADE`;
349
+ }
350
+ // Create the references table.
351
+ await this.queryRun(
352
+ `CREATE TABLE IF NOT EXISTS ${MySQLDriver.escape(
353
+ `${this.prefix}references_${etype}`,
354
+ )} (
355
+ \`guid\` BINARY(12) NOT NULL${foreignKey},
255
356
  \`name\` TEXT NOT NULL,
256
357
  \`reference\` BINARY(12) NOT NULL,
257
358
  PRIMARY KEY (\`guid\`, \`name\`(255), \`reference\`),
258
- INDEX \`id_name_reference\` USING BTREE (\`name\`(255), \`reference\`)
359
+ INDEX \`id_name_reference_guid\` USING HASH (\`name\`(255), \`reference\`, \`guid\`)
259
360
  ) ENGINE ${this.config.engine}
260
- CHARACTER SET utf8 COLLATE utf8_bin;`
261
- );
361
+ CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`,
362
+ );
363
+ }
364
+
365
+ private async createTokensTable(etype: string) {
366
+ let foreignKey = '';
367
+ if (this.config.foreignKeys) {
368
+ foreignKey = ` REFERENCES ${MySQLDriver.escape(
369
+ `${this.prefix}entities_${etype}`,
370
+ )}(\`guid\`) ON DELETE CASCADE`;
371
+ }
372
+ // Create the tokens table.
373
+ await this.queryRun(
374
+ `CREATE TABLE IF NOT EXISTS ${MySQLDriver.escape(
375
+ `${this.prefix}tokens_${etype}`,
376
+ )} (
377
+ \`guid\` BINARY(12) NOT NULL${foreignKey},
378
+ \`name\` TEXT NOT NULL,
379
+ \`token\` INT NOT NULL,
380
+ \`position\` INT UNSIGNED NOT NULL,
381
+ \`stem\` BOOLEAN NOT NULL,
382
+ PRIMARY KEY (\`guid\`, \`name\`(255), \`token\`, \`position\`),
383
+ INDEX \`id_name_token_guid\` USING btree (\`name\`(255), \`token\`, \`guid\`)
384
+ ) ENGINE ${this.config.engine}
385
+ CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`,
386
+ );
387
+ }
388
+
389
+ private async createUniquesTable(etype: string) {
390
+ let foreignKey = '';
391
+ if (this.config.foreignKeys) {
392
+ foreignKey = ` REFERENCES ${MySQLDriver.escape(
393
+ `${this.prefix}entities_${etype}`,
394
+ )}(\`guid\`) ON DELETE CASCADE`;
395
+ }
396
+ // Create the unique strings table.
397
+ // 255 key is max length for charset UTF8.
398
+ await this.queryRun(
399
+ `CREATE TABLE IF NOT EXISTS ${MySQLDriver.escape(
400
+ `${this.prefix}uniques_${etype}`,
401
+ )} (
402
+ \`guid\` BINARY(12) NOT NULL${foreignKey},
403
+ \`unique\` TEXT NOT NULL,
404
+ PRIMARY KEY (\`guid\`, \`unique\`(255)),
405
+ INDEX \`id_guid\` USING HASH (\`guid\`),
406
+ CONSTRAINT \`uc_unique\` UNIQUE (\`unique\`(255))
407
+ ) ENGINE ${this.config.engine}
408
+ CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`,
409
+ );
410
+ }
411
+
412
+ /**
413
+ * Create entity tables in the database.
414
+ *
415
+ * @param etype The entity type to create a table for. If this is blank, the default tables are created.
416
+ * @returns True on success, false on failure.
417
+ */
418
+ private async createTables(etype: string | null = null) {
419
+ await this.queryRun('SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";');
420
+ if (etype != null) {
421
+ await this.createEntitiesTable(etype);
422
+ await this.createDataTable(etype);
423
+ await this.createReferencesTable(etype);
424
+ await this.createTokensTable(etype);
425
+ await this.createUniquesTable(etype);
262
426
  } else {
263
427
  // Create the UID table.
264
- this.queryRunSync(
428
+ await this.queryRun(
265
429
  `CREATE TABLE IF NOT EXISTS ${MySQLDriver.escape(
266
- `${this.prefix}uids`
430
+ `${this.prefix}uids`,
267
431
  )} (
268
432
  \`name\` TEXT NOT NULL,
269
433
  \`cur_uid\` BIGINT(20) UNSIGNED NOT NULL,
270
434
  PRIMARY KEY (\`name\`(100))
271
435
  ) ENGINE ${this.config.engine}
272
- CHARACTER SET utf8 COLLATE utf8_bin;`
436
+ CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;`,
273
437
  );
274
438
  }
439
+ if (
440
+ this.transaction &&
441
+ this.transaction.count > 0 &&
442
+ this.config.transactions
443
+ ) {
444
+ this.transaction.count = 0;
445
+ this.transaction.connection?.release();
446
+ this.transaction.connection = null;
447
+ this.transaction = null;
448
+ }
275
449
  return true;
276
450
  }
277
451
 
278
452
  private translateQuery(
279
453
  origQuery: string,
280
- origParams: { [k: string]: any }
454
+ origParams: { [k: string]: any },
281
455
  ): { query: string; params: any[] } {
282
456
  const params: any[] = [];
283
457
  let query = origQuery;
@@ -296,32 +470,37 @@ export default class MySQLDriver extends NymphDriver {
296
470
  private async query<T extends () => any>(
297
471
  runQuery: T,
298
472
  query: string,
299
- etypes: string[] = []
473
+ etypes: string[] = [],
300
474
  // @ts-ignore: The return type of T is a promise.
301
475
  ): ReturnType<T> {
302
476
  try {
477
+ this.nymph.config.debugInfo('mysql:query', query);
303
478
  return await runQuery();
304
479
  } catch (e: any) {
305
480
  const errorCode = e?.errno;
306
- if (errorCode === 1146 && this.createTables()) {
481
+ if (errorCode === 1146 && (await this.createTables())) {
307
482
  // If the tables don't exist yet, create them.
308
483
  for (let etype of etypes) {
309
- this.createTables(etype);
484
+ await this.createTables(etype);
310
485
  }
311
486
  try {
312
487
  return await runQuery();
313
488
  } catch (e2: any) {
314
489
  throw new QueryFailedError(
315
490
  'Query failed: ' + e2?.errno + ' - ' + e2?.message,
316
- query
491
+ query,
492
+ e2?.errno,
317
493
  );
318
494
  }
495
+ } else if (errorCode === 1062) {
496
+ throw new EntityUniqueConstraintError(`Unique constraint violation.`);
319
497
  } else if (errorCode === 2006) {
320
498
  // If the MySQL server disconnected, reconnect to it.
321
- if (!this.connect()) {
499
+ if (!(await this.connect())) {
322
500
  throw new QueryFailedError(
323
501
  'Query failed: ' + e?.errno + ' - ' + e?.message,
324
- query
502
+ query,
503
+ e?.errno,
325
504
  );
326
505
  }
327
506
  try {
@@ -329,60 +508,21 @@ export default class MySQLDriver extends NymphDriver {
329
508
  } catch (e2: any) {
330
509
  throw new QueryFailedError(
331
510
  'Query failed: ' + e2?.errno + ' - ' + e2?.message,
332
- query
333
- );
334
- }
335
- } else {
336
- throw e;
337
- }
338
- }
339
- }
340
-
341
- private querySync<T extends () => any>(
342
- runQuery: T,
343
- query: string,
344
- etypes: string[] = []
345
- ): ReturnType<T> {
346
- try {
347
- return runQuery();
348
- } catch (e: any) {
349
- const errorCode = e?.errno;
350
- if (errorCode === 1146 && this.createTables()) {
351
- // If the tables don't exist yet, create them.
352
- for (let etype of etypes) {
353
- this.createTables(etype);
354
- }
355
- try {
356
- return runQuery();
357
- } catch (e2: any) {
358
- throw new QueryFailedError(
359
- 'Query failed: ' + e2?.errno + ' - ' + e2?.message,
360
- query
361
- );
362
- }
363
- } else if (errorCode === 2006) {
364
- // If the MySQL server disconnected, reconnect to it.
365
- if (!this.connect()) {
366
- throw new QueryFailedError(
367
- 'Query failed: ' + e?.errno + ' - ' + e?.message,
368
- query
369
- );
370
- }
371
- try {
372
- return runQuery();
373
- } catch (e2: any) {
374
- throw new QueryFailedError(
375
- 'Query failed: ' + e2?.errno + ' - ' + e2?.message,
376
- query
511
+ query,
512
+ e2?.errno,
377
513
  );
378
514
  }
379
515
  } else {
380
- throw e;
516
+ throw new QueryFailedError(
517
+ 'Query failed: ' + e?.errno + ' - ' + e?.message,
518
+ query,
519
+ e?.errno,
520
+ );
381
521
  }
382
522
  }
383
523
  }
384
524
 
385
- private queryIter(
525
+ private queryArray(
386
526
  query: string,
387
527
  {
388
528
  etypes = [],
@@ -390,82 +530,42 @@ export default class MySQLDriver extends NymphDriver {
390
530
  }: {
391
531
  etypes?: string[];
392
532
  params?: { [k: string]: any };
393
- } = {}
533
+ } = {},
394
534
  ) {
395
535
  const { query: newQuery, params: newParams } = this.translateQuery(
396
536
  query,
397
- params
537
+ params,
398
538
  );
399
539
  return this.query(
400
540
  async () => {
401
- const results: any = await new Promise((resolve, reject) => {
402
- try {
403
- (this.transaction?.connection ?? this.link).query(
404
- newQuery,
405
- newParams,
406
- (error, results) => {
407
- if (error) {
408
- reject(error);
409
- }
541
+ const results: any = await new Promise(async (resolve, reject) => {
542
+ // Only do one query at a time in transations.
543
+ let myResolve: (value: void | PromiseLike<void>) => void = () => {};
544
+ const myPromise = new Promise<void>((resolve) => {
545
+ myResolve = resolve;
546
+ });
547
+ if (this.transaction) {
548
+ const latestPromise = this.transaction.latestPromise;
549
+ this.transaction.latestPromise = myPromise;
550
+ await latestPromise;
551
+ }
552
+ (this.transaction?.connection ?? this.link).query(
553
+ newQuery,
554
+ newParams,
555
+ (error, results) => {
556
+ if (error) {
557
+ reject(error);
558
+ } else {
410
559
  resolve(results);
411
560
  }
412
- );
413
- } catch (e) {
414
- reject(e);
415
- }
561
+ myResolve();
562
+ },
563
+ );
416
564
  });
417
565
  return results;
418
566
  },
419
567
  `${query} -- ${JSON.stringify(params)}`,
420
- etypes
421
- );
422
- }
423
-
424
- private queryIterSync(
425
- query: string,
426
- {
427
- etypes = [],
428
- params = {},
429
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
430
- ) {
431
- const { query: newQuery, params: newParams } = this.translateQuery(
432
- query,
433
- params
434
- );
435
- return this.querySync(
436
- () => {
437
- const output = cp.spawnSync(
438
- process.argv0,
439
- [__dirname + '/runMysqlSync.js'],
440
- {
441
- input: JSON.stringify({
442
- mysqlConfig: this.mysqlConfig,
443
- query: newQuery,
444
- params: newParams,
445
- }),
446
- timeout: 30000,
447
- maxBuffer: 100 * 1024 * 1024,
448
- encoding: 'utf8',
449
- windowsHide: true,
450
- }
451
- );
452
- try {
453
- const { results } = JSON.parse(output.stdout);
454
- return results;
455
- } catch (e) {
456
- // Do nothing.
457
- }
458
- if (output.status === 0) {
459
- throw new Error('Unknown parse error.');
460
- }
461
- const err = JSON.parse(output.stderr);
462
- const e = new Error(err.name);
463
- for (const name in err) {
464
- (e as any)[name] = err[name];
465
- }
466
- },
467
- `${query} -- ${JSON.stringify(params)}`,
468
- etypes
568
+ etypes,
469
569
  );
470
570
  }
471
571
 
@@ -477,34 +577,42 @@ export default class MySQLDriver extends NymphDriver {
477
577
  }: {
478
578
  etypes?: string[];
479
579
  params?: { [k: string]: any };
480
- } = {}
580
+ } = {},
481
581
  ) {
482
582
  const { query: newQuery, params: newParams } = this.translateQuery(
483
583
  query,
484
- params
584
+ params,
485
585
  );
486
586
  return this.query(
487
587
  async () => {
488
- const results: any = await new Promise((resolve, reject) => {
489
- try {
490
- (this.transaction?.connection ?? this.link).query(
491
- newQuery,
492
- newParams,
493
- (error, results) => {
494
- if (error) {
495
- reject(error);
496
- }
588
+ const results: any = await new Promise(async (resolve, reject) => {
589
+ // Only do one query at a time in transations.
590
+ let myResolve: (value: void | PromiseLike<void>) => void = () => {};
591
+ const myPromise = new Promise<void>((resolve) => {
592
+ myResolve = resolve;
593
+ });
594
+ if (this.transaction) {
595
+ const latestPromise = this.transaction.latestPromise;
596
+ this.transaction.latestPromise = myPromise;
597
+ await latestPromise;
598
+ }
599
+ (this.transaction?.connection ?? this.link).query(
600
+ newQuery,
601
+ newParams,
602
+ (error, results) => {
603
+ if (error) {
604
+ reject(error);
605
+ } else {
497
606
  resolve(results);
498
607
  }
499
- );
500
- } catch (e) {
501
- reject(e);
502
- }
608
+ myResolve();
609
+ },
610
+ );
503
611
  });
504
612
  return results[0];
505
613
  },
506
614
  `${query} -- ${JSON.stringify(params)}`,
507
- etypes
615
+ etypes,
508
616
  );
509
617
  }
510
618
 
@@ -513,92 +621,55 @@ export default class MySQLDriver extends NymphDriver {
513
621
  {
514
622
  etypes = [],
515
623
  params = {},
624
+ transaction,
516
625
  }: {
517
626
  etypes?: string[];
518
627
  params?: { [k: string]: any };
519
- } = {}
628
+ transaction?: MySQLDriverTransaction;
629
+ } = {},
520
630
  ) {
521
631
  const { query: newQuery, params: newParams } = this.translateQuery(
522
632
  query,
523
- params
633
+ params,
524
634
  );
525
635
  return this.query(
526
636
  async () => {
527
- const results: any = await new Promise((resolve, reject) => {
528
- try {
529
- (this.transaction?.connection ?? this.link).query(
530
- newQuery,
531
- newParams,
532
- (error, results) => {
533
- if (error) {
534
- reject(error);
535
- }
637
+ const results: any = await new Promise(async (resolve, reject) => {
638
+ // Only do one query at a time in transations.
639
+ let myResolve: (value: void | PromiseLike<void>) => void = () => {};
640
+ const myPromise = new Promise<void>((resolve) => {
641
+ myResolve = resolve;
642
+ });
643
+ const trans = transaction ?? this.transaction;
644
+ if (trans) {
645
+ const latestPromise = trans.latestPromise;
646
+ trans.latestPromise = myPromise;
647
+ await latestPromise;
648
+ }
649
+ (trans?.connection ?? this.link).query(
650
+ newQuery,
651
+ newParams,
652
+ (error, results) => {
653
+ if (error) {
654
+ reject(error);
655
+ } else {
536
656
  resolve(results);
537
657
  }
538
- );
539
- } catch (e) {
540
- reject(e);
541
- }
658
+ myResolve();
659
+ },
660
+ );
542
661
  });
543
662
  return { changes: results.changedRows ?? 0 };
544
663
  },
545
664
  `${query} -- ${JSON.stringify(params)}`,
546
- etypes
547
- );
548
- }
549
-
550
- private queryRunSync(
551
- query: string,
552
- {
553
- etypes = [],
554
- params = {},
555
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
556
- ) {
557
- const { query: newQuery, params: newParams } = this.translateQuery(
558
- query,
559
- params
560
- );
561
- return this.querySync(
562
- () => {
563
- const output = cp.spawnSync(
564
- process.argv0,
565
- [__dirname + '/runMysqlSync.js'],
566
- {
567
- input: JSON.stringify({
568
- mysqlConfig: this.mysqlConfig,
569
- query: newQuery,
570
- params: newParams,
571
- }),
572
- timeout: 30000,
573
- maxBuffer: 100 * 1024 * 1024,
574
- encoding: 'utf8',
575
- windowsHide: true,
576
- }
577
- );
578
- try {
579
- const { results } = JSON.parse(output.stdout);
580
- return { changes: results.changedRows ?? 0 };
581
- } catch (e) {
582
- // Do nothing.
583
- }
584
- if (output.status === 0) {
585
- throw new Error('Unknown parse error.');
586
- }
587
- const err = JSON.parse(output.stderr);
588
- const e = new Error(err.name);
589
- for (const name in err) {
590
- (e as any)[name] = err[name];
591
- }
592
- },
593
- `${query} -- ${JSON.stringify(params)}`,
594
- etypes
665
+ etypes,
595
666
  );
596
667
  }
597
668
 
598
669
  public async commit(name: string) {
599
670
  if (name == null || typeof name !== 'string' || name.length === 0) {
600
671
  throw new InvalidParametersError(
601
- 'Transaction commit attempted without a name.'
672
+ 'Transaction commit attempted without a name.',
602
673
  );
603
674
  }
604
675
  if (!this.transaction || this.transaction.count === 0) {
@@ -620,7 +691,7 @@ export default class MySQLDriver extends NymphDriver {
620
691
 
621
692
  public async deleteEntityByID(
622
693
  guid: string,
623
- className?: EntityConstructor | string | null
694
+ className?: EntityConstructor | string | null,
624
695
  ) {
625
696
  let EntityClass: EntityConstructor;
626
697
  if (typeof className === 'string' || className == null) {
@@ -634,58 +705,71 @@ export default class MySQLDriver extends NymphDriver {
634
705
  try {
635
706
  await this.queryRun(
636
707
  `DELETE FROM ${MySQLDriver.escape(
637
- `${this.prefix}entities_${etype}`
708
+ `${this.prefix}entities_${etype}`,
638
709
  )} WHERE \`guid\`=UNHEX(@guid);`,
639
710
  {
640
711
  etypes: [etype],
641
712
  params: {
642
713
  guid,
643
714
  },
644
- }
715
+ },
645
716
  );
646
717
  await this.queryRun(
647
718
  `DELETE FROM ${MySQLDriver.escape(
648
- `${this.prefix}data_${etype}`
719
+ `${this.prefix}data_${etype}`,
649
720
  )} WHERE \`guid\`=UNHEX(@guid);`,
650
721
  {
651
722
  etypes: [etype],
652
723
  params: {
653
724
  guid,
654
725
  },
655
- }
726
+ },
656
727
  );
657
728
  await this.queryRun(
658
729
  `DELETE FROM ${MySQLDriver.escape(
659
- `${this.prefix}comparisons_${etype}`
730
+ `${this.prefix}references_${etype}`,
660
731
  )} WHERE \`guid\`=UNHEX(@guid);`,
661
732
  {
662
733
  etypes: [etype],
663
734
  params: {
664
735
  guid,
665
736
  },
666
- }
737
+ },
667
738
  );
668
739
  await this.queryRun(
669
740
  `DELETE FROM ${MySQLDriver.escape(
670
- `${this.prefix}references_${etype}`
741
+ `${this.prefix}tokens_${etype}`,
671
742
  )} WHERE \`guid\`=UNHEX(@guid);`,
672
743
  {
673
744
  etypes: [etype],
674
745
  params: {
675
746
  guid,
676
747
  },
677
- }
748
+ },
749
+ );
750
+ await this.queryRun(
751
+ `DELETE FROM ${MySQLDriver.escape(
752
+ `${this.prefix}uniques_${etype}`,
753
+ )} WHERE \`guid\`=UNHEX(@guid);`,
754
+ {
755
+ etypes: [etype],
756
+ params: {
757
+ guid,
758
+ },
759
+ },
678
760
  );
679
- await this.commit('nymph-delete');
680
- // Remove any cached versions of this entity.
681
- if (this.nymph.config.cache) {
682
- this.cleanCache(guid);
683
- }
684
- return true;
685
761
  } catch (e: any) {
762
+ this.nymph.config.debugError('mysql', `Delete entity error: "${e}"`);
686
763
  await this.rollback('nymph-delete');
687
764
  throw e;
688
765
  }
766
+
767
+ await this.commit('nymph-delete');
768
+ // Remove any cached versions of this entity.
769
+ if (this.nymph.config.cache) {
770
+ this.cleanCache(guid);
771
+ }
772
+ return true;
689
773
  }
690
774
 
691
775
  public async deleteUID(name: string) {
@@ -694,101 +778,363 @@ export default class MySQLDriver extends NymphDriver {
694
778
  }
695
779
  await this.queryRun(
696
780
  `DELETE FROM ${MySQLDriver.escape(
697
- `${this.prefix}uids`
781
+ `${this.prefix}uids`,
698
782
  )} WHERE \`name\`=@name;`,
699
783
  {
700
784
  params: {
701
785
  name,
702
786
  },
703
- }
787
+ },
704
788
  );
705
789
  return true;
706
790
  }
707
791
 
708
- protected async exportEntities(writeLine: (line: string) => void) {
709
- writeLine('#nex2');
710
- writeLine('# Nymph Entity Exchange v2');
711
- writeLine('# http://nymph.io');
712
- writeLine('#');
713
- writeLine('# Generation Time: ' + new Date().toLocaleString());
714
- writeLine('');
792
+ public async getIndexes(etype: string) {
793
+ const indexes: {
794
+ scope: 'data' | 'references' | 'tokens';
795
+ name: string;
796
+ property: string;
797
+ }[] = [];
715
798
 
716
- writeLine('#');
717
- writeLine('# UIDs');
718
- writeLine('#');
719
- writeLine('');
799
+ for (let [scope, suffix] of [
800
+ ['data', '_string_guid'],
801
+ ['references', '_reference_guid'],
802
+ ['tokens', '_token_guid'],
803
+ ] as (
804
+ | ['data', '_string_guid']
805
+ | ['references', '_reference_guid']
806
+ | ['tokens', '_token_guid']
807
+ )[]) {
808
+ const indexDefinitions = await this.queryArray(
809
+ `SHOW INDEXES FROM ${MySQLDriver.escape(
810
+ `${this.prefix}${scope}_${etype}`,
811
+ )} WHERE \`Key_name\` LIKE @pattern AND \`Seq_in_index\`=1;`,
812
+ {
813
+ params: {
814
+ pattern: `${this.prefix}${scope}_${etype}_id_custom_%${suffix}`,
815
+ },
816
+ },
817
+ );
818
+ for (const indexDefinition of indexDefinitions) {
819
+ indexes.push({
820
+ scope,
821
+ name: (indexDefinition.Key_name as string).substring(
822
+ `${this.prefix}${scope}_${etype}_id_custom_`.length,
823
+ indexDefinition.Key_name.length - suffix.length,
824
+ ),
825
+ property:
826
+ ((indexDefinition.Expression as string).match(
827
+ /\s*\(?\s*`?name`?\s*=\s*(?:\w*)\\*'(.*?)\\*'/,
828
+ ) ?? [])[1] ?? '',
829
+ });
830
+ }
831
+ }
720
832
 
721
- // Export UIDs.
722
- let uids = await this.queryIter(
833
+ return indexes;
834
+ }
835
+
836
+ public async addIndex(
837
+ etype: string,
838
+ definition: {
839
+ scope: 'data' | 'references' | 'tokens';
840
+ name: string;
841
+ property: string;
842
+ },
843
+ ) {
844
+ this.checkIndexName(definition.name);
845
+ await this.deleteIndex(etype, definition.scope, definition.name);
846
+ if (definition.scope === 'data') {
847
+ await this.queryRun(
848
+ `CREATE INDEX ${MySQLDriver.escape(
849
+ `${this.prefix}data_${etype}_id_custom_${definition.name}_string_guid`,
850
+ )} USING btree ON ${MySQLDriver.escape(
851
+ `${this.prefix}data_${etype}`,
852
+ )} (((\`name\` = ${MySQLDriver.escapeValue(definition.property)})), \`string\`(500), \`guid\`);`,
853
+ );
854
+ await this.queryRun(
855
+ `CREATE INDEX ${MySQLDriver.escape(
856
+ `${this.prefix}data_${etype}_id_custom_${definition.name}_number_guid`,
857
+ )} USING btree ON ${MySQLDriver.escape(
858
+ `${this.prefix}data_${etype}`,
859
+ )} (((\`name\` = ${MySQLDriver.escapeValue(definition.property)})), \`number\`, \`guid\`);`,
860
+ );
861
+ await this.queryRun(
862
+ `CREATE INDEX ${MySQLDriver.escape(
863
+ `${this.prefix}data_${etype}_id_custom_${definition.name}_truthy_guid`,
864
+ )} USING btree ON ${MySQLDriver.escape(
865
+ `${this.prefix}data_${etype}`,
866
+ )} (((\`name\` = ${MySQLDriver.escapeValue(definition.property)})), \`truthy\`, \`guid\`);`,
867
+ );
868
+ await this.queryRun(
869
+ `CREATE INDEX ${MySQLDriver.escape(
870
+ `${this.prefix}data_${etype}_id_custom_${definition.name}_json_guid`,
871
+ )} USING btree ON ${MySQLDriver.escape(
872
+ `${this.prefix}data_${etype}`,
873
+ )} (((\`name\` = ${MySQLDriver.escapeValue(definition.property)})), (CAST(\`json\` AS CHAR(500)) COLLATE utf8mb4_bin), \`guid\`);`,
874
+ );
875
+ } else if (definition.scope === 'references') {
876
+ await this.queryRun(
877
+ `CREATE INDEX ${MySQLDriver.escape(
878
+ `${this.prefix}references_${etype}_id_custom_${definition.name}_reference_guid`,
879
+ )} USING btree ON ${MySQLDriver.escape(
880
+ `${this.prefix}references_${etype}`,
881
+ )} (((\`name\` = ${MySQLDriver.escapeValue(definition.property)})), \`reference\`, \`guid\`);`,
882
+ );
883
+ } else if (definition.scope === 'tokens') {
884
+ await this.queryRun(
885
+ `CREATE INDEX ${MySQLDriver.escape(
886
+ `${this.prefix}tokens_${etype}_id_custom_${definition.name}_token_guid`,
887
+ )} USING btree ON ${MySQLDriver.escape(
888
+ `${this.prefix}tokens_${etype}`,
889
+ )} (((\`name\` = ${MySQLDriver.escapeValue(definition.property)})), \`token\`, \`guid\`);`,
890
+ );
891
+ }
892
+ return true;
893
+ }
894
+
895
+ public async deleteIndex(
896
+ etype: string,
897
+ scope: 'data' | 'references' | 'tokens',
898
+ name: string,
899
+ ) {
900
+ this.checkIndexName(name);
901
+ try {
902
+ if (scope === 'data') {
903
+ await this.queryRun(
904
+ `DROP INDEX ${MySQLDriver.escape(
905
+ `${this.prefix}data_${etype}_id_custom_${name}_string_guid`,
906
+ )} ON ${MySQLDriver.escape(`${this.prefix}data_${etype}`)};`,
907
+ );
908
+ await this.queryRun(
909
+ `DROP INDEX ${MySQLDriver.escape(
910
+ `${this.prefix}data_${etype}_id_custom_${name}_number_guid`,
911
+ )} ON ${MySQLDriver.escape(`${this.prefix}data_${etype}`)};`,
912
+ );
913
+ await this.queryRun(
914
+ `DROP INDEX ${MySQLDriver.escape(
915
+ `${this.prefix}data_${etype}_id_custom_${name}_truthy_guid`,
916
+ )} ON ${MySQLDriver.escape(`${this.prefix}data_${etype}`)};`,
917
+ );
918
+ await this.queryRun(
919
+ `DROP INDEX ${MySQLDriver.escape(
920
+ `${this.prefix}data_${etype}_id_custom_${name}_json_guid`,
921
+ )} ON ${MySQLDriver.escape(`${this.prefix}data_${etype}`)};`,
922
+ );
923
+ } else if (scope === 'references') {
924
+ await this.queryRun(
925
+ `DROP INDEX ${MySQLDriver.escape(
926
+ `${this.prefix}references_${etype}_id_custom_${name}_reference_guid`,
927
+ )} ON ${MySQLDriver.escape(`${this.prefix}references_${etype}`)};`,
928
+ );
929
+ } else if (scope === 'tokens') {
930
+ await this.queryRun(
931
+ `DROP INDEX ${MySQLDriver.escape(
932
+ `${this.prefix}tokens_${etype}_id_custom_${name}_token_guid`,
933
+ )} ON ${MySQLDriver.escape(`${this.prefix}tokens_${etype}`)};`,
934
+ );
935
+ }
936
+ } catch (e: any) {
937
+ if (e?.errorCode !== 1091) {
938
+ throw e;
939
+ }
940
+ }
941
+ return true;
942
+ }
943
+
944
+ public async getEtypes() {
945
+ const tables = await this.queryArray(
946
+ 'SELECT `table_name` AS `table_name` FROM `information_schema`.`tables` WHERE `table_schema`=@db AND `table_name` LIKE @prefix;',
947
+ {
948
+ params: {
949
+ db: this.config.database,
950
+ prefix: this.prefix + 'entities_' + '%',
951
+ },
952
+ },
953
+ );
954
+ const etypes: string[] = [];
955
+ for (const table of tables) {
956
+ etypes.push(table.table_name.substr((this.prefix + 'entities_').length));
957
+ }
958
+
959
+ return etypes;
960
+ }
961
+
962
+ public async *exportDataIterator(): AsyncGenerator<
963
+ { type: 'comment' | 'uid' | 'entity'; content: string },
964
+ void,
965
+ false | undefined
966
+ > {
967
+ if (
968
+ yield {
969
+ type: 'comment',
970
+ content: `#nex2
971
+ # Nymph Entity Exchange v2
972
+ # http://nymph.io
973
+ #
974
+ # Generation Time: ${new Date().toLocaleString()}
975
+ `,
976
+ }
977
+ ) {
978
+ return;
979
+ }
980
+
981
+ if (
982
+ yield {
983
+ type: 'comment',
984
+ content: `
985
+
986
+ #
987
+ # UIDs
988
+ #
989
+
990
+ `,
991
+ }
992
+ ) {
993
+ return;
994
+ }
995
+
996
+ // Export UIDs.
997
+ let uids = await this.queryArray(
723
998
  `SELECT * FROM ${MySQLDriver.escape(
724
- `${this.prefix}uids`
725
- )} ORDER BY \`name\`;`
999
+ `${this.prefix}uids`,
1000
+ )} ORDER BY \`name\`;`,
726
1001
  );
727
1002
  for (const uid of uids) {
728
- writeLine(`<${uid.name}>[${uid.cur_uid}]`);
1003
+ if (yield { type: 'uid', content: `<${uid.name}>[${uid.cur_uid}]\n` }) {
1004
+ return;
1005
+ }
729
1006
  }
730
1007
 
731
- writeLine('');
732
- writeLine('#');
733
- writeLine('# Entities');
734
- writeLine('#');
735
- writeLine('');
1008
+ if (
1009
+ yield {
1010
+ type: 'comment',
1011
+ content: `
736
1012
 
737
- // Get the etypes.
738
- const tables = await this.queryIter('SHOW TABLES;');
739
- const etypes = [];
740
- for (const tableRow of tables) {
741
- const table = tableRow[Object.keys(tableRow)[0]];
742
- if (table.startsWith(this.prefix + 'entities_')) {
743
- etypes.push(table.substr((this.prefix + 'entities_').length));
1013
+ #
1014
+ # Entities
1015
+ #
1016
+
1017
+ `,
744
1018
  }
1019
+ ) {
1020
+ return;
745
1021
  }
746
1022
 
1023
+ // Get the etypes.
1024
+ const etypes = await this.getEtypes();
1025
+
747
1026
  for (const etype of etypes) {
748
1027
  // Export entities.
749
- const dataIterator = (
750
- await this.queryIter(
751
- `SELECT LOWER(HEX(e.\`guid\`)) AS \`guid\`, e.\`tags\`, e.\`cdate\`, e.\`mdate\`, d.\`name\` AS \`dname\`, d.\`value\` AS \`dvalue\`, c.\`string\`, c.\`number\`
1028
+ const entityIterator = this.exportEntityInternal(etype);
1029
+ for await (let content of entityIterator) {
1030
+ yield { type: 'entity', content };
1031
+ }
1032
+ }
1033
+ }
1034
+
1035
+ public async exportEntity(etype: string, guid: string): Promise<string> {
1036
+ const entityIterator = this.exportEntityInternal(etype, guid);
1037
+ let exportContent = '';
1038
+ for await (let content of entityIterator) {
1039
+ exportContent += content;
1040
+ }
1041
+
1042
+ return exportContent;
1043
+ }
1044
+
1045
+ private async *exportEntityInternal(
1046
+ etype: string,
1047
+ guid?: string,
1048
+ ): AsyncGenerator<string, void, false | undefined> {
1049
+ // Export entities.
1050
+ const dataIterator = (
1051
+ await this.queryArray(
1052
+ `SELECT LOWER(HEX(e.\`guid\`)) AS \`guid\`, e.\`tags\`, e.\`cdate\`, e.\`mdate\`, LOWER(HEX(e.\`user\`)) AS \`user\`, LOWER(HEX(e.\`group\`)) AS \`group\`, e.\`acUser\`, e.\`acGroup\`, e.\`acOther\`, e.\`acRead\`, e.\`acWrite\`, e.\`acFull\`, d.\`name\`, d.\`value\`, d.\`json\`, d.\`string\`, d.\`number\`
752
1053
  FROM ${MySQLDriver.escape(`${this.prefix}entities_${etype}`)} e
753
1054
  LEFT JOIN ${MySQLDriver.escape(
754
- `${this.prefix}data_${etype}`
755
- )} d ON e.\`guid\`=d.\`guid\`
756
- INNER JOIN ${MySQLDriver.escape(
757
- `${this.prefix}comparisons_${etype}`
758
- )} c ON d.\`guid\`=c.\`guid\` AND d.\`name\`=c.\`name\`
759
- ORDER BY e.\`guid\`;`
760
- )
761
- )[Symbol.iterator]();
762
- let datum = dataIterator.next();
763
- while (!datum.done) {
764
- const guid = datum.value.guid;
765
- const tags = datum.value.tags.slice(1, -1).split(' ').join(',');
766
- const cdate = datum.value.cdate;
767
- const mdate = datum.value.mdate;
768
- writeLine(`{${guid}}<${etype}>[${tags}]`);
769
- writeLine(`\tcdate=${JSON.stringify(cdate)}`);
770
- writeLine(`\tmdate=${JSON.stringify(mdate)}`);
771
- if (datum.value.dname != null) {
772
- // This do will keep going and adding the data until the
773
- // next entity is reached. $row will end on the next entity.
774
- do {
775
- const value =
776
- datum.value.dvalue === 'N'
777
- ? JSON.stringify(datum.value.number)
778
- : datum.value.dvalue === 'S'
1055
+ `${this.prefix}data_${etype}`,
1056
+ )} d ON e.\`guid\`=d.\`guid\`${guid == null ? '' : ` WHERE e.\`guid\`=UNHEX(@guid)`}
1057
+ ORDER BY e.\`guid\`, d.\`name\`;`,
1058
+ guid == null ? undefined : { params: { guid } },
1059
+ )
1060
+ )[Symbol.iterator]();
1061
+ let datum = dataIterator.next();
1062
+ while (!datum.done) {
1063
+ const guid = datum.value.guid;
1064
+ const tags = datum.value.tags
1065
+ .slice(1, -1)
1066
+ .split(' ')
1067
+ .filter((tag: string) => tag)
1068
+ .join(',');
1069
+ const cdate = datum.value.cdate;
1070
+ const mdate = datum.value.mdate;
1071
+ const user = datum.value.user;
1072
+ const group = datum.value.group;
1073
+ const acUser = datum.value.acUser;
1074
+ const acGroup = datum.value.acGroup;
1075
+ const acOther = datum.value.acOther;
1076
+ const acRead = datum.value.acRead?.slice(1, -1).split(' ');
1077
+ const acWrite = datum.value.acWrite?.slice(1, -1).split(' ');
1078
+ const acFull = datum.value.acFull?.slice(1, -1).split(' ');
1079
+ let currentEntityExport: string[] = [];
1080
+ currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
1081
+ currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
1082
+ currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
1083
+ if (this.nymph.tilmeld != null) {
1084
+ if (user != null) {
1085
+ currentEntityExport.push(
1086
+ `\tuser=${JSON.stringify(['nymph_entity_reference', user, 'User'])}`,
1087
+ );
1088
+ }
1089
+ if (group != null) {
1090
+ currentEntityExport.push(
1091
+ `\tgroup=${JSON.stringify(['nymph_entity_reference', group, 'Group'])}`,
1092
+ );
1093
+ }
1094
+ if (acUser != null) {
1095
+ currentEntityExport.push(`\tacUser=${JSON.stringify(acUser)}`);
1096
+ }
1097
+ if (acGroup != null) {
1098
+ currentEntityExport.push(`\tacGroup=${JSON.stringify(acGroup)}`);
1099
+ }
1100
+ if (acOther != null) {
1101
+ currentEntityExport.push(`\tacOther=${JSON.stringify(acOther)}`);
1102
+ }
1103
+ if (acRead != null) {
1104
+ currentEntityExport.push(`\tacRead=${JSON.stringify(acRead)}`);
1105
+ }
1106
+ if (acWrite != null) {
1107
+ currentEntityExport.push(`\tacWrite=${JSON.stringify(acWrite)}`);
1108
+ }
1109
+ if (acFull != null) {
1110
+ currentEntityExport.push(`\tacFull=${JSON.stringify(acFull)}`);
1111
+ }
1112
+ }
1113
+ if (datum.value.name != null) {
1114
+ // This do will keep going and adding the data until the
1115
+ // next entity is reached. $row will end on the next entity.
1116
+ do {
1117
+ const value =
1118
+ datum.value.value === 'N'
1119
+ ? JSON.stringify(datum.value.number)
1120
+ : datum.value.value === 'S'
779
1121
  ? JSON.stringify(datum.value.string)
780
- : datum.value.dvalue;
781
- writeLine(`\t${datum.value.dname}=${value}`);
782
- datum = dataIterator.next();
783
- } while (!datum.done && datum.value.guid === guid);
784
- } else {
785
- // Make sure that datum is incremented :)
1122
+ : datum.value.value === 'J'
1123
+ ? JSON.stringify(datum.value.json)
1124
+ : datum.value.value;
1125
+ currentEntityExport.push(`\t${datum.value.name}=${value}`);
786
1126
  datum = dataIterator.next();
787
- }
1127
+ } while (!datum.done && datum.value.guid === guid);
1128
+ } else {
1129
+ // Make sure that datum is incremented :)
1130
+ datum = dataIterator.next();
788
1131
  }
789
- }
1132
+ currentEntityExport.push('');
790
1133
 
791
- return;
1134
+ if (yield currentEntityExport.join('\n')) {
1135
+ return;
1136
+ }
1137
+ }
792
1138
  }
793
1139
 
794
1140
  /**
@@ -809,21 +1155,23 @@ export default class MySQLDriver extends NymphDriver {
809
1155
  params: { [k: string]: any } = {},
810
1156
  subquery = false,
811
1157
  tableSuffix = '',
812
- etypes: string[] = []
1158
+ etypes: string[] = [],
1159
+ guidSelector: string | undefined = undefined,
1160
+ guidExplicitSelector: ((guid: string) => string) | undefined = undefined,
813
1161
  ) {
814
1162
  if (typeof options.class?.alterOptions === 'function') {
815
1163
  options = options.class.alterOptions(options);
816
1164
  }
817
1165
  const eTable = `e${tableSuffix}`;
818
1166
  const dTable = `d${tableSuffix}`;
819
- const cTable = `c${tableSuffix}`;
820
1167
  const fTable = `f${tableSuffix}`;
821
1168
  const ieTable = `ie${tableSuffix}`;
822
1169
  const countTable = `count${tableSuffix}`;
823
- const sort = options.sort ?? 'cdate';
1170
+ const sTable = `s${tableSuffix}`;
1171
+ const sort = options.sort === undefined ? 'cdate' : options.sort;
824
1172
  const queryParts = this.iterateSelectorsForQuery(
825
1173
  formattedSelectors,
826
- (key, value, typeIsOr, typeIsNot) => {
1174
+ ({ key, value, typeIsOr, typeIsNot }) => {
827
1175
  const clauseNot = key.startsWith('!');
828
1176
  let curQuery = '';
829
1177
  for (const curValue of value) {
@@ -885,17 +1233,39 @@ export default class MySQLDriver extends NymphDriver {
885
1233
  if (curQuery) {
886
1234
  curQuery += typeIsOr ? ' OR ' : ' AND ';
887
1235
  }
888
- const name = `param${++count.i}`;
889
- curQuery +=
890
- ieTable +
891
- '.`guid` ' +
892
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
893
- 'IN (SELECT `guid` FROM ' +
894
- MySQLDriver.escape(this.prefix + 'data_' + etype) +
895
- ' WHERE `name`=@' +
896
- name +
897
- ')';
898
- params[name] = curVar;
1236
+ if (
1237
+ curVar === 'cdate' ||
1238
+ curVar === 'mdate' ||
1239
+ (this.nymph.tilmeld != null &&
1240
+ (curVar === 'user' ||
1241
+ curVar === 'group' ||
1242
+ curVar === 'acUser' ||
1243
+ curVar === 'acGroup' ||
1244
+ curVar === 'acOther' ||
1245
+ curVar === 'acRead' ||
1246
+ curVar === 'acWrite' ||
1247
+ curVar === 'acFull'))
1248
+ ) {
1249
+ curQuery +=
1250
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1251
+ '(' +
1252
+ ieTable +
1253
+ '.' +
1254
+ MySQLDriver.escape(curVar) +
1255
+ ' IS NOT NULL)';
1256
+ } else {
1257
+ const name = `param${++count.i}`;
1258
+ curQuery +=
1259
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1260
+ 'EXISTS (SELECT `guid` FROM ' +
1261
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1262
+ ' WHERE `guid`=' +
1263
+ ieTable +
1264
+ '.`guid` AND `name`=@' +
1265
+ name +
1266
+ ')';
1267
+ params[name] = curVar;
1268
+ }
899
1269
  }
900
1270
  break;
901
1271
  case 'truthy':
@@ -904,28 +1274,35 @@ export default class MySQLDriver extends NymphDriver {
904
1274
  if (curQuery) {
905
1275
  curQuery += typeIsOr ? ' OR ' : ' AND ';
906
1276
  }
907
- if (curVar === 'cdate') {
908
- curQuery +=
909
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
910
- '(' +
911
- ieTable +
912
- '.`cdate` NOT NULL)';
913
- break;
914
- } else if (curVar === 'mdate') {
1277
+ if (
1278
+ curVar === 'cdate' ||
1279
+ curVar === 'mdate' ||
1280
+ (this.nymph.tilmeld != null &&
1281
+ (curVar === 'user' ||
1282
+ curVar === 'group' ||
1283
+ curVar === 'acUser' ||
1284
+ curVar === 'acGroup' ||
1285
+ curVar === 'acOther' ||
1286
+ curVar === 'acRead' ||
1287
+ curVar === 'acWrite' ||
1288
+ curVar === 'acFull'))
1289
+ ) {
915
1290
  curQuery +=
916
1291
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
917
1292
  '(' +
918
1293
  ieTable +
919
- '.`mdate` NOT NULL)';
920
- break;
1294
+ '.' +
1295
+ MySQLDriver.escape(curVar) +
1296
+ ' IS NOT NULL)';
921
1297
  } else {
922
1298
  const name = `param${++count.i}`;
923
1299
  curQuery +=
924
1300
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1301
+ 'EXISTS (SELECT `guid` FROM ' +
1302
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1303
+ ' WHERE `guid`=' +
925
1304
  ieTable +
926
- '.`guid` IN (SELECT `guid` FROM ' +
927
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
928
- ' WHERE `name`=@' +
1305
+ '.`guid` AND `name`=@' +
929
1306
  name +
930
1307
  ' AND `truthy`=TRUE)';
931
1308
  params[name] = curVar;
@@ -934,34 +1311,65 @@ export default class MySQLDriver extends NymphDriver {
934
1311
  break;
935
1312
  case 'equal':
936
1313
  case '!equal':
937
- if (curValue[0] === 'cdate') {
1314
+ if (
1315
+ curValue[0] === 'cdate' ||
1316
+ curValue[0] === 'mdate' ||
1317
+ (this.nymph.tilmeld != null &&
1318
+ (curValue[0] === 'acUser' ||
1319
+ curValue[0] === 'acGroup' ||
1320
+ curValue[0] === 'acOther'))
1321
+ ) {
938
1322
  if (curQuery) {
939
1323
  curQuery += typeIsOr ? ' OR ' : ' AND ';
940
1324
  }
941
- const cdate = `param${++count.i}`;
1325
+ const value = `param${++count.i}`;
942
1326
  curQuery +=
943
1327
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
944
1328
  ieTable +
945
- '.`cdate`=@' +
946
- cdate;
947
- params[cdate] = isNaN(Number(curValue[1]))
1329
+ '.' +
1330
+ MySQLDriver.escape(curValue[0]) +
1331
+ '=@' +
1332
+ value;
1333
+ params[value] = isNaN(Number(curValue[1]))
948
1334
  ? null
949
1335
  : Number(curValue[1]);
950
- break;
951
- } else if (curValue[0] === 'mdate') {
1336
+ } else if (
1337
+ this.nymph.tilmeld != null &&
1338
+ (curValue[0] === 'user' || curValue[0] === 'group')
1339
+ ) {
952
1340
  if (curQuery) {
953
1341
  curQuery += typeIsOr ? ' OR ' : ' AND ';
954
1342
  }
955
- const mdate = `param${++count.i}`;
1343
+ const value = `param${++count.i}`;
956
1344
  curQuery +=
957
1345
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
958
1346
  ieTable +
959
- '.`mdate`=@' +
960
- mdate;
961
- params[mdate] = isNaN(Number(curValue[1]))
962
- ? null
963
- : Number(curValue[1]);
964
- break;
1347
+ '.' +
1348
+ MySQLDriver.escape(curValue[0]) +
1349
+ '=UNHEX(@' +
1350
+ value +
1351
+ ')';
1352
+ params[value] = `${curValue[1]}`;
1353
+ } else if (
1354
+ this.nymph.tilmeld != null &&
1355
+ (curValue[0] === 'acRead' ||
1356
+ curValue[0] === 'acWrite' ||
1357
+ curValue[0] === 'acFull')
1358
+ ) {
1359
+ if (curQuery) {
1360
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1361
+ }
1362
+ const value = `param${++count.i}`;
1363
+ curQuery +=
1364
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1365
+ ieTable +
1366
+ '.' +
1367
+ MySQLDriver.escape(curValue[0]) +
1368
+ '=@' +
1369
+ value;
1370
+ params[value] = Array.isArray(curValue[1])
1371
+ ? ' ' + curValue[1].join(' ') + ' '
1372
+ : '';
965
1373
  } else if (typeof curValue[1] === 'number') {
966
1374
  if (curQuery) {
967
1375
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -970,10 +1378,11 @@ export default class MySQLDriver extends NymphDriver {
970
1378
  const value = `param${++count.i}`;
971
1379
  curQuery +=
972
1380
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1381
+ 'EXISTS (SELECT `guid` FROM ' +
1382
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1383
+ ' WHERE `guid`=' +
973
1384
  ieTable +
974
- '.`guid` IN (SELECT `guid` FROM ' +
975
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
976
- ' WHERE `name`=@' +
1385
+ '.`guid` AND `name`=@' +
977
1386
  name +
978
1387
  ' AND `number`=@' +
979
1388
  value +
@@ -988,10 +1397,11 @@ export default class MySQLDriver extends NymphDriver {
988
1397
  const value = `param${++count.i}`;
989
1398
  curQuery +=
990
1399
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1400
+ 'EXISTS (SELECT `guid` FROM ' +
1401
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1402
+ ' WHERE `guid`=' +
991
1403
  ieTable +
992
- '.`guid` IN (SELECT `guid` FROM ' +
993
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
994
- ' WHERE `name`=@' +
1404
+ '.`guid` AND `name`=@' +
995
1405
  name +
996
1406
  ' AND `string`=@' +
997
1407
  value +
@@ -1013,136 +1423,313 @@ export default class MySQLDriver extends NymphDriver {
1013
1423
  }
1014
1424
  const name = `param${++count.i}`;
1015
1425
  const value = `param${++count.i}`;
1016
- curQuery +=
1017
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1018
- ieTable +
1019
- '.`guid` IN (SELECT `guid` FROM ' +
1020
- MySQLDriver.escape(this.prefix + 'data_' + etype) +
1021
- ' WHERE `name`=@' +
1022
- name +
1023
- ' AND `value`=@' +
1024
- value +
1025
- ')';
1426
+ if (svalue.length < 500) {
1427
+ curQuery +=
1428
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1429
+ 'EXISTS (SELECT `guid` FROM ' +
1430
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1431
+ ' WHERE `guid`=' +
1432
+ ieTable +
1433
+ '.`guid` AND `name`=@' +
1434
+ name +
1435
+ ' AND (CAST(`json` AS CHAR(500)) COLLATE utf8mb4_bin)=CAST(CAST(@' +
1436
+ value +
1437
+ ' AS JSON) AS CHAR(500)))';
1438
+ } else {
1439
+ curQuery +=
1440
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1441
+ 'EXISTS (SELECT `guid` FROM ' +
1442
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1443
+ ' WHERE `guid`=' +
1444
+ ieTable +
1445
+ '.`guid` AND `name`=@' +
1446
+ name +
1447
+ ' AND `json`=CAST(@' +
1448
+ value +
1449
+ ' AS JSON))';
1450
+ }
1026
1451
  params[name] = curValue[0];
1027
1452
  params[value] = svalue;
1028
1453
  }
1029
1454
  break;
1030
1455
  case 'contain':
1031
1456
  case '!contain':
1032
- if (curValue[0] === 'cdate') {
1457
+ if (
1458
+ curValue[0] === 'cdate' ||
1459
+ curValue[0] === 'mdate' ||
1460
+ (this.nymph.tilmeld != null &&
1461
+ (curValue[0] === 'acUser' ||
1462
+ curValue[0] === 'acGroup' ||
1463
+ curValue[0] === 'acOther'))
1464
+ ) {
1033
1465
  if (curQuery) {
1034
1466
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1035
1467
  }
1036
- const cdate = `param${++count.i}`;
1468
+ const value = `param${++count.i}`;
1037
1469
  curQuery +=
1038
1470
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1039
1471
  ieTable +
1040
- '.`cdate`=' +
1041
- cdate;
1042
- params[cdate] = isNaN(Number(curValue[1]))
1472
+ '.' +
1473
+ MySQLDriver.escape(curValue[0]) +
1474
+ '=@' +
1475
+ value;
1476
+ params[value] = isNaN(Number(curValue[1]))
1043
1477
  ? null
1044
1478
  : Number(curValue[1]);
1045
- break;
1046
- } else if (curValue[0] === 'mdate') {
1479
+ } else if (
1480
+ this.nymph.tilmeld != null &&
1481
+ (curValue[0] === 'user' || curValue[0] === 'group')
1482
+ ) {
1047
1483
  if (curQuery) {
1048
1484
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1049
1485
  }
1050
- const mdate = `param${++count.i}`;
1486
+ const value = `param${++count.i}`;
1051
1487
  curQuery +=
1052
1488
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1053
1489
  ieTable +
1054
- '.`mdate`=' +
1055
- mdate;
1056
- params[mdate] = isNaN(Number(curValue[1]))
1057
- ? null
1058
- : Number(curValue[1]);
1059
- break;
1490
+ '.' +
1491
+ MySQLDriver.escape(curValue[0]) +
1492
+ '=UNHEX(@' +
1493
+ value +
1494
+ ')';
1495
+ params[value] = `${curValue[1]}`;
1496
+ } else if (
1497
+ this.nymph.tilmeld != null &&
1498
+ (curValue[0] === 'acRead' ||
1499
+ curValue[0] === 'acWrite' ||
1500
+ curValue[0] === 'acFull')
1501
+ ) {
1502
+ if (curQuery) {
1503
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1504
+ }
1505
+ const queryParam = `param${++count.i}`;
1506
+ curQuery +=
1507
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1508
+ 'MATCH (' +
1509
+ ieTable +
1510
+ '.' +
1511
+ MySQLDriver.escape(curValue[0]) +
1512
+ ') AGAINST (@' +
1513
+ queryParam +
1514
+ ' IN BOOLEAN MODE)';
1515
+ params[queryParam] = '+' + curValue[1];
1060
1516
  } else {
1061
1517
  if (curQuery) {
1062
1518
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1063
1519
  }
1064
1520
  let svalue: string;
1065
- let stringValue: string;
1066
1521
  if (
1067
1522
  curValue[1] instanceof Object &&
1068
1523
  typeof curValue[1].toReference === 'function'
1069
1524
  ) {
1070
1525
  svalue = JSON.stringify(curValue[1].toReference());
1071
- stringValue = `${curValue[1].toReference()}`;
1072
1526
  } else {
1073
1527
  svalue = JSON.stringify(curValue[1]);
1074
- stringValue = `${curValue[1]}`;
1075
1528
  }
1076
1529
  const name = `param${++count.i}`;
1077
1530
  const value = `param${++count.i}`;
1078
- if (typeof curValue[1] === 'string') {
1079
- const stringParam = `param${++count.i}`;
1531
+ curQuery +=
1532
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1533
+ 'EXISTS (SELECT `guid` FROM ' +
1534
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1535
+ ' WHERE `guid`=' +
1536
+ ieTable +
1537
+ '.`guid` AND `name`=@' +
1538
+ name +
1539
+ ' AND JSON_CONTAINS(`json`, @' +
1540
+ value +
1541
+ '))';
1542
+ params[name] = curValue[0];
1543
+ params[value] = svalue;
1544
+ }
1545
+ break;
1546
+ case 'search':
1547
+ case '!search':
1548
+ if (
1549
+ curValue[0] === 'cdate' ||
1550
+ curValue[0] === 'mdate' ||
1551
+ (this.nymph.tilmeld != null &&
1552
+ (curValue[0] === 'user' ||
1553
+ curValue[0] === 'group' ||
1554
+ curValue[0] === 'acUser' ||
1555
+ curValue[0] === 'acGroup' ||
1556
+ curValue[0] === 'acOther' ||
1557
+ curValue[0] === 'acRead' ||
1558
+ curValue[0] === 'acWrite' ||
1559
+ curValue[0] === 'acFull'))
1560
+ ) {
1561
+ if (curQuery) {
1562
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1563
+ }
1564
+ curQuery +=
1565
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(FALSE)';
1566
+ } else {
1567
+ if (curQuery) {
1568
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1569
+ }
1570
+
1571
+ const parsedFTSQuery = this.tokenizer.parseSearchQuery(
1572
+ curValue[1],
1573
+ );
1574
+
1575
+ if (!parsedFTSQuery.length) {
1080
1576
  curQuery +=
1081
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1082
- '(' +
1083
- ieTable +
1084
- '.`guid` IN (SELECT `guid` FROM ' +
1085
- MySQLDriver.escape(this.prefix + 'data_' + etype) +
1086
- ' WHERE `name`=@' +
1087
- name +
1088
- ' AND INSTR(`value`, @' +
1089
- value +
1090
- ')) OR ' +
1091
- ieTable +
1092
- '.`guid` IN (SELECT `guid` FROM ' +
1093
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1094
- ' WHERE `name`=@' +
1095
- name +
1096
- ' AND `string`=@' +
1097
- stringParam +
1098
- '))';
1099
- params[stringParam] = stringValue;
1577
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(FALSE)';
1100
1578
  } else {
1579
+ const name = `param${++count.i}`;
1580
+
1581
+ const queryPartToken = (term: SearchTerm) => {
1582
+ const value = `param${++count.i}`;
1583
+ params[value] = term.token;
1584
+ return (
1585
+ 'EXISTS (SELECT `guid` FROM ' +
1586
+ MySQLDriver.escape(this.prefix + 'tokens_' + etype) +
1587
+ ' WHERE `guid`=' +
1588
+ ieTable +
1589
+ '.`guid` AND `name`=@' +
1590
+ name +
1591
+ ' AND `token`=@' +
1592
+ value +
1593
+ (term.nostemmed ? ' AND `stem`=FALSE' : '') +
1594
+ ')'
1595
+ );
1596
+ };
1597
+
1598
+ const queryPartSeries = (series: SearchSeriesTerm) => {
1599
+ const tokenTableSuffix = makeTableSuffix();
1600
+ const tokenParts = series.tokens.map((token, i) => {
1601
+ const value = `param${++count.i}`;
1602
+ params[value] = token.token;
1603
+ return {
1604
+ fromClause:
1605
+ i === 0
1606
+ ? 'FROM ' +
1607
+ MySQLDriver.escape(
1608
+ this.prefix + 'tokens_' + etype,
1609
+ ) +
1610
+ ' t' +
1611
+ tokenTableSuffix +
1612
+ '0'
1613
+ : 'JOIN ' +
1614
+ MySQLDriver.escape(
1615
+ this.prefix + 'tokens_' + etype,
1616
+ ) +
1617
+ ' t' +
1618
+ tokenTableSuffix +
1619
+ i +
1620
+ ' ON t' +
1621
+ tokenTableSuffix +
1622
+ i +
1623
+ '.`guid` = t' +
1624
+ tokenTableSuffix +
1625
+ '0.`guid` AND t' +
1626
+ tokenTableSuffix +
1627
+ i +
1628
+ '.`name` = t' +
1629
+ tokenTableSuffix +
1630
+ '0.`name` AND t' +
1631
+ tokenTableSuffix +
1632
+ i +
1633
+ '.`position` = t' +
1634
+ tokenTableSuffix +
1635
+ '0.`position` + ' +
1636
+ i,
1637
+ whereClause:
1638
+ 't' +
1639
+ tokenTableSuffix +
1640
+ i +
1641
+ '.`token`=@' +
1642
+ value +
1643
+ (token.nostemmed
1644
+ ? ' AND t' + tokenTableSuffix + i + '.`stem`=FALSE'
1645
+ : ''),
1646
+ };
1647
+ });
1648
+ return (
1649
+ 'EXISTS (SELECT t' +
1650
+ tokenTableSuffix +
1651
+ '0.`guid` ' +
1652
+ tokenParts.map((part) => part.fromClause).join(' ') +
1653
+ ' WHERE t' +
1654
+ tokenTableSuffix +
1655
+ '0.`guid`=' +
1656
+ ieTable +
1657
+ '.`guid` AND t' +
1658
+ tokenTableSuffix +
1659
+ '0.`name`=@' +
1660
+ name +
1661
+ ' AND ' +
1662
+ tokenParts.map((part) => part.whereClause).join(' AND ') +
1663
+ ')'
1664
+ );
1665
+ };
1666
+
1667
+ const queryPartTerm = (
1668
+ term:
1669
+ | SearchTerm
1670
+ | SearchOrTerm
1671
+ | SearchNotTerm
1672
+ | SearchSeriesTerm,
1673
+ ): string => {
1674
+ if (term.type === 'series') {
1675
+ return queryPartSeries(term);
1676
+ } else if (term.type === 'not') {
1677
+ return 'NOT ' + queryPartTerm(term.operand);
1678
+ } else if (term.type === 'or') {
1679
+ let queryParts: string[] = [];
1680
+ for (let operand of term.operands) {
1681
+ queryParts.push(queryPartTerm(operand));
1682
+ }
1683
+ return '(' + queryParts.join(' OR ') + ')';
1684
+ }
1685
+ return queryPartToken(term);
1686
+ };
1687
+
1688
+ // Run through the query and add terms.
1689
+ let termStrings: string[] = [];
1690
+ for (let term of parsedFTSQuery) {
1691
+ termStrings.push(queryPartTerm(term));
1692
+ }
1693
+
1101
1694
  curQuery +=
1102
1695
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1103
- ieTable +
1104
- '.`guid` IN (SELECT `guid` FROM ' +
1105
- MySQLDriver.escape(this.prefix + 'data_' + etype) +
1106
- ' WHERE `name`=@' +
1107
- name +
1108
- ' AND INSTR(`value`, @' +
1109
- value +
1110
- '))';
1696
+ '(' +
1697
+ termStrings.join(' AND ') +
1698
+ ')';
1699
+
1700
+ params[name] = curValue[0];
1111
1701
  }
1112
- params[name] = curValue[0];
1113
- params[value] = svalue;
1114
1702
  }
1115
1703
  break;
1116
1704
  case 'match':
1117
1705
  case '!match':
1118
- if (curValue[0] === 'cdate') {
1119
- if (curQuery) {
1120
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1121
- }
1122
- const cdate = `param${++count.i}`;
1123
- curQuery +=
1124
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1125
- '(' +
1126
- ieTable +
1127
- '.`cdate` REGEXP @' +
1128
- cdate +
1129
- ')';
1130
- params[cdate] = curValue[1];
1131
- break;
1132
- } else if (curValue[0] === 'mdate') {
1706
+ if (
1707
+ curValue[0] === 'cdate' ||
1708
+ curValue[0] === 'mdate' ||
1709
+ (this.nymph.tilmeld != null &&
1710
+ (curValue[0] === 'user' ||
1711
+ curValue[0] === 'group' ||
1712
+ curValue[0] === 'acUser' ||
1713
+ curValue[0] === 'acGroup' ||
1714
+ curValue[0] === 'acOther' ||
1715
+ curValue[0] === 'acRead' ||
1716
+ curValue[0] === 'acWrite' ||
1717
+ curValue[0] === 'acFull'))
1718
+ ) {
1133
1719
  if (curQuery) {
1134
1720
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1135
1721
  }
1136
- const mdate = `param${++count.i}`;
1722
+ const value = `param${++count.i}`;
1137
1723
  curQuery +=
1138
1724
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1139
1725
  '(' +
1140
1726
  ieTable +
1141
- '.`mdate` REGEXP @' +
1142
- mdate +
1727
+ '.' +
1728
+ MySQLDriver.escape(curValue[0]) +
1729
+ ' REGEXP @' +
1730
+ value +
1143
1731
  ')';
1144
- params[mdate] = curValue[1];
1145
- break;
1732
+ params[value] = curValue[1];
1146
1733
  } else {
1147
1734
  if (curQuery) {
1148
1735
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1151,10 +1738,11 @@ export default class MySQLDriver extends NymphDriver {
1151
1738
  const value = `param${++count.i}`;
1152
1739
  curQuery +=
1153
1740
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1741
+ 'EXISTS (SELECT `guid` FROM ' +
1742
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1743
+ ' WHERE `guid`=' +
1154
1744
  ieTable +
1155
- '.`guid` IN (SELECT `guid` FROM ' +
1156
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1157
- ' WHERE `name`=@' +
1745
+ '.`guid` AND `name`=@' +
1158
1746
  name +
1159
1747
  ' AND `string` REGEXP @' +
1160
1748
  value +
@@ -1165,34 +1753,33 @@ export default class MySQLDriver extends NymphDriver {
1165
1753
  break;
1166
1754
  case 'imatch':
1167
1755
  case '!imatch':
1168
- if (curValue[0] === 'cdate') {
1169
- if (curQuery) {
1170
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1171
- }
1172
- const cdate = `param${++count.i}`;
1173
- curQuery +=
1174
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1175
- '(' +
1176
- ieTable +
1177
- '.`cdate` REGEXP @' +
1178
- cdate +
1179
- ')';
1180
- params[cdate] = curValue[1];
1181
- break;
1182
- } else if (curValue[0] === 'mdate') {
1756
+ if (
1757
+ curValue[0] === 'cdate' ||
1758
+ curValue[0] === 'mdate' ||
1759
+ (this.nymph.tilmeld != null &&
1760
+ (curValue[0] === 'user' ||
1761
+ curValue[0] === 'group' ||
1762
+ curValue[0] === 'acUser' ||
1763
+ curValue[0] === 'acGroup' ||
1764
+ curValue[0] === 'acOther' ||
1765
+ curValue[0] === 'acRead' ||
1766
+ curValue[0] === 'acWrite' ||
1767
+ curValue[0] === 'acFull'))
1768
+ ) {
1183
1769
  if (curQuery) {
1184
1770
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1185
1771
  }
1186
- const mdate = `param${++count.i}`;
1772
+ const value = `param${++count.i}`;
1187
1773
  curQuery +=
1188
1774
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1189
1775
  '(' +
1190
1776
  ieTable +
1191
- '.`mdate` REGEXP @' +
1192
- mdate +
1777
+ '.' +
1778
+ MySQLDriver.escape(curValue[0]) +
1779
+ ' REGEXP @' +
1780
+ value +
1193
1781
  ')';
1194
- params[mdate] = curValue[1];
1195
- break;
1782
+ params[value] = curValue[1];
1196
1783
  } else {
1197
1784
  if (curQuery) {
1198
1785
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1201,10 +1788,11 @@ export default class MySQLDriver extends NymphDriver {
1201
1788
  const value = `param${++count.i}`;
1202
1789
  curQuery +=
1203
1790
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1791
+ 'EXISTS (SELECT `guid` FROM ' +
1792
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1793
+ ' WHERE `guid`=' +
1204
1794
  ieTable +
1205
- '.`guid` IN (SELECT `guid` FROM ' +
1206
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1207
- ' WHERE `name`=@' +
1795
+ '.`guid` AND `name`=@' +
1208
1796
  name +
1209
1797
  ' AND LOWER(`string`) REGEXP LOWER(@' +
1210
1798
  value +
@@ -1215,34 +1803,33 @@ export default class MySQLDriver extends NymphDriver {
1215
1803
  break;
1216
1804
  case 'like':
1217
1805
  case '!like':
1218
- if (curValue[0] === 'cdate') {
1219
- if (curQuery) {
1220
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1221
- }
1222
- const cdate = `param${++count.i}`;
1223
- curQuery +=
1224
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1225
- '(' +
1226
- ieTable +
1227
- '.`cdate` LIKE @' +
1228
- cdate +
1229
- ')';
1230
- params[cdate] = curValue[1];
1231
- break;
1232
- } else if (curValue[0] === 'mdate') {
1806
+ if (
1807
+ curValue[0] === 'cdate' ||
1808
+ curValue[0] === 'mdate' ||
1809
+ (this.nymph.tilmeld != null &&
1810
+ (curValue[0] === 'user' ||
1811
+ curValue[0] === 'group' ||
1812
+ curValue[0] === 'acUser' ||
1813
+ curValue[0] === 'acGroup' ||
1814
+ curValue[0] === 'acOther' ||
1815
+ curValue[0] === 'acRead' ||
1816
+ curValue[0] === 'acWrite' ||
1817
+ curValue[0] === 'acFull'))
1818
+ ) {
1233
1819
  if (curQuery) {
1234
1820
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1235
1821
  }
1236
- const mdate = `param${++count.i}`;
1822
+ const value = `param${++count.i}`;
1237
1823
  curQuery +=
1238
1824
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1239
1825
  '(' +
1240
1826
  ieTable +
1241
- '.`mdate` LIKE @' +
1242
- mdate +
1827
+ '.' +
1828
+ MySQLDriver.escape(curValue[0]) +
1829
+ ' LIKE @' +
1830
+ value +
1243
1831
  ')';
1244
- params[mdate] = curValue[1];
1245
- break;
1832
+ params[value] = curValue[1];
1246
1833
  } else {
1247
1834
  if (curQuery) {
1248
1835
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1251,10 +1838,11 @@ export default class MySQLDriver extends NymphDriver {
1251
1838
  const value = `param${++count.i}`;
1252
1839
  curQuery +=
1253
1840
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1841
+ 'EXISTS (SELECT `guid` FROM ' +
1842
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1843
+ ' WHERE `guid`=' +
1254
1844
  ieTable +
1255
- '.`guid` IN (SELECT `guid` FROM ' +
1256
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1257
- ' WHERE `name`=@' +
1845
+ '.`guid` AND `name`=@' +
1258
1846
  name +
1259
1847
  ' AND `string` LIKE @' +
1260
1848
  value +
@@ -1265,34 +1853,33 @@ export default class MySQLDriver extends NymphDriver {
1265
1853
  break;
1266
1854
  case 'ilike':
1267
1855
  case '!ilike':
1268
- if (curValue[0] === 'cdate') {
1856
+ if (
1857
+ curValue[0] === 'cdate' ||
1858
+ curValue[0] === 'mdate' ||
1859
+ (this.nymph.tilmeld != null &&
1860
+ (curValue[0] === 'user' ||
1861
+ curValue[0] === 'group' ||
1862
+ curValue[0] === 'acUser' ||
1863
+ curValue[0] === 'acGroup' ||
1864
+ curValue[0] === 'acOther' ||
1865
+ curValue[0] === 'acRead' ||
1866
+ curValue[0] === 'acWrite' ||
1867
+ curValue[0] === 'acFull'))
1868
+ ) {
1269
1869
  if (curQuery) {
1270
1870
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1271
1871
  }
1272
- const cdate = `param${++count.i}`;
1872
+ const value = `param${++count.i}`;
1273
1873
  curQuery +=
1274
1874
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1275
1875
  '(' +
1276
1876
  ieTable +
1277
- '.`cdate` LIKE @' +
1278
- cdate +
1877
+ '.' +
1878
+ MySQLDriver.escape(curValue[0]) +
1879
+ ' LIKE @' +
1880
+ value +
1279
1881
  ')';
1280
- params[cdate] = curValue[1];
1281
- break;
1282
- } else if (curValue[0] === 'mdate') {
1283
- if (curQuery) {
1284
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1285
- }
1286
- const mdate = `param${++count.i}`;
1287
- curQuery +=
1288
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1289
- '(' +
1290
- ieTable +
1291
- '.`mdate` LIKE @' +
1292
- mdate +
1293
- ')';
1294
- params[mdate] = curValue[1];
1295
- break;
1882
+ params[value] = curValue[1];
1296
1883
  } else {
1297
1884
  if (curQuery) {
1298
1885
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1301,10 +1888,11 @@ export default class MySQLDriver extends NymphDriver {
1301
1888
  const value = `param${++count.i}`;
1302
1889
  curQuery +=
1303
1890
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1891
+ 'EXISTS (SELECT `guid` FROM ' +
1892
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1893
+ ' WHERE `guid`=' +
1304
1894
  ieTable +
1305
- '.`guid` IN (SELECT `guid` FROM ' +
1306
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1307
- ' WHERE `name`=@' +
1895
+ '.`guid` AND `name`=@' +
1308
1896
  name +
1309
1897
  ' AND LOWER(`string`) LIKE LOWER(@' +
1310
1898
  value +
@@ -1315,34 +1903,33 @@ export default class MySQLDriver extends NymphDriver {
1315
1903
  break;
1316
1904
  case 'gt':
1317
1905
  case '!gt':
1318
- if (curValue[0] === 'cdate') {
1319
- if (curQuery) {
1320
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1321
- }
1322
- const cdate = `param${++count.i}`;
1323
- curQuery +=
1324
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1325
- ieTable +
1326
- '.`cdate`>@' +
1327
- cdate;
1328
- params[cdate] = isNaN(Number(curValue[1]))
1329
- ? null
1330
- : Number(curValue[1]);
1331
- break;
1332
- } else if (curValue[0] === 'mdate') {
1906
+ if (
1907
+ curValue[0] === 'cdate' ||
1908
+ curValue[0] === 'mdate' ||
1909
+ (this.nymph.tilmeld != null &&
1910
+ (curValue[0] === 'user' ||
1911
+ curValue[0] === 'group' ||
1912
+ curValue[0] === 'acUser' ||
1913
+ curValue[0] === 'acGroup' ||
1914
+ curValue[0] === 'acOther' ||
1915
+ curValue[0] === 'acRead' ||
1916
+ curValue[0] === 'acWrite' ||
1917
+ curValue[0] === 'acFull'))
1918
+ ) {
1333
1919
  if (curQuery) {
1334
1920
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1335
1921
  }
1336
- const mdate = `param${++count.i}`;
1922
+ const value = `param${++count.i}`;
1337
1923
  curQuery +=
1338
1924
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1339
1925
  ieTable +
1340
- '.`mdate`>@' +
1341
- mdate;
1342
- params[mdate] = isNaN(Number(curValue[1]))
1926
+ '.' +
1927
+ MySQLDriver.escape(curValue[0]) +
1928
+ '>@' +
1929
+ value;
1930
+ params[value] = isNaN(Number(curValue[1]))
1343
1931
  ? null
1344
1932
  : Number(curValue[1]);
1345
- break;
1346
1933
  } else {
1347
1934
  if (curQuery) {
1348
1935
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1351,10 +1938,11 @@ export default class MySQLDriver extends NymphDriver {
1351
1938
  const value = `param${++count.i}`;
1352
1939
  curQuery +=
1353
1940
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1941
+ 'EXISTS (SELECT `guid` FROM ' +
1942
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1943
+ ' WHERE `guid`=' +
1354
1944
  ieTable +
1355
- '.`guid` IN (SELECT `guid` FROM ' +
1356
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1357
- ' WHERE `name`=@' +
1945
+ '.`guid` AND `name`=@' +
1358
1946
  name +
1359
1947
  ' AND `number`>@' +
1360
1948
  value +
@@ -1367,34 +1955,33 @@ export default class MySQLDriver extends NymphDriver {
1367
1955
  break;
1368
1956
  case 'gte':
1369
1957
  case '!gte':
1370
- if (curValue[0] === 'cdate') {
1371
- if (curQuery) {
1372
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1373
- }
1374
- const cdate = `param${++count.i}`;
1375
- curQuery +=
1376
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1377
- ieTable +
1378
- '.`cdate`>=@' +
1379
- cdate;
1380
- params[cdate] = isNaN(Number(curValue[1]))
1381
- ? null
1382
- : Number(curValue[1]);
1383
- break;
1384
- } else if (curValue[0] === 'mdate') {
1958
+ if (
1959
+ curValue[0] === 'cdate' ||
1960
+ curValue[0] === 'mdate' ||
1961
+ (this.nymph.tilmeld != null &&
1962
+ (curValue[0] === 'user' ||
1963
+ curValue[0] === 'group' ||
1964
+ curValue[0] === 'acUser' ||
1965
+ curValue[0] === 'acGroup' ||
1966
+ curValue[0] === 'acOther' ||
1967
+ curValue[0] === 'acRead' ||
1968
+ curValue[0] === 'acWrite' ||
1969
+ curValue[0] === 'acFull'))
1970
+ ) {
1385
1971
  if (curQuery) {
1386
1972
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1387
1973
  }
1388
- const mdate = `param${++count.i}`;
1974
+ const value = `param${++count.i}`;
1389
1975
  curQuery +=
1390
1976
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1391
1977
  ieTable +
1392
- '.`mdate`>=@' +
1393
- mdate;
1394
- params[mdate] = isNaN(Number(curValue[1]))
1978
+ '.' +
1979
+ MySQLDriver.escape(curValue[0]) +
1980
+ '>=@' +
1981
+ value;
1982
+ params[value] = isNaN(Number(curValue[1]))
1395
1983
  ? null
1396
1984
  : Number(curValue[1]);
1397
- break;
1398
1985
  } else {
1399
1986
  if (curQuery) {
1400
1987
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1403,10 +1990,11 @@ export default class MySQLDriver extends NymphDriver {
1403
1990
  const value = `param${++count.i}`;
1404
1991
  curQuery +=
1405
1992
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1993
+ 'EXISTS (SELECT `guid` FROM ' +
1994
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
1995
+ ' WHERE `guid`=' +
1406
1996
  ieTable +
1407
- '.`guid` IN (SELECT `guid` FROM ' +
1408
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1409
- ' WHERE `name`=@' +
1997
+ '.`guid` AND `name`=@' +
1410
1998
  name +
1411
1999
  ' AND `number`>=@' +
1412
2000
  value +
@@ -1419,34 +2007,33 @@ export default class MySQLDriver extends NymphDriver {
1419
2007
  break;
1420
2008
  case 'lt':
1421
2009
  case '!lt':
1422
- if (curValue[0] === 'cdate') {
2010
+ if (
2011
+ curValue[0] === 'cdate' ||
2012
+ curValue[0] === 'mdate' ||
2013
+ (this.nymph.tilmeld != null &&
2014
+ (curValue[0] === 'user' ||
2015
+ curValue[0] === 'group' ||
2016
+ curValue[0] === 'acUser' ||
2017
+ curValue[0] === 'acGroup' ||
2018
+ curValue[0] === 'acOther' ||
2019
+ curValue[0] === 'acRead' ||
2020
+ curValue[0] === 'acWrite' ||
2021
+ curValue[0] === 'acFull'))
2022
+ ) {
1423
2023
  if (curQuery) {
1424
2024
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1425
2025
  }
1426
- const cdate = `param${++count.i}`;
1427
- curQuery +=
1428
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1429
- ieTable +
1430
- '.`cdate`<@' +
1431
- cdate;
1432
- params[cdate] = isNaN(Number(curValue[1]))
1433
- ? null
1434
- : Number(curValue[1]);
1435
- break;
1436
- } else if (curValue[0] === 'mdate') {
1437
- if (curQuery) {
1438
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1439
- }
1440
- const mdate = `param${++count.i}`;
2026
+ const value = `param${++count.i}`;
1441
2027
  curQuery +=
1442
2028
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1443
2029
  ieTable +
1444
- '.`mdate`<@' +
1445
- mdate;
1446
- params[mdate] = isNaN(Number(curValue[1]))
2030
+ '.' +
2031
+ MySQLDriver.escape(curValue[0]) +
2032
+ '<@' +
2033
+ value;
2034
+ params[value] = isNaN(Number(curValue[1]))
1447
2035
  ? null
1448
2036
  : Number(curValue[1]);
1449
- break;
1450
2037
  } else {
1451
2038
  if (curQuery) {
1452
2039
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1455,10 +2042,11 @@ export default class MySQLDriver extends NymphDriver {
1455
2042
  const value = `param${++count.i}`;
1456
2043
  curQuery +=
1457
2044
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2045
+ 'EXISTS (SELECT `guid` FROM ' +
2046
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
2047
+ ' WHERE `guid`=' +
1458
2048
  ieTable +
1459
- '.`guid` IN (SELECT `guid` FROM ' +
1460
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1461
- ' WHERE `name`=@' +
2049
+ '.`guid` AND `name`=@' +
1462
2050
  name +
1463
2051
  ' AND `number`<@' +
1464
2052
  value +
@@ -1471,34 +2059,33 @@ export default class MySQLDriver extends NymphDriver {
1471
2059
  break;
1472
2060
  case 'lte':
1473
2061
  case '!lte':
1474
- if (curValue[0] === 'cdate') {
1475
- if (curQuery) {
1476
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1477
- }
1478
- const cdate = `param${++count.i}`;
1479
- curQuery +=
1480
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1481
- ieTable +
1482
- '.`cdate`<=@' +
1483
- cdate;
1484
- params[cdate] = isNaN(Number(curValue[1]))
1485
- ? null
1486
- : Number(curValue[1]);
1487
- break;
1488
- } else if (curValue[0] === 'mdate') {
2062
+ if (
2063
+ curValue[0] === 'cdate' ||
2064
+ curValue[0] === 'mdate' ||
2065
+ (this.nymph.tilmeld != null &&
2066
+ (curValue[0] === 'user' ||
2067
+ curValue[0] === 'group' ||
2068
+ curValue[0] === 'acUser' ||
2069
+ curValue[0] === 'acGroup' ||
2070
+ curValue[0] === 'acOther' ||
2071
+ curValue[0] === 'acRead' ||
2072
+ curValue[0] === 'acWrite' ||
2073
+ curValue[0] === 'acFull'))
2074
+ ) {
1489
2075
  if (curQuery) {
1490
2076
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1491
2077
  }
1492
- const mdate = `param${++count.i}`;
2078
+ const value = `param${++count.i}`;
1493
2079
  curQuery +=
1494
2080
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1495
2081
  ieTable +
1496
- '.`mdate`<=@' +
1497
- mdate;
1498
- params[mdate] = isNaN(Number(curValue[1]))
2082
+ '.' +
2083
+ MySQLDriver.escape(curValue[0]) +
2084
+ '<=@' +
2085
+ value;
2086
+ params[value] = isNaN(Number(curValue[1]))
1499
2087
  ? null
1500
2088
  : Number(curValue[1]);
1501
- break;
1502
2089
  } else {
1503
2090
  if (curQuery) {
1504
2091
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1507,10 +2094,11 @@ export default class MySQLDriver extends NymphDriver {
1507
2094
  const value = `param${++count.i}`;
1508
2095
  curQuery +=
1509
2096
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2097
+ 'EXISTS (SELECT `guid` FROM ' +
2098
+ MySQLDriver.escape(this.prefix + 'data_' + etype) +
2099
+ ' WHERE `guid`=' +
1510
2100
  ieTable +
1511
- '.`guid` IN (SELECT `guid` FROM ' +
1512
- MySQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1513
- ' WHERE `name`=@' +
2101
+ '.`guid` AND `name`=@' +
1514
2102
  name +
1515
2103
  ' AND `number`<=@' +
1516
2104
  value +
@@ -1534,32 +2122,66 @@ export default class MySQLDriver extends NymphDriver {
1534
2122
  if (curQuery) {
1535
2123
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1536
2124
  }
1537
- const name = `param${++count.i}`;
1538
- const guid = `param${++count.i}`;
1539
- curQuery +=
1540
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1541
- ieTable +
1542
- '.`guid` IN (SELECT `guid` FROM ' +
1543
- MySQLDriver.escape(this.prefix + 'references_' + etype) +
1544
- ' WHERE `name`=@' +
1545
- name +
1546
- ' AND `reference`=UNHEX(@' +
1547
- guid +
1548
- '))';
1549
- params[name] = curValue[0];
1550
- params[guid] = curQguid;
2125
+ if (
2126
+ this.nymph.tilmeld != null &&
2127
+ (curValue[0] === 'user' || curValue[0] === 'group')
2128
+ ) {
2129
+ const guid = `param${++count.i}`;
2130
+ curQuery +=
2131
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2132
+ ieTable +
2133
+ '.' +
2134
+ MySQLDriver.escape(curValue[0]) +
2135
+ '=UNHEX(@' +
2136
+ guid +
2137
+ ')';
2138
+ params[guid] = curQguid;
2139
+ } else if (
2140
+ this.nymph.tilmeld != null &&
2141
+ (curValue[0] === 'acRead' ||
2142
+ curValue[0] === 'acWrite' ||
2143
+ curValue[0] === 'acFull')
2144
+ ) {
2145
+ const queryParam = `param${++count.i}`;
2146
+ curQuery +=
2147
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2148
+ 'MATCH (' +
2149
+ ieTable +
2150
+ '.' +
2151
+ MySQLDriver.escape(curValue[0]) +
2152
+ ') AGAINST (@' +
2153
+ queryParam +
2154
+ ' IN BOOLEAN MODE)';
2155
+ params[queryParam] = '+' + curQguid;
2156
+ } else {
2157
+ const name = `param${++count.i}`;
2158
+ const guid = `param${++count.i}`;
2159
+ curQuery +=
2160
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2161
+ 'EXISTS (SELECT `guid` FROM ' +
2162
+ MySQLDriver.escape(this.prefix + 'references_' + etype) +
2163
+ ' WHERE `guid`=' +
2164
+ ieTable +
2165
+ '.`guid` AND `name`=@' +
2166
+ name +
2167
+ ' AND `reference`=UNHEX(@' +
2168
+ guid +
2169
+ '))';
2170
+ params[name] = curValue[0];
2171
+ params[guid] = curQguid;
2172
+ }
1551
2173
  break;
1552
2174
  case 'selector':
1553
2175
  case '!selector':
1554
- const subquery = this.makeEntityQuery(
1555
- options,
2176
+ const innerquery = this.makeEntityQuery(
2177
+ { ...options, sort: null, limit: undefined },
1556
2178
  [curValue],
1557
2179
  etype,
1558
2180
  count,
1559
2181
  params,
1560
2182
  true,
1561
2183
  tableSuffix,
1562
- etypes
2184
+ etypes,
1563
2185
  );
1564
2186
  if (curQuery) {
1565
2187
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1567,61 +2189,174 @@ export default class MySQLDriver extends NymphDriver {
1567
2189
  curQuery +=
1568
2190
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1569
2191
  '(' +
1570
- subquery.query +
2192
+ innerquery.query +
1571
2193
  ')';
1572
2194
  break;
1573
2195
  case 'qref':
1574
2196
  case '!qref':
2197
+ const referenceTableSuffix = makeTableSuffix();
1575
2198
  const [qrefOptions, ...qrefSelectors] = curValue[1] as [
1576
2199
  Options,
1577
- ...FormattedSelector[]
2200
+ ...FormattedSelector[],
1578
2201
  ];
1579
2202
  const QrefEntityClass = qrefOptions.class as EntityConstructor;
1580
2203
  etypes.push(QrefEntityClass.ETYPE);
1581
- const qrefQuery = this.makeEntityQuery(
1582
- { ...qrefOptions, return: 'guid', class: QrefEntityClass },
1583
- qrefSelectors,
1584
- QrefEntityClass.ETYPE,
1585
- count,
1586
- params,
1587
- false,
1588
- makeTableSuffix(),
1589
- etypes
1590
- );
1591
2204
  if (curQuery) {
1592
2205
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1593
2206
  }
1594
- const qrefName = `param${++count.i}`;
1595
- curQuery +=
1596
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1597
- ieTable +
1598
- '.`guid` IN (SELECT `guid` FROM ' +
1599
- MySQLDriver.escape(this.prefix + 'references_' + etype) +
1600
- ' WHERE `name`=@' +
1601
- qrefName +
1602
- ' AND `reference` IN (' +
1603
- qrefQuery.query +
1604
- '))';
1605
- params[qrefName] = curValue[0];
2207
+ if (
2208
+ this.nymph.tilmeld != null &&
2209
+ (curValue[0] === 'user' || curValue[0] === 'group')
2210
+ ) {
2211
+ const qrefQuery = this.makeEntityQuery(
2212
+ {
2213
+ ...qrefOptions,
2214
+ sort: qrefOptions.sort ?? null,
2215
+ return: 'guid',
2216
+ class: QrefEntityClass,
2217
+ },
2218
+ qrefSelectors,
2219
+ QrefEntityClass.ETYPE,
2220
+ count,
2221
+ params,
2222
+ false,
2223
+ makeTableSuffix(),
2224
+ etypes,
2225
+ 'r' +
2226
+ referenceTableSuffix +
2227
+ '.' +
2228
+ MySQLDriver.escape(curValue[0]),
2229
+ );
2230
+
2231
+ curQuery +=
2232
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2233
+ 'EXISTS (SELECT `guid` FROM ' +
2234
+ MySQLDriver.escape(this.prefix + 'entities_' + etype) +
2235
+ ' r' +
2236
+ referenceTableSuffix +
2237
+ ' WHERE r' +
2238
+ referenceTableSuffix +
2239
+ '.`guid`=' +
2240
+ ieTable +
2241
+ '.`guid` AND EXISTS (' +
2242
+ qrefQuery.query +
2243
+ '))';
2244
+ } else if (
2245
+ this.nymph.tilmeld != null &&
2246
+ (curValue[0] === 'acRead' ||
2247
+ curValue[0] === 'acWrite' ||
2248
+ curValue[0] === 'acFull')
2249
+ ) {
2250
+ const qrefQuery = this.makeEntityQuery(
2251
+ {
2252
+ ...qrefOptions,
2253
+ sort: qrefOptions.sort ?? null,
2254
+ return: 'guid',
2255
+ class: QrefEntityClass,
2256
+ },
2257
+ qrefSelectors,
2258
+ QrefEntityClass.ETYPE,
2259
+ count,
2260
+ params,
2261
+ false,
2262
+ makeTableSuffix(),
2263
+ etypes,
2264
+ undefined,
2265
+ (guid) =>
2266
+ ieTable +
2267
+ '.' +
2268
+ MySQLDriver.escape(curValue[0]) +
2269
+ " LIKE CONCAT('% ', LOWER(HEX(" +
2270
+ guid +
2271
+ ")), ' %')",
2272
+ );
2273
+
2274
+ curQuery +=
2275
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2276
+ 'EXISTS (' +
2277
+ qrefQuery.query +
2278
+ ')';
2279
+ } else {
2280
+ const qrefQuery = this.makeEntityQuery(
2281
+ {
2282
+ ...qrefOptions,
2283
+ sort: qrefOptions.sort ?? null,
2284
+ return: 'guid',
2285
+ class: QrefEntityClass,
2286
+ },
2287
+ qrefSelectors,
2288
+ QrefEntityClass.ETYPE,
2289
+ count,
2290
+ params,
2291
+ false,
2292
+ makeTableSuffix(),
2293
+ etypes,
2294
+ 'r' + referenceTableSuffix + '.`reference`',
2295
+ );
2296
+ const qrefName = `param${++count.i}`;
2297
+ curQuery +=
2298
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2299
+ 'EXISTS (SELECT `guid` FROM ' +
2300
+ MySQLDriver.escape(this.prefix + 'references_' + etype) +
2301
+ ' r' +
2302
+ referenceTableSuffix +
2303
+ ' WHERE r' +
2304
+ referenceTableSuffix +
2305
+ '.`guid`=' +
2306
+ ieTable +
2307
+ '.`guid` AND r' +
2308
+ referenceTableSuffix +
2309
+ '.`name`=@' +
2310
+ qrefName +
2311
+ ' AND EXISTS (' +
2312
+ qrefQuery.query +
2313
+ '))';
2314
+ params[qrefName] = curValue[0];
2315
+ }
1606
2316
  break;
1607
2317
  }
1608
2318
  }
1609
2319
  return curQuery;
1610
- }
2320
+ },
1611
2321
  );
1612
2322
 
1613
2323
  let sortBy: string;
1614
- switch (sort) {
1615
- case 'mdate':
1616
- sortBy = '`mdate`';
1617
- break;
1618
- case 'cdate':
1619
- default:
1620
- sortBy = '`cdate`';
1621
- break;
1622
- }
1623
- if (options.reverse) {
1624
- sortBy += ' DESC';
2324
+ let sortByInner: string;
2325
+ let sortJoin = '';
2326
+ let sortJoinInner = '';
2327
+ const order = options.reverse ? ' DESC' : '';
2328
+ if (sort == null) {
2329
+ sortBy = '';
2330
+ sortByInner = '';
2331
+ } else {
2332
+ switch (sort) {
2333
+ case 'mdate':
2334
+ sortBy = `ORDER BY ${eTable}.\`mdate\`${order}`;
2335
+ sortByInner = `ORDER BY ${ieTable}.\`mdate\`${order}`;
2336
+ break;
2337
+ case 'cdate':
2338
+ sortBy = `ORDER BY ${eTable}.\`cdate\`${order}`;
2339
+ sortByInner = `ORDER BY ${ieTable}.\`cdate\`${order}`;
2340
+ break;
2341
+ default:
2342
+ const name = `param${++count.i}`;
2343
+ sortJoin = `LEFT JOIN (
2344
+ SELECT \`guid\`, \`string\`, \`number\`
2345
+ FROM ${MySQLDriver.escape(this.prefix + 'data_' + etype)}
2346
+ WHERE \`name\`=@${name}
2347
+ ORDER BY \`number\`${order}, \`string\`${order}
2348
+ ) ${sTable} ON ${eTable}.\`guid\`=${sTable}.\`guid\``;
2349
+ sortJoinInner = `LEFT JOIN (
2350
+ SELECT \`guid\`, \`string\`, \`number\`
2351
+ FROM ${MySQLDriver.escape(this.prefix + 'data_' + etype)}
2352
+ WHERE \`name\`=@${name}
2353
+ ORDER BY \`number\`${order}, \`string\`${order}
2354
+ ) ${sTable} ON ${ieTable}.\`guid\`=${sTable}.\`guid\``;
2355
+ sortBy = `ORDER BY ${sTable}.\`number\`${order}, ${sTable}.\`string\`${order}`;
2356
+ sortByInner = sortBy;
2357
+ params[name] = sort;
2358
+ break;
2359
+ }
1625
2360
  }
1626
2361
 
1627
2362
  let query: string;
@@ -1632,31 +2367,36 @@ export default class MySQLDriver extends NymphDriver {
1632
2367
  let limit = '';
1633
2368
  if ('limit' in options) {
1634
2369
  limit = ` LIMIT ${Math.floor(
1635
- isNaN(Number(options.limit)) ? 0 : Number(options.limit)
2370
+ isNaN(Number(options.limit)) ? 0 : Number(options.limit),
1636
2371
  )}`;
1637
2372
  }
1638
2373
  let offset = '';
1639
2374
  if ('offset' in options) {
1640
2375
  offset = ` OFFSET ${Math.floor(
1641
- isNaN(Number(options.offset)) ? 0 : Number(options.offset)
2376
+ isNaN(Number(options.offset)) ? 0 : Number(options.offset),
1642
2377
  )}`;
1643
2378
  }
1644
2379
  const whereClause = queryParts.join(') AND (');
2380
+ const guidClause = guidExplicitSelector
2381
+ ? `${guidExplicitSelector(`${ieTable}.\`guid\``)} AND `
2382
+ : guidSelector
2383
+ ? `${ieTable}.\`guid\`=${guidSelector} AND `
2384
+ : '';
1645
2385
  if (options.return === 'count') {
1646
2386
  if (limit || offset) {
1647
2387
  query = `SELECT COUNT(${countTable}.\`guid\`) AS \`count\` FROM (
1648
2388
  SELECT ${ieTable}.\`guid\` AS \`guid\`
1649
2389
  FROM ${MySQLDriver.escape(
1650
- `${this.prefix}entities_${etype}`
2390
+ `${this.prefix}entities_${etype}`,
1651
2391
  )} ${ieTable}
1652
- WHERE (${whereClause})${limit}${offset}
2392
+ WHERE ${guidClause}(${whereClause})${limit}${offset}
1653
2393
  ) ${countTable}`;
1654
2394
  } else {
1655
2395
  query = `SELECT COUNT(${ieTable}.\`guid\`) AS \`count\`
1656
2396
  FROM ${MySQLDriver.escape(
1657
- `${this.prefix}entities_${etype}`
2397
+ `${this.prefix}entities_${etype}`,
1658
2398
  )} ${ieTable}
1659
- WHERE (${whereClause})`;
2399
+ WHERE ${guidClause}(${whereClause})`;
1660
2400
  }
1661
2401
  } else if (options.return === 'guid') {
1662
2402
  const guidColumn =
@@ -1665,38 +2405,47 @@ export default class MySQLDriver extends NymphDriver {
1665
2405
  : `${ieTable}.\`guid\``;
1666
2406
  query = `SELECT ${guidColumn} AS \`guid\`
1667
2407
  FROM ${MySQLDriver.escape(
1668
- `${this.prefix}entities_${etype}`
2408
+ `${this.prefix}entities_${etype}`,
1669
2409
  )} ${ieTable}
1670
- WHERE (${whereClause})
1671
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
2410
+ ${sortJoinInner}
2411
+ WHERE ${guidClause}(${whereClause})
2412
+ ${sortByInner ? sortByInner + ', ' : 'ORDER BY '}${ieTable}.\`guid\`${limit}${offset}`;
1672
2413
  } else {
1673
2414
  query = `SELECT
1674
2415
  LOWER(HEX(${eTable}.\`guid\`)) AS \`guid\`,
1675
2416
  ${eTable}.\`tags\`,
1676
2417
  ${eTable}.\`cdate\`,
1677
2418
  ${eTable}.\`mdate\`,
2419
+ LOWER(HEX(${eTable}.\`user\`)) AS \`user\`,
2420
+ LOWER(HEX(${eTable}.\`group\`)) AS \`group\`,
2421
+ ${eTable}.\`acUser\`,
2422
+ ${eTable}.\`acGroup\`,
2423
+ ${eTable}.\`acOther\`,
2424
+ ${eTable}.\`acRead\`,
2425
+ ${eTable}.\`acWrite\`,
2426
+ ${eTable}.\`acFull\`,
1678
2427
  ${dTable}.\`name\`,
1679
2428
  ${dTable}.\`value\`,
1680
- ${cTable}.\`string\`,
1681
- ${cTable}.\`number\`
2429
+ ${dTable}.\`json\`,
2430
+ ${dTable}.\`string\`,
2431
+ ${dTable}.\`number\`
1682
2432
  FROM ${MySQLDriver.escape(
1683
- `${this.prefix}entities_${etype}`
2433
+ `${this.prefix}entities_${etype}`,
1684
2434
  )} ${eTable}
1685
2435
  LEFT JOIN ${MySQLDriver.escape(
1686
- `${this.prefix}data_${etype}`
2436
+ `${this.prefix}data_${etype}`,
1687
2437
  )} ${dTable} ON ${eTable}.\`guid\`=${dTable}.\`guid\`
1688
- INNER JOIN ${MySQLDriver.escape(
1689
- `${this.prefix}comparisons_${etype}`
1690
- )} ${cTable} ON ${dTable}.\`guid\`=${cTable}.\`guid\` AND ${dTable}.\`name\`=${cTable}.\`name\`
2438
+ ${sortJoin}
1691
2439
  INNER JOIN (
1692
2440
  SELECT ${ieTable}.\`guid\`
1693
2441
  FROM ${MySQLDriver.escape(
1694
- `${this.prefix}entities_${etype}`
2442
+ `${this.prefix}entities_${etype}`,
1695
2443
  )} ${ieTable}
1696
- WHERE (${whereClause})
1697
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
2444
+ ${sortJoinInner}
2445
+ WHERE ${guidClause}(${whereClause})
2446
+ ${sortByInner}${limit}${offset}
1698
2447
  ) ${fTable} ON ${eTable}.\`guid\`=${fTable}.\`guid\`
1699
- ORDER BY ${eTable}.${sortBy}`;
2448
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}.\`guid\``;
1700
2449
  }
1701
2450
  }
1702
2451
  } else {
@@ -1706,28 +2455,33 @@ export default class MySQLDriver extends NymphDriver {
1706
2455
  let limit = '';
1707
2456
  if ('limit' in options) {
1708
2457
  limit = ` LIMIT ${Math.floor(
1709
- isNaN(Number(options.limit)) ? 0 : Number(options.limit)
2458
+ isNaN(Number(options.limit)) ? 0 : Number(options.limit),
1710
2459
  )}`;
1711
2460
  }
1712
2461
  let offset = '';
1713
2462
  if ('offset' in options) {
1714
2463
  offset = ` OFFSET ${Math.floor(
1715
- isNaN(Number(options.offset)) ? 0 : Number(options.offset)
2464
+ isNaN(Number(options.offset)) ? 0 : Number(options.offset),
1716
2465
  )}`;
1717
2466
  }
2467
+ const guidClause = guidExplicitSelector
2468
+ ? ` WHERE ${guidExplicitSelector(`${ieTable}.\`guid\``)}`
2469
+ : guidSelector
2470
+ ? ` WHERE ${ieTable}.\`guid\`=${guidSelector}`
2471
+ : '';
1718
2472
  if (options.return === 'count') {
1719
2473
  if (limit || offset) {
1720
2474
  query = `SELECT COUNT(${countTable}.\`guid\`) AS \`count\` FROM (
1721
2475
  SELECT ${ieTable}.\`guid\` AS \`guid\`
1722
2476
  FROM ${MySQLDriver.escape(
1723
- `${this.prefix}entities_${etype}`
1724
- )} ${ieTable}${limit}${offset}
2477
+ `${this.prefix}entities_${etype}`,
2478
+ )} ${ieTable}${guidClause}${limit}${offset}
1725
2479
  ) ${countTable}`;
1726
2480
  } else {
1727
2481
  query = `SELECT COUNT(${ieTable}.\`guid\`) AS \`count\`
1728
2482
  FROM ${MySQLDriver.escape(
1729
- `${this.prefix}entities_${etype}`
1730
- )} ${ieTable}`;
2483
+ `${this.prefix}entities_${etype}`,
2484
+ )} ${ieTable}${guidClause}`;
1731
2485
  }
1732
2486
  } else if (options.return === 'guid') {
1733
2487
  const guidColumn =
@@ -1736,9 +2490,11 @@ export default class MySQLDriver extends NymphDriver {
1736
2490
  : `${ieTable}.\`guid\``;
1737
2491
  query = `SELECT ${guidColumn} AS \`guid\`
1738
2492
  FROM ${MySQLDriver.escape(
1739
- `${this.prefix}entities_${etype}`
2493
+ `${this.prefix}entities_${etype}`,
1740
2494
  )} ${ieTable}
1741
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
2495
+ ${sortJoinInner}
2496
+ ${guidClause}
2497
+ ${sortByInner ? sortByInner + ', ' : 'ORDER BY '}${ieTable}.\`guid\`${limit}${offset}`;
1742
2498
  } else {
1743
2499
  if (limit || offset) {
1744
2500
  query = `SELECT
@@ -1746,47 +2502,64 @@ export default class MySQLDriver extends NymphDriver {
1746
2502
  ${eTable}.\`tags\`,
1747
2503
  ${eTable}.\`cdate\`,
1748
2504
  ${eTable}.\`mdate\`,
2505
+ LOWER(HEX(${eTable}.\`user\`)) AS \`user\`,
2506
+ LOWER(HEX(${eTable}.\`group\`)) AS \`group\`,
2507
+ ${eTable}.\`acUser\`,
2508
+ ${eTable}.\`acGroup\`,
2509
+ ${eTable}.\`acOther\`,
2510
+ ${eTable}.\`acRead\`,
2511
+ ${eTable}.\`acWrite\`,
2512
+ ${eTable}.\`acFull\`,
1749
2513
  ${dTable}.\`name\`,
1750
2514
  ${dTable}.\`value\`,
1751
- ${cTable}.\`string\`,
1752
- ${cTable}.\`number\`
2515
+ ${dTable}.\`json\`,
2516
+ ${dTable}.\`string\`,
2517
+ ${dTable}.\`number\`
1753
2518
  FROM ${MySQLDriver.escape(
1754
- `${this.prefix}entities_${etype}`
2519
+ `${this.prefix}entities_${etype}`,
1755
2520
  )} ${eTable}
1756
2521
  LEFT JOIN ${MySQLDriver.escape(
1757
- `${this.prefix}data_${etype}`
2522
+ `${this.prefix}data_${etype}`,
1758
2523
  )} ${dTable} ON ${eTable}.\`guid\`=${dTable}.\`guid\`
1759
- INNER JOIN ${MySQLDriver.escape(
1760
- `${this.prefix}comparisons_${etype}`
1761
- )} ${cTable} ON ${dTable}.\`guid\`=${cTable}.\`guid\` AND ${dTable}.\`name\`=${cTable}.\`name\`
2524
+ ${sortJoin}
1762
2525
  INNER JOIN (
1763
2526
  SELECT ${ieTable}.\`guid\`
1764
2527
  FROM ${MySQLDriver.escape(
1765
- `${this.prefix}entities_${etype}`
2528
+ `${this.prefix}entities_${etype}`,
1766
2529
  )} ${ieTable}
1767
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
2530
+ ${sortJoinInner}
2531
+ ${guidClause}
2532
+ ${sortByInner}${limit}${offset}
1768
2533
  ) ${fTable} ON ${eTable}.\`guid\`=${fTable}.\`guid\`
1769
- ORDER BY ${eTable}.${sortBy}`;
2534
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}.\`guid\``;
1770
2535
  } else {
1771
2536
  query = `SELECT
1772
2537
  LOWER(HEX(${eTable}.\`guid\`)) AS \`guid\`,
1773
2538
  ${eTable}.\`tags\`,
1774
2539
  ${eTable}.\`cdate\`,
1775
2540
  ${eTable}.\`mdate\`,
2541
+ LOWER(HEX(${eTable}.\`user\`)) AS \`user\`,
2542
+ LOWER(HEX(${eTable}.\`group\`)) AS \`group\`,
2543
+ ${eTable}.\`acUser\`,
2544
+ ${eTable}.\`acGroup\`,
2545
+ ${eTable}.\`acOther\`,
2546
+ ${eTable}.\`acRead\`,
2547
+ ${eTable}.\`acWrite\`,
2548
+ ${eTable}.\`acFull\`,
1776
2549
  ${dTable}.\`name\`,
1777
2550
  ${dTable}.\`value\`,
1778
- ${cTable}.\`string\`,
1779
- ${cTable}.\`number\`
2551
+ ${dTable}.\`json\`,
2552
+ ${dTable}.\`string\`,
2553
+ ${dTable}.\`number\`
1780
2554
  FROM ${MySQLDriver.escape(
1781
- `${this.prefix}entities_${etype}`
2555
+ `${this.prefix}entities_${etype}`,
1782
2556
  )} ${eTable}
1783
2557
  LEFT JOIN ${MySQLDriver.escape(
1784
- `${this.prefix}data_${etype}`
2558
+ `${this.prefix}data_${etype}`,
1785
2559
  )} ${dTable} ON ${eTable}.\`guid\`=${dTable}.\`guid\`
1786
- INNER JOIN ${MySQLDriver.escape(
1787
- `${this.prefix}comparisons_${etype}`
1788
- )} ${cTable} ON ${dTable}.\`guid\`=${cTable}.\`guid\` AND ${dTable}.\`name\`=${cTable}.\`name\`
1789
- ORDER BY ${eTable}.${sortBy}`;
2560
+ ${sortJoin}
2561
+ ${guidExplicitSelector ? `WHERE ${guidExplicitSelector(`${eTable}.\`guid\``)}` : guidSelector ? `WHERE ${eTable}.\`guid\`=${guidSelector}` : ''}
2562
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}.\`guid\``;
1790
2563
  }
1791
2564
  }
1792
2565
  }
@@ -1806,38 +2579,18 @@ export default class MySQLDriver extends NymphDriver {
1806
2579
  protected performQuery(
1807
2580
  options: Options,
1808
2581
  formattedSelectors: FormattedSelector[],
1809
- etype: string
2582
+ etype: string,
1810
2583
  ): {
1811
2584
  result: any;
1812
2585
  } {
1813
2586
  const { query, params, etypes } = this.makeEntityQuery(
1814
2587
  options,
1815
2588
  formattedSelectors,
1816
- etype
1817
- );
1818
- const result = this.queryIter(query, { etypes, params }).then((val) =>
1819
- val[Symbol.iterator]()
2589
+ etype,
1820
2590
  );
1821
- return {
1822
- result,
1823
- };
1824
- }
1825
-
1826
- protected performQuerySync(
1827
- options: Options,
1828
- formattedSelectors: FormattedSelector[],
1829
- etype: string
1830
- ): {
1831
- result: any;
1832
- } {
1833
- const { query, params, etypes } = this.makeEntityQuery(
1834
- options,
1835
- formattedSelectors,
1836
- etype
2591
+ const result = this.queryArray(query, { etypes, params }).then((val) =>
2592
+ val[Symbol.iterator](),
1837
2593
  );
1838
- const result = (this.queryIterSync(query, { etypes, params }) || [])[
1839
- Symbol.iterator
1840
- ]();
1841
2594
  return {
1842
2595
  result,
1843
2596
  };
@@ -1851,20 +2604,26 @@ export default class MySQLDriver extends NymphDriver {
1851
2604
  options: Options<T> & { return: 'guid' },
1852
2605
  ...selectors: Selector[]
1853
2606
  ): Promise<string[]>;
2607
+ public async getEntities<T extends EntityConstructor = EntityConstructor>(
2608
+ options: Options<T> & { return: 'object' },
2609
+ ...selectors: Selector[]
2610
+ ): Promise<EntityObjectType<T>[]>;
1854
2611
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
1855
2612
  options?: Options<T>,
1856
2613
  ...selectors: Selector[]
1857
- ): Promise<ReturnType<T['factorySync']>[]>;
2614
+ ): Promise<EntityInstanceType<T>[]>;
1858
2615
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
1859
2616
  options: Options<T> = {},
1860
2617
  ...selectors: Selector[]
1861
- ): Promise<ReturnType<T['factorySync']>[] | string[] | number> {
1862
- const { result: resultPromise, process } = this.getEntitesRowLike<T>(
2618
+ ): Promise<
2619
+ EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] | number
2620
+ > {
2621
+ const { result: resultPromise, process } = this.getEntitiesRowLike<T>(
1863
2622
  // @ts-ignore: options is correct here.
1864
2623
  options,
1865
2624
  selectors,
1866
- (options, formattedSelectors, etype) =>
1867
- this.performQuery(options, formattedSelectors, etype),
2625
+ ({ options, selectors, etype }) =>
2626
+ this.performQuery(options, selectors, etype),
1868
2627
  () => {
1869
2628
  const next: any = result.next();
1870
2629
  return next.done ? null : next.value;
@@ -1873,9 +2632,23 @@ export default class MySQLDriver extends NymphDriver {
1873
2632
  (row) => Number(row.count),
1874
2633
  (row) => row.guid,
1875
2634
  (row) => ({
1876
- tags: row.tags.length > 2 ? row.tags.slice(1, -1).split(' ') : [],
1877
- cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
1878
- mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
2635
+ tags:
2636
+ row.tags.length > 2
2637
+ ? row.tags
2638
+ .slice(1, -1)
2639
+ .split(' ')
2640
+ .filter((tag: string) => tag)
2641
+ : [],
2642
+ cdate: isNaN(Number(row.cdate)) ? Date.now() : Number(row.cdate),
2643
+ mdate: isNaN(Number(row.mdate)) ? Date.now() : Number(row.mdate),
2644
+ user: row.user,
2645
+ group: row.group,
2646
+ acUser: row.acUser,
2647
+ acGroup: row.acGroup,
2648
+ acOther: row.acOther,
2649
+ acRead: row.acRead?.slice(1, -1).split(' ') ?? [],
2650
+ acWrite: row.acWrite?.slice(1, -1).split(' ') ?? [],
2651
+ acFull: row.acFull?.slice(1, -1).split(' ') ?? [],
1879
2652
  }),
1880
2653
  (row) => ({
1881
2654
  name: row.name,
@@ -1883,9 +2656,11 @@ export default class MySQLDriver extends NymphDriver {
1883
2656
  row.value === 'N'
1884
2657
  ? JSON.stringify(row.number)
1885
2658
  : row.value === 'S'
1886
- ? JSON.stringify(row.string)
1887
- : row.value,
1888
- })
2659
+ ? JSON.stringify(row.string)
2660
+ : row.value === 'J'
2661
+ ? JSON.stringify(row.json)
2662
+ : row.value,
2663
+ }),
1889
2664
  );
1890
2665
 
1891
2666
  const result = await resultPromise;
@@ -1896,231 +2671,374 @@ export default class MySQLDriver extends NymphDriver {
1896
2671
  return value;
1897
2672
  }
1898
2673
 
1899
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1900
- options: Options<T> & { return: 'count' },
1901
- ...selectors: Selector[]
1902
- ): number;
1903
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1904
- options: Options<T> & { return: 'guid' },
1905
- ...selectors: Selector[]
1906
- ): string[];
1907
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1908
- options?: Options<T>,
1909
- ...selectors: Selector[]
1910
- ): ReturnType<T['factorySync']>[];
1911
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1912
- options: Options<T> = {},
1913
- ...selectors: Selector[]
1914
- ): ReturnType<T['factorySync']>[] | string[] | number {
1915
- const { result, process } = this.getEntitesRowLike<T>(
1916
- // @ts-ignore: options is correct here.
1917
- options,
1918
- selectors,
1919
- (options, formattedSelectors, etype) =>
1920
- this.performQuerySync(options, formattedSelectors, etype),
1921
- () => {
1922
- const next: any = result.next();
1923
- return next.done ? null : next.value;
1924
- },
1925
- () => undefined,
1926
- (row) => Number(row.count),
1927
- (row) => row.guid,
1928
- (row) => ({
1929
- tags: row.tags.length > 2 ? row.tags.slice(1, -1).split(',') : [],
1930
- cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
1931
- mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
1932
- }),
1933
- (row) => ({
1934
- name: row.name,
1935
- svalue:
1936
- row.value === 'N'
1937
- ? JSON.stringify(row.number)
1938
- : row.value === 'S'
1939
- ? JSON.stringify(row.string)
1940
- : row.value,
1941
- })
1942
- );
1943
- const value = process();
1944
- if (value instanceof Error) {
1945
- throw value;
1946
- }
1947
- return value;
1948
- }
1949
-
1950
2674
  public async getUID(name: string) {
1951
2675
  if (name == null) {
1952
2676
  throw new InvalidParametersError('Name not given for UID.');
1953
2677
  }
1954
2678
  const result = await this.queryGet(
1955
2679
  `SELECT \`cur_uid\` FROM ${MySQLDriver.escape(
1956
- `${this.prefix}uids`
2680
+ `${this.prefix}uids`,
1957
2681
  )} WHERE \`name\`=@name;`,
1958
2682
  {
1959
2683
  params: {
1960
2684
  name: name,
1961
2685
  },
1962
- }
2686
+ },
1963
2687
  );
1964
2688
  return (result?.cur_uid as number | null) ?? null;
1965
2689
  }
1966
2690
 
1967
- public async import(filename: string) {
2691
+ public async importEntity(entity: {
2692
+ guid: string;
2693
+ cdate: number;
2694
+ mdate: number;
2695
+ tags: string[];
2696
+ sdata: SerializedEntityData;
2697
+ etype: string;
2698
+ }) {
2699
+ return await this.importEntityInternal(entity);
2700
+ }
2701
+
2702
+ public async importEntityTokens(entity: {
2703
+ guid: string;
2704
+ cdate: number;
2705
+ mdate: number;
2706
+ tags: string[];
2707
+ sdata: SerializedEntityData;
2708
+ etype: string;
2709
+ }) {
2710
+ return await this.importEntityInternal(entity, { only: 'tokens' });
2711
+ }
2712
+
2713
+ public async importEntityTilmeldAC(entity: {
2714
+ guid: string;
2715
+ cdate: number;
2716
+ mdate: number;
2717
+ tags: string[];
2718
+ sdata: SerializedEntityData;
2719
+ etype: string;
2720
+ }) {
2721
+ return await this.importEntityInternal(entity, { only: 'tilmeldAC' });
2722
+ }
2723
+
2724
+ private async importEntityInternal(
2725
+ {
2726
+ guid,
2727
+ cdate,
2728
+ mdate,
2729
+ tags,
2730
+ sdata,
2731
+ etype,
2732
+ }: {
2733
+ guid: string;
2734
+ cdate: number;
2735
+ mdate: number;
2736
+ tags: string[];
2737
+ sdata: SerializedEntityData;
2738
+ etype: string;
2739
+ },
2740
+ { only = undefined }: { only?: 'tokens' | 'tilmeldAC' } = {},
2741
+ ) {
1968
2742
  try {
1969
- const result = await this.importFromFile(
1970
- filename,
1971
- async (guid, tags, sdata, etype) => {
1972
- await this.queryRun(
1973
- `REPLACE INTO ${MySQLDriver.escape(
1974
- `${this.prefix}entities_${etype}`
1975
- )} (\`guid\`, \`tags\`, \`cdate\`, \`mdate\`) VALUES (UNHEX(@guid), @tags, @cdate, @mdate);`,
2743
+ let promises = [];
2744
+ if (only == null) {
2745
+ promises.push(
2746
+ this.queryRun(
2747
+ `DELETE FROM ${MySQLDriver.escape(
2748
+ `${this.prefix}entities_${etype}`,
2749
+ )} WHERE \`guid\`=UNHEX(@guid);`,
1976
2750
  {
1977
2751
  etypes: [etype],
1978
2752
  params: {
1979
2753
  guid,
1980
- tags: ' ' + tags.join(' ') + ' ',
1981
- cdate: isNaN(Number(JSON.parse(sdata.cdate)))
1982
- ? null
1983
- : Number(JSON.parse(sdata.cdate)),
1984
- mdate: isNaN(Number(JSON.parse(sdata.mdate)))
1985
- ? null
1986
- : Number(JSON.parse(sdata.mdate)),
1987
2754
  },
1988
- }
1989
- );
1990
- const promises = [];
1991
- promises.push(
1992
- this.queryRun(
1993
- `DELETE FROM ${MySQLDriver.escape(
1994
- `${this.prefix}data_${etype}`
1995
- )} WHERE \`guid\`=UNHEX(@guid);`,
1996
- {
1997
- etypes: [etype],
1998
- params: {
1999
- guid,
2000
- },
2001
- }
2002
- )
2003
- );
2004
- promises.push(
2005
- this.queryRun(
2006
- `DELETE FROM ${MySQLDriver.escape(
2007
- `${this.prefix}comparisons_${etype}`
2008
- )} WHERE \`guid\`=UNHEX(@guid);`,
2009
- {
2010
- etypes: [etype],
2011
- params: {
2012
- guid,
2013
- },
2014
- }
2015
- )
2016
- );
2755
+ },
2756
+ ),
2757
+ );
2758
+ promises.push(
2759
+ this.queryRun(
2760
+ `DELETE FROM ${MySQLDriver.escape(
2761
+ `${this.prefix}data_${etype}`,
2762
+ )} WHERE \`guid\`=UNHEX(@guid);`,
2763
+ {
2764
+ etypes: [etype],
2765
+ params: {
2766
+ guid,
2767
+ },
2768
+ },
2769
+ ),
2770
+ );
2771
+ promises.push(
2772
+ this.queryRun(
2773
+ `DELETE FROM ${MySQLDriver.escape(
2774
+ `${this.prefix}references_${etype}`,
2775
+ )} WHERE \`guid\`=UNHEX(@guid);`,
2776
+ {
2777
+ etypes: [etype],
2778
+ params: {
2779
+ guid,
2780
+ },
2781
+ },
2782
+ ),
2783
+ );
2784
+ }
2785
+ if (only == null || only === 'tokens') {
2786
+ promises.push(
2787
+ this.queryRun(
2788
+ `DELETE FROM ${MySQLDriver.escape(
2789
+ `${this.prefix}tokens_${etype}`,
2790
+ )} WHERE \`guid\`=UNHEX(@guid);`,
2791
+ {
2792
+ etypes: [etype],
2793
+ params: {
2794
+ guid,
2795
+ },
2796
+ },
2797
+ ),
2798
+ );
2799
+ }
2800
+ if (only == null) {
2801
+ promises.push(
2802
+ this.queryRun(
2803
+ `DELETE FROM ${MySQLDriver.escape(
2804
+ `${this.prefix}uniques_${etype}`,
2805
+ )} WHERE \`guid\`=UNHEX(@guid);`,
2806
+ {
2807
+ etypes: [etype],
2808
+ params: {
2809
+ guid,
2810
+ },
2811
+ },
2812
+ ),
2813
+ );
2814
+ }
2815
+
2816
+ await Promise.all(promises);
2817
+ promises = [];
2818
+
2819
+ if (only == null) {
2820
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull } =
2821
+ this.removeAndReturnACValues(etype, {}, sdata);
2822
+
2823
+ await this.queryRun(
2824
+ `INSERT INTO ${MySQLDriver.escape(
2825
+ `${this.prefix}entities_${etype}`,
2826
+ )} (\`guid\`, \`tags\`, \`cdate\`, \`mdate\`, \`user\`, \`group\`, \`acUser\`, \`acGroup\`, \`acOther\`, \`acRead\`, \`acWrite\`, \`acFull\`) VALUES (UNHEX(@guid), @tags, @cdate, @mdate, ${user == null ? '@user' : 'UNHEX(@user)'}, ${group == null ? '@group' : 'UNHEX(@group)'}, @acUser, @acGroup, @acOther, @acRead, @acWrite, @acFull);`,
2827
+ {
2828
+ etypes: [etype],
2829
+ params: {
2830
+ guid,
2831
+ tags: ' ' + tags.join(' ') + ' ',
2832
+ cdate: isNaN(cdate) ? null : cdate,
2833
+ mdate: isNaN(mdate) ? null : mdate,
2834
+ user,
2835
+ group,
2836
+ acUser,
2837
+ acGroup,
2838
+ acOther,
2839
+ acRead: acRead && ' ' + acRead.join(' ') + ' ',
2840
+ acWrite: acWrite && ' ' + acWrite.join(' ') + ' ',
2841
+ acFull: acFull && ' ' + acFull.join(' ') + ' ',
2842
+ },
2843
+ },
2844
+ );
2845
+
2846
+ for (const name in sdata) {
2847
+ const value = sdata[name];
2848
+ const uvalue = JSON.parse(value);
2849
+ if (value === undefined) {
2850
+ continue;
2851
+ }
2852
+ const storageValue =
2853
+ typeof uvalue === 'number'
2854
+ ? 'N'
2855
+ : typeof uvalue === 'string'
2856
+ ? 'S'
2857
+ : 'J';
2858
+ const jsonValue = storageValue === 'J' ? value : null;
2859
+
2017
2860
  promises.push(
2018
2861
  this.queryRun(
2019
- `DELETE FROM ${MySQLDriver.escape(
2020
- `${this.prefix}references_${etype}`
2021
- )} WHERE \`guid\`=UNHEX(@guid);`,
2862
+ `INSERT INTO ${MySQLDriver.escape(
2863
+ `${this.prefix}data_${etype}`,
2864
+ )} (\`guid\`, \`name\`, \`value\`, \`json\`, \`string\`, \`number\`, \`truthy\`) VALUES (UNHEX(@guid), @name, @storageValue, @jsonValue, @string, @number, @truthy);`,
2022
2865
  {
2023
2866
  etypes: [etype],
2024
2867
  params: {
2025
2868
  guid,
2869
+ name,
2870
+ storageValue,
2871
+ jsonValue,
2872
+ string: storageValue === 'J' ? null : `${uvalue}`,
2873
+ number: isNaN(Number(uvalue)) ? null : Number(uvalue),
2874
+ truthy: !!uvalue,
2026
2875
  },
2027
- }
2028
- )
2876
+ },
2877
+ ),
2029
2878
  );
2030
- await Promise.all(promises);
2031
- delete sdata.cdate;
2032
- delete sdata.mdate;
2033
- for (const name in sdata) {
2034
- const value = sdata[name];
2035
- const uvalue = JSON.parse(value);
2036
- if (value === undefined) {
2037
- continue;
2038
- }
2039
- const storageValue =
2040
- typeof uvalue === 'number'
2041
- ? 'N'
2042
- : typeof uvalue === 'string'
2043
- ? 'S'
2044
- : value;
2045
- const promises = [];
2879
+
2880
+ const references = this.findReferences(value);
2881
+ for (const reference of references) {
2046
2882
  promises.push(
2047
2883
  this.queryRun(
2048
2884
  `INSERT INTO ${MySQLDriver.escape(
2049
- `${this.prefix}data_${etype}`
2050
- )} (\`guid\`, \`name\`, \`value\`) VALUES (UNHEX(@guid), @name, @storageValue);`,
2885
+ `${this.prefix}references_${etype}`,
2886
+ )} (\`guid\`, \`name\`, \`reference\`) VALUES (UNHEX(@guid), @name, UNHEX(@reference));`,
2051
2887
  {
2052
2888
  etypes: [etype],
2053
2889
  params: {
2054
2890
  guid,
2055
2891
  name,
2056
- storageValue,
2892
+ reference,
2057
2893
  },
2058
- }
2059
- )
2894
+ },
2895
+ ),
2060
2896
  );
2061
- promises.push(
2062
- this.queryRun(
2063
- `INSERT INTO ${MySQLDriver.escape(
2064
- `${this.prefix}comparisons_${etype}`
2065
- )} (\`guid\`, \`name\`, \`truthy\`, \`string\`, \`number\`) VALUES (UNHEX(@guid), @name, @truthy, @string, @number);`,
2066
- {
2067
- etypes: [etype],
2068
- params: {
2069
- guid,
2070
- name,
2071
- truthy: !!uvalue,
2072
- string: `${uvalue}`,
2073
- number: isNaN(Number(uvalue)) ? null : Number(uvalue),
2074
- },
2075
- }
2076
- )
2077
- );
2078
- const references = this.findReferences(value);
2079
- for (const reference of references) {
2897
+ }
2898
+ }
2899
+ }
2900
+
2901
+ if (only === 'tilmeldAC') {
2902
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull } =
2903
+ this.removeAndReturnACValues(etype, {}, sdata);
2904
+
2905
+ promises.push(
2906
+ this.queryRun(
2907
+ `UPDATE ${MySQLDriver.escape(
2908
+ `${this.prefix}entities_${etype}`,
2909
+ )} SET \`user\`=${user == null ? '@user' : 'UNHEX(@user)'}, \`group\`=${group == null ? '@group' : 'UNHEX(@group)'}, \`acUser\`=@acUser, \`acGroup\`=@acGroup, \`acOther\`=@acOther, \`acRead\`=@acRead, \`acWrite\`=@acWrite, \`acFull\`=@acFull WHERE \`guid\`=UNHEX(@guid);`,
2910
+ {
2911
+ etypes: [etype],
2912
+ params: {
2913
+ user,
2914
+ group,
2915
+ acUser,
2916
+ acGroup,
2917
+ acOther,
2918
+ acRead: acRead && ' ' + acRead.join(' ') + ' ',
2919
+ acWrite: acWrite && ' ' + acWrite.join(' ') + ' ',
2920
+ acFull: acFull && ' ' + acFull.join(' ') + ' ',
2921
+ guid,
2922
+ },
2923
+ },
2924
+ ),
2925
+ );
2926
+ }
2927
+
2928
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
2929
+
2930
+ if (only == null || only === 'tokens') {
2931
+ for (let name in sdata) {
2932
+ let tokenString: string | null = null;
2933
+ try {
2934
+ tokenString = EntityClass.getFTSText(name, JSON.parse(sdata[name]));
2935
+ } catch (e: any) {
2936
+ // Ignore error.
2937
+ }
2938
+
2939
+ if (tokenString != null) {
2940
+ const tokens = this.tokenizer.tokenize(tokenString);
2941
+ while (tokens.length) {
2942
+ const currentTokens = tokens.splice(0, 100);
2943
+ const params: { [k: string]: any } = {
2944
+ guid,
2945
+ name,
2946
+ };
2947
+ const values: string[] = [];
2948
+
2949
+ for (let i = 0; i < currentTokens.length; i++) {
2950
+ const token = currentTokens[i];
2951
+ params['token' + i] = token.token;
2952
+ params['position' + i] = token.position;
2953
+ params['stem' + i] = token.stem;
2954
+ values.push(
2955
+ '(UNHEX(@guid), @name, @token' +
2956
+ i +
2957
+ ', @position' +
2958
+ i +
2959
+ ', @stem' +
2960
+ i +
2961
+ ')',
2962
+ );
2963
+ }
2964
+
2080
2965
  promises.push(
2081
2966
  this.queryRun(
2082
2967
  `INSERT INTO ${MySQLDriver.escape(
2083
- `${this.prefix}references_${etype}`
2084
- )} (\`guid\`, \`name\`, \`reference\`) VALUES (UNHEX(@guid), @name, UNHEX(@reference));`,
2968
+ `${this.prefix}tokens_${etype}`,
2969
+ )} (\`guid\`, \`name\`, \`token\`, \`position\`, \`stem\`) VALUES ${values.join(', ')};`,
2085
2970
  {
2086
2971
  etypes: [etype],
2087
- params: {
2088
- guid,
2089
- name,
2090
- reference,
2091
- },
2092
- }
2093
- )
2972
+ params,
2973
+ },
2974
+ ),
2094
2975
  );
2095
2976
  }
2096
2977
  }
2097
- await Promise.all(promises);
2098
- },
2099
- async (name, curUid) => {
2100
- await this.queryRun(
2101
- `INSERT INTO ${MySQLDriver.escape(
2102
- `${this.prefix}uids`
2103
- )} (\`name\`, \`cur_uid\`) VALUES (@name, @curUid) ON DUPLICATE KEY UPDATE \`cur_uid\`=@curUid;`,
2104
- {
2105
- params: {
2106
- name,
2107
- curUid,
2978
+ }
2979
+ }
2980
+
2981
+ if (only == null) {
2982
+ const uniques = await EntityClass.getUniques({
2983
+ guid,
2984
+ cdate,
2985
+ mdate,
2986
+ tags,
2987
+ data: {},
2988
+ sdata,
2989
+ });
2990
+ for (const unique of uniques) {
2991
+ promises.push(
2992
+ this.queryRun(
2993
+ `INSERT INTO ${MySQLDriver.escape(
2994
+ `${this.prefix}uniques_${etype}`,
2995
+ )} (\`guid\`, \`unique\`) VALUES (UNHEX(@guid), @unique);`,
2996
+ {
2997
+ etypes: [etype],
2998
+ params: {
2999
+ guid,
3000
+ unique,
3001
+ },
2108
3002
  },
2109
- }
3003
+ ).catch((e: any) => {
3004
+ if (e instanceof EntityUniqueConstraintError) {
3005
+ this.nymph.config.debugError(
3006
+ 'mysql',
3007
+ `Import entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`,
3008
+ );
3009
+ }
3010
+ return e;
3011
+ }),
2110
3012
  );
2111
- },
2112
- async () => {
2113
- await this.internalTransaction('nymph-import');
2114
- },
2115
- async () => {
2116
- await this.commit('nymph-import');
2117
3013
  }
2118
- );
3014
+ }
2119
3015
 
2120
- return result;
3016
+ await Promise.all(promises);
2121
3017
  } catch (e: any) {
2122
- await this.rollback('nymph-import');
2123
- return false;
3018
+ this.nymph.config.debugError('mysql', `Import entity error: "${e}"`);
3019
+ throw e;
3020
+ }
3021
+ }
3022
+
3023
+ public async importUID({ name, value }: { name: string; value: number }) {
3024
+ try {
3025
+ await this.internalTransaction(`nymph-import-uid-${name}`);
3026
+ await this.queryRun(
3027
+ `INSERT INTO ${MySQLDriver.escape(
3028
+ `${this.prefix}uids`,
3029
+ )} (\`name\`, \`cur_uid\`) VALUES (@name, @value) ON DUPLICATE KEY UPDATE \`cur_uid\`=@value;`,
3030
+ {
3031
+ params: {
3032
+ name,
3033
+ value,
3034
+ },
3035
+ },
3036
+ );
3037
+ await this.commit(`nymph-import-uid-${name}`);
3038
+ } catch (e: any) {
3039
+ this.nymph.config.debugError('mysql', `Import UID error: "${e}"`);
3040
+ await this.rollback(`nymph-import-uid-${name}`);
3041
+ throw e;
2124
3042
  }
2125
3043
  }
2126
3044
 
@@ -2129,55 +3047,57 @@ export default class MySQLDriver extends NymphDriver {
2129
3047
  throw new InvalidParametersError('Name not given for UID.');
2130
3048
  }
2131
3049
  await this.internalTransaction('nymph-newuid');
3050
+ let curUid: number | null = null;
2132
3051
  try {
2133
3052
  const lock = await this.queryGet(
2134
3053
  `SELECT GET_LOCK(${MySQLDriver.escapeValue(
2135
- `${this.prefix}uids_${name}`
2136
- )}, 10) AS \`lock\`;`
3054
+ `${this.prefix}uids_${name}`,
3055
+ )}, 10) AS \`lock\`;`,
2137
3056
  );
2138
3057
  if (lock.lock !== 1) {
2139
3058
  throw new QueryFailedError("Couldn't get lock for UID: " + name);
2140
3059
  }
2141
- let curUid: number | null = null;
2142
3060
  await this.queryRun(
2143
3061
  `INSERT INTO ${MySQLDriver.escape(
2144
- `${this.prefix}uids`
3062
+ `${this.prefix}uids`,
2145
3063
  )} (\`name\`, \`cur_uid\`) VALUES (@name, 1) ON DUPLICATE KEY UPDATE \`cur_uid\`=\`cur_uid\`+1;`,
2146
3064
  {
2147
3065
  params: {
2148
3066
  name,
2149
3067
  },
2150
- }
3068
+ },
2151
3069
  );
2152
3070
  const result = await this.queryGet(
2153
3071
  `SELECT \`cur_uid\` FROM ${MySQLDriver.escape(
2154
- `${this.prefix}uids`
3072
+ `${this.prefix}uids`,
2155
3073
  )} WHERE \`name\`=@name;`,
2156
3074
  {
2157
3075
  params: {
2158
3076
  name,
2159
3077
  },
2160
- }
3078
+ },
2161
3079
  );
2162
3080
  await this.queryRun(
2163
3081
  `SELECT RELEASE_LOCK(${MySQLDriver.escapeValue(
2164
- `${this.prefix}uids_${name}`
2165
- )});`
3082
+ `${this.prefix}uids_${name}`,
3083
+ )});`,
2166
3084
  );
2167
- await this.commit('nymph-newuid');
2168
3085
  curUid = result.cur_uid ?? null;
2169
- return curUid;
2170
3086
  } catch (e: any) {
3087
+ this.nymph.config.debugError('mysql', `New UID error: "${e}"`);
2171
3088
  if (e?.message !== "Couldn't get lock for UID: " + name) {
2172
3089
  await this.queryRun(
2173
3090
  `SELECT RELEASE_LOCK(${MySQLDriver.escapeValue(
2174
- `${this.prefix}uids_${name}`
2175
- )});`
3091
+ `${this.prefix}uids_${name}`,
3092
+ )});`,
2176
3093
  );
2177
3094
  }
2178
3095
  await this.rollback('nymph-newuid');
2179
3096
  throw e;
2180
3097
  }
3098
+
3099
+ await this.commit('nymph-newuid');
3100
+ return curUid;
2181
3101
  }
2182
3102
 
2183
3103
  public async renameUID(oldName: string, newName: string) {
@@ -2186,14 +3106,14 @@ export default class MySQLDriver extends NymphDriver {
2186
3106
  }
2187
3107
  await this.queryRun(
2188
3108
  `UPDATE ${MySQLDriver.escape(
2189
- `${this.prefix}uids`
3109
+ `${this.prefix}uids`,
2190
3110
  )} SET \`name\`=@newName WHERE \`name\`=@oldName;`,
2191
3111
  {
2192
3112
  params: {
2193
3113
  newName,
2194
3114
  oldName,
2195
3115
  },
2196
- }
3116
+ },
2197
3117
  );
2198
3118
  return true;
2199
3119
  }
@@ -2201,7 +3121,7 @@ export default class MySQLDriver extends NymphDriver {
2201
3121
  public async rollback(name: string) {
2202
3122
  if (name == null || typeof name !== 'string' || name.length === 0) {
2203
3123
  throw new InvalidParametersError(
2204
- 'Transaction rollback attempted without a name.'
3124
+ 'Transaction rollback attempted without a name.',
2205
3125
  );
2206
3126
  }
2207
3127
  if (!this.transaction || this.transaction.count === 0) {
@@ -2226,12 +3146,15 @@ export default class MySQLDriver extends NymphDriver {
2226
3146
  guid: string,
2227
3147
  data: EntityData,
2228
3148
  sdata: SerializedEntityData,
2229
- etype: string
3149
+ uniques: string[],
3150
+ etype: string,
2230
3151
  ) => {
3152
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
3153
+
2231
3154
  const runInsertQuery = async (
2232
3155
  name: string,
2233
3156
  value: any,
2234
- svalue: string
3157
+ svalue: string,
2235
3158
  ) => {
2236
3159
  if (value === undefined) {
2237
3160
  return;
@@ -2240,47 +3163,38 @@ export default class MySQLDriver extends NymphDriver {
2240
3163
  typeof value === 'number'
2241
3164
  ? 'N'
2242
3165
  : typeof value === 'string'
2243
- ? 'S'
2244
- : svalue;
3166
+ ? 'S'
3167
+ : 'J';
3168
+ const jsonValue = storageValue === 'J' ? svalue : null;
3169
+
2245
3170
  const promises = [];
3171
+
2246
3172
  promises.push(
2247
3173
  this.queryRun(
2248
3174
  `INSERT INTO ${MySQLDriver.escape(
2249
- `${this.prefix}data_${etype}`
2250
- )} (\`guid\`, \`name\`, \`value\`) VALUES (UNHEX(@guid), @name, @storageValue);`,
3175
+ `${this.prefix}data_${etype}`,
3176
+ )} (\`guid\`, \`name\`, \`value\`, \`json\`, \`string\`, \`number\`, \`truthy\`) VALUES (UNHEX(@guid), @name, @storageValue, @jsonValue, @string, @number, @truthy);`,
2251
3177
  {
2252
3178
  etypes: [etype],
2253
3179
  params: {
2254
3180
  guid,
2255
3181
  name,
2256
3182
  storageValue,
2257
- },
2258
- }
2259
- )
2260
- );
2261
- promises.push(
2262
- this.queryRun(
2263
- `INSERT INTO ${MySQLDriver.escape(
2264
- `${this.prefix}comparisons_${etype}`
2265
- )} (\`guid\`, \`name\`, \`truthy\`, \`string\`, \`number\`) VALUES (UNHEX(@guid), @name, @truthy, @string, @number);`,
2266
- {
2267
- etypes: [etype],
2268
- params: {
2269
- guid,
2270
- name,
2271
- truthy: !!value,
2272
- string: `${value}`,
3183
+ jsonValue,
3184
+ string: storageValue === 'J' ? null : `${value}`,
2273
3185
  number: isNaN(Number(value)) ? null : Number(value),
3186
+ truthy: !!value,
2274
3187
  },
2275
- }
2276
- )
3188
+ },
3189
+ ),
2277
3190
  );
3191
+
2278
3192
  const references = this.findReferences(svalue);
2279
3193
  for (const reference of references) {
2280
3194
  promises.push(
2281
3195
  this.queryRun(
2282
3196
  `INSERT INTO ${MySQLDriver.escape(
2283
- `${this.prefix}references_${etype}`
3197
+ `${this.prefix}references_${etype}`,
2284
3198
  )} (\`guid\`, \`name\`, \`reference\`) VALUES (UNHEX(@guid), @name, UNHEX(@reference));`,
2285
3199
  {
2286
3200
  etypes: [etype],
@@ -2289,12 +3203,85 @@ export default class MySQLDriver extends NymphDriver {
2289
3203
  name,
2290
3204
  reference,
2291
3205
  },
2292
- }
2293
- )
3206
+ },
3207
+ ),
2294
3208
  );
2295
3209
  }
3210
+
3211
+ let tokenString: string | null = null;
3212
+ try {
3213
+ tokenString = EntityClass.getFTSText(name, value);
3214
+ } catch (e: any) {
3215
+ // Ignore error.
3216
+ }
3217
+
3218
+ if (tokenString != null) {
3219
+ const tokens = this.tokenizer.tokenize(tokenString);
3220
+ while (tokens.length) {
3221
+ const currentTokens = tokens.splice(0, 100);
3222
+ const params: { [k: string]: any } = {
3223
+ guid,
3224
+ name,
3225
+ };
3226
+ const values: string[] = [];
3227
+
3228
+ for (let i = 0; i < currentTokens.length; i++) {
3229
+ const token = currentTokens[i];
3230
+ params['token' + i] = token.token;
3231
+ params['position' + i] = token.position;
3232
+ params['stem' + i] = token.stem;
3233
+ values.push(
3234
+ '(UNHEX(@guid), @name, @token' +
3235
+ i +
3236
+ ', @position' +
3237
+ i +
3238
+ ', @stem' +
3239
+ i +
3240
+ ')',
3241
+ );
3242
+ }
3243
+
3244
+ promises.push(
3245
+ this.queryRun(
3246
+ `INSERT INTO ${MySQLDriver.escape(
3247
+ `${this.prefix}tokens_${etype}`,
3248
+ )} (\`guid\`, \`name\`, \`token\`, \`position\`, \`stem\`) VALUES ${values.join(', ')};`,
3249
+ {
3250
+ etypes: [etype],
3251
+ params,
3252
+ },
3253
+ ),
3254
+ );
3255
+ }
3256
+ }
3257
+
2296
3258
  await Promise.all(promises);
2297
3259
  };
3260
+
3261
+ for (const unique of uniques) {
3262
+ try {
3263
+ await this.queryRun(
3264
+ `INSERT INTO ${MySQLDriver.escape(
3265
+ `${this.prefix}uniques_${etype}`,
3266
+ )} (\`guid\`, \`unique\`) VALUES (UNHEX(@guid), @unique);`,
3267
+ {
3268
+ etypes: [etype],
3269
+ params: {
3270
+ guid,
3271
+ unique,
3272
+ },
3273
+ },
3274
+ );
3275
+ } catch (e: any) {
3276
+ if (e instanceof EntityUniqueConstraintError) {
3277
+ this.nymph.config.debugError(
3278
+ 'mysql',
3279
+ `Save entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`,
3280
+ );
3281
+ }
3282
+ throw e;
3283
+ }
3284
+ }
2298
3285
  for (const name in data) {
2299
3286
  await runInsertQuery(name, data[name], JSON.stringify(data[name]));
2300
3287
  }
@@ -2302,109 +3289,185 @@ export default class MySQLDriver extends NymphDriver {
2302
3289
  await runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
2303
3290
  }
2304
3291
  };
3292
+ let inTransaction = false;
2305
3293
  try {
2306
3294
  const result = await this.saveEntityRowLike(
2307
3295
  entity,
2308
- async (_entity, guid, tags, data, sdata, cdate, etype) => {
3296
+ async ({ guid, tags, data, sdata, uniques, cdate, etype }) => {
3297
+ if (
3298
+ Object.keys(data).length === 0 &&
3299
+ Object.keys(sdata).length === 0
3300
+ ) {
3301
+ throw new EntityInvalidDataError('Entity contains no data.');
3302
+ }
3303
+ let processedData = this.removeAndReturnACValues(
3304
+ etype,
3305
+ { ...data },
3306
+ { ...sdata },
3307
+ );
3308
+ ({ data, sdata } = processedData);
3309
+ let {
3310
+ user,
3311
+ group,
3312
+ acUser,
3313
+ acGroup,
3314
+ acOther,
3315
+ acRead,
3316
+ acWrite,
3317
+ acFull,
3318
+ } = processedData;
2309
3319
  await this.queryRun(
2310
3320
  `INSERT INTO ${MySQLDriver.escape(
2311
- `${this.prefix}entities_${etype}`
2312
- )} (\`guid\`, \`tags\`, \`cdate\`, \`mdate\`) VALUES (UNHEX(@guid), @tags, @cdate, @cdate);`,
3321
+ `${this.prefix}entities_${etype}`,
3322
+ )} (\`guid\`, \`tags\`, \`cdate\`, \`mdate\`, \`user\`, \`group\`, \`acUser\`, \`acGroup\`, \`acOther\`, \`acRead\`, \`acWrite\`, \`acFull\`) VALUES (UNHEX(@guid), @tags, @cdate, @cdate, ${user == null ? '@user' : 'UNHEX(@user)'}, ${group == null ? '@group' : 'UNHEX(@group)'}, @acUser, @acGroup, @acOther, @acRead, @acWrite, @acFull);`,
2313
3323
  {
2314
3324
  etypes: [etype],
2315
3325
  params: {
2316
3326
  guid,
2317
3327
  tags: ' ' + tags.join(' ') + ' ',
2318
3328
  cdate,
3329
+ user,
3330
+ group,
3331
+ acUser,
3332
+ acGroup,
3333
+ acOther,
3334
+ acRead: acRead && ' ' + acRead.join(' ') + ' ',
3335
+ acWrite: acWrite && ' ' + acWrite.join(' ') + ' ',
3336
+ acFull: acFull && ' ' + acFull.join(' ') + ' ',
2319
3337
  },
2320
- }
3338
+ },
2321
3339
  );
2322
- await insertData(guid, data, sdata, etype);
3340
+ await insertData(guid, data, sdata, uniques, etype);
2323
3341
  return true;
2324
3342
  },
2325
- async (entity, guid, tags, data, sdata, mdate, etype) => {
3343
+ async ({ entity, guid, tags, data, sdata, uniques, mdate, etype }) => {
3344
+ if (
3345
+ Object.keys(data).length === 0 &&
3346
+ Object.keys(sdata).length === 0
3347
+ ) {
3348
+ throw new EntityInvalidDataError('Entity contains no data.');
3349
+ }
3350
+ let processedData = this.removeAndReturnACValues(
3351
+ etype,
3352
+ { ...data },
3353
+ { ...sdata },
3354
+ );
3355
+ ({ data, sdata } = processedData);
3356
+ let {
3357
+ user,
3358
+ group,
3359
+ acUser,
3360
+ acGroup,
3361
+ acOther,
3362
+ acRead,
3363
+ acWrite,
3364
+ acFull,
3365
+ } = processedData;
2326
3366
  if (this.config.rowLocking) {
2327
3367
  const promises = [];
2328
3368
  promises.push(
2329
3369
  this.queryRun(
2330
3370
  `SELECT 1 FROM ${MySQLDriver.escape(
2331
- `${this.prefix}entities_${etype}`
3371
+ `${this.prefix}entities_${etype}`,
2332
3372
  )} WHERE \`guid\`=UNHEX(@guid) GROUP BY 1 FOR UPDATE;`,
2333
3373
  {
2334
3374
  etypes: [etype],
2335
3375
  params: {
2336
3376
  guid,
2337
3377
  },
2338
- }
2339
- )
3378
+ },
3379
+ ),
2340
3380
  );
2341
3381
  promises.push(
2342
3382
  this.queryRun(
2343
3383
  `SELECT 1 FROM ${MySQLDriver.escape(
2344
- `${this.prefix}data_${etype}`
3384
+ `${this.prefix}data_${etype}`,
2345
3385
  )} WHERE \`guid\`=UNHEX(@guid) GROUP BY 1 FOR UPDATE;`,
2346
3386
  {
2347
3387
  etypes: [etype],
2348
3388
  params: {
2349
3389
  guid,
2350
3390
  },
2351
- }
2352
- )
3391
+ },
3392
+ ),
2353
3393
  );
2354
3394
  promises.push(
2355
3395
  this.queryRun(
2356
3396
  `SELECT 1 FROM ${MySQLDriver.escape(
2357
- `${this.prefix}comparisons_${etype}`
3397
+ `${this.prefix}references_${etype}`,
2358
3398
  )} WHERE \`guid\`=UNHEX(@guid) GROUP BY 1 FOR UPDATE;`,
2359
3399
  {
2360
3400
  etypes: [etype],
2361
3401
  params: {
2362
3402
  guid,
2363
3403
  },
2364
- }
2365
- )
3404
+ },
3405
+ ),
2366
3406
  );
2367
3407
  promises.push(
2368
3408
  this.queryRun(
2369
3409
  `SELECT 1 FROM ${MySQLDriver.escape(
2370
- `${this.prefix}references_${etype}`
3410
+ `${this.prefix}tokens_${etype}`,
2371
3411
  )} WHERE \`guid\`=UNHEX(@guid) GROUP BY 1 FOR UPDATE;`,
2372
3412
  {
2373
3413
  etypes: [etype],
2374
3414
  params: {
2375
3415
  guid,
2376
3416
  },
2377
- }
2378
- )
3417
+ },
3418
+ ),
3419
+ );
3420
+ promises.push(
3421
+ this.queryRun(
3422
+ `SELECT 1 FROM ${MySQLDriver.escape(
3423
+ `${this.prefix}uniques_${etype}`,
3424
+ )} WHERE \`guid\`=UNHEX(@guid) GROUP BY 1 FOR UPDATE;`,
3425
+ {
3426
+ etypes: [etype],
3427
+ params: {
3428
+ guid,
3429
+ },
3430
+ },
3431
+ ),
2379
3432
  );
2380
3433
  await Promise.all(promises);
2381
3434
  }
2382
3435
  if (this.config.tableLocking) {
2383
3436
  await this.queryRun(
2384
3437
  `LOCK TABLES ${MySQLDriver.escape(
2385
- `${this.prefix}entities_${etype}`
3438
+ `${this.prefix}entities_${etype}`,
3439
+ )} WRITE, ${MySQLDriver.escape(
3440
+ `${this.prefix}data_${etype}`,
2386
3441
  )} WRITE, ${MySQLDriver.escape(
2387
- `${this.prefix}data_${etype}`
3442
+ `${this.prefix}references_${etype}`,
2388
3443
  )} WRITE, ${MySQLDriver.escape(
2389
- `${this.prefix}comparisons_${etype}`
3444
+ `${this.prefix}tokens_${etype}`,
2390
3445
  )} WRITE, ${MySQLDriver.escape(
2391
- `${this.prefix}references_${etype}`
2392
- )} WRITE;`
3446
+ `${this.prefix}uniques_${etype}`,
3447
+ )} WRITE;`,
2393
3448
  );
2394
3449
  }
2395
3450
  const info = await this.queryRun(
2396
3451
  `UPDATE ${MySQLDriver.escape(
2397
- `${this.prefix}entities_${etype}`
2398
- )} SET \`tags\`=@tags, \`mdate\`=@mdate WHERE \`guid\`=UNHEX(@guid) AND \`mdate\` <= @emdate;`,
3452
+ `${this.prefix}entities_${etype}`,
3453
+ )} SET \`tags\`=@tags, \`mdate\`=@mdate, \`user\`=${user == null ? '@user' : 'UNHEX(@user)'}, \`group\`=${group == null ? '@group' : 'UNHEX(@group)'}, \`acUser\`=@acUser, \`acGroup\`=@acGroup, \`acOther\`=@acOther, \`acRead\`=@acRead, \`acWrite\`=@acWrite, \`acFull\`=@acFull WHERE \`guid\`=UNHEX(@guid) AND \`mdate\` <= @emdate;`,
2399
3454
  {
2400
3455
  etypes: [etype],
2401
3456
  params: {
2402
3457
  tags: ' ' + tags.join(' ') + ' ',
2403
3458
  mdate,
3459
+ user,
3460
+ group,
3461
+ acUser,
3462
+ acGroup,
3463
+ acOther,
3464
+ acRead: acRead && ' ' + acRead.join(' ') + ' ',
3465
+ acWrite: acWrite && ' ' + acWrite.join(' ') + ' ',
3466
+ acFull: acFull && ' ' + acFull.join(' ') + ' ',
2404
3467
  guid,
2405
3468
  emdate: isNaN(Number(entity.mdate)) ? 0 : Number(entity.mdate),
2406
3469
  },
2407
- }
3470
+ },
2408
3471
  );
2409
3472
  let success = false;
2410
3473
  if (info.changes === 1) {
@@ -2412,67 +3475,92 @@ export default class MySQLDriver extends NymphDriver {
2412
3475
  promises.push(
2413
3476
  this.queryRun(
2414
3477
  `DELETE FROM ${MySQLDriver.escape(
2415
- `${this.prefix}data_${etype}`
3478
+ `${this.prefix}data_${etype}`,
2416
3479
  )} WHERE \`guid\`=UNHEX(@guid);`,
2417
3480
  {
2418
3481
  etypes: [etype],
2419
3482
  params: {
2420
3483
  guid,
2421
3484
  },
2422
- }
2423
- )
3485
+ },
3486
+ ),
2424
3487
  );
2425
3488
  promises.push(
2426
3489
  this.queryRun(
2427
3490
  `DELETE FROM ${MySQLDriver.escape(
2428
- `${this.prefix}comparisons_${etype}`
3491
+ `${this.prefix}references_${etype}`,
2429
3492
  )} WHERE \`guid\`=UNHEX(@guid);`,
2430
3493
  {
2431
3494
  etypes: [etype],
2432
3495
  params: {
2433
3496
  guid,
2434
3497
  },
2435
- }
2436
- )
3498
+ },
3499
+ ),
2437
3500
  );
2438
3501
  promises.push(
2439
3502
  this.queryRun(
2440
3503
  `DELETE FROM ${MySQLDriver.escape(
2441
- `${this.prefix}references_${etype}`
3504
+ `${this.prefix}tokens_${etype}`,
2442
3505
  )} WHERE \`guid\`=UNHEX(@guid);`,
2443
3506
  {
2444
3507
  etypes: [etype],
2445
3508
  params: {
2446
3509
  guid,
2447
3510
  },
2448
- }
2449
- )
3511
+ },
3512
+ ),
3513
+ );
3514
+ promises.push(
3515
+ this.queryRun(
3516
+ `DELETE FROM ${MySQLDriver.escape(
3517
+ `${this.prefix}uniques_${etype}`,
3518
+ )} WHERE \`guid\`=UNHEX(@guid);`,
3519
+ {
3520
+ etypes: [etype],
3521
+ params: {
3522
+ guid,
3523
+ },
3524
+ },
3525
+ ),
2450
3526
  );
2451
3527
  await Promise.all(promises);
2452
- await insertData(guid, data, sdata, etype);
3528
+ await insertData(guid, data, sdata, uniques, etype);
2453
3529
  success = true;
2454
3530
  }
2455
3531
  if (this.config.tableLocking) {
2456
3532
  await this.queryRun('UNLOCK TABLES;');
2457
3533
  }
3534
+ if (!success && info.changes === 0) {
3535
+ throw new EntityConflictError(
3536
+ "Entity either doesn't exist or is newer in the DB.",
3537
+ );
3538
+ }
2458
3539
  return success;
2459
3540
  },
2460
3541
  async () => {
2461
3542
  await this.internalTransaction('nymph-save');
3543
+ inTransaction = true;
2462
3544
  },
2463
3545
  async (success) => {
2464
- if (success) {
2465
- await this.commit('nymph-save');
2466
- } else {
2467
- await this.rollback('nymph-save');
3546
+ if (inTransaction) {
3547
+ inTransaction = false;
3548
+ if (success) {
3549
+ success = await this.commit('nymph-save');
3550
+ } else {
3551
+ await this.rollback('nymph-save');
3552
+ }
2468
3553
  }
2469
3554
  return success;
2470
- }
3555
+ },
2471
3556
  );
2472
3557
 
2473
3558
  return result;
2474
3559
  } catch (e: any) {
2475
- await this.rollback('nymph-save');
3560
+ this.nymph.config.debugError('mysql', `Save entity error: "${e}"`);
3561
+ if (inTransaction) {
3562
+ await this.rollback('nymph-save');
3563
+ }
2476
3564
  throw e;
2477
3565
  }
2478
3566
  }
@@ -2483,56 +3571,209 @@ export default class MySQLDriver extends NymphDriver {
2483
3571
  }
2484
3572
  await this.queryRun(
2485
3573
  `INSERT INTO ${MySQLDriver.escape(
2486
- `${this.prefix}uids`
3574
+ `${this.prefix}uids`,
2487
3575
  )} (\`name\`, \`cur_uid\`) VALUES (@name, @curUid) ON DUPLICATE KEY UPDATE \`cur_uid\`=@curUid;`,
2488
3576
  {
2489
3577
  params: {
2490
3578
  name,
2491
3579
  curUid,
2492
3580
  },
2493
- }
3581
+ },
2494
3582
  );
2495
3583
  return true;
2496
3584
  }
2497
3585
 
2498
- private async internalTransaction(name: string) {
3586
+ protected async internalTransaction(name: string, setOnSelf = true) {
2499
3587
  if (name == null || typeof name !== 'string' || name.length === 0) {
2500
3588
  throw new InvalidParametersError(
2501
- 'Transaction start attempted without a name.'
3589
+ 'Transaction start attempted without a name.',
2502
3590
  );
2503
3591
  }
2504
3592
 
2505
- if (!this.transaction || this.transaction.count === 0) {
3593
+ let transaction = this.transaction;
3594
+
3595
+ if (!transaction) {
2506
3596
  // Lock to one connection.
2507
- this.transaction = {
3597
+ transaction = {
2508
3598
  count: 0,
2509
3599
  connection: await this.getConnection(),
3600
+ latestPromise: Promise.resolve(),
2510
3601
  };
2511
- if (this.config.transactions) {
2512
- // We're not in a transaction yet, so start one.
2513
- await this.queryRun('START TRANSACTION;');
3602
+ if (setOnSelf) {
3603
+ this.transaction = transaction;
2514
3604
  }
2515
3605
  }
2516
3606
 
2517
- if (this.config.transactions) {
2518
- await this.queryRun(`SAVEPOINT ${MySQLDriver.escape(name)};`);
3607
+ if (this.config.transactions && transaction.count === 0) {
3608
+ // We're not in a transaction yet, so start one.
3609
+ await this.queryRun('START TRANSACTION;', { transaction });
2519
3610
  }
2520
3611
 
2521
- this.transaction.count++;
3612
+ transaction.count++;
3613
+
3614
+ if (this.config.transactions) {
3615
+ await this.queryRun(`SAVEPOINT ${MySQLDriver.escape(name)};`, {
3616
+ transaction,
3617
+ });
3618
+ }
2522
3619
 
2523
- return this.transaction;
3620
+ return transaction;
2524
3621
  }
2525
3622
 
2526
3623
  public async startTransaction(name: string) {
2527
- const inTransaction = this.inTransaction();
2528
- const transaction = await this.internalTransaction(name);
2529
- if (!inTransaction) {
2530
- this.transaction = null;
2531
- }
3624
+ const transaction = await this.internalTransaction(name, false);
2532
3625
 
2533
3626
  const nymph = this.nymph.clone();
2534
3627
  (nymph.driver as MySQLDriver).transaction = transaction;
2535
3628
 
2536
3629
  return nymph;
2537
3630
  }
3631
+
3632
+ private async removeTilmeldOldRows(etype: string) {
3633
+ await this.internalTransaction('nymph-remove-tilmeld-rows');
3634
+ try {
3635
+ for (let name of [
3636
+ 'user',
3637
+ 'group',
3638
+ 'acUser',
3639
+ 'acGroup',
3640
+ 'acOther',
3641
+ 'acRead',
3642
+ 'acWrite',
3643
+ 'acFull',
3644
+ ]) {
3645
+ await this.queryRun(
3646
+ `DELETE FROM ${MySQLDriver.escape(
3647
+ `${this.prefix}data_${etype}`,
3648
+ )} WHERE \`name\`=@name;`,
3649
+ {
3650
+ etypes: [etype],
3651
+ params: {
3652
+ name,
3653
+ },
3654
+ },
3655
+ );
3656
+ await this.queryRun(
3657
+ `DELETE FROM ${MySQLDriver.escape(
3658
+ `${this.prefix}references_${etype}`,
3659
+ )} WHERE \`name\`=@name;`,
3660
+ {
3661
+ etypes: [etype],
3662
+ params: {
3663
+ name,
3664
+ },
3665
+ },
3666
+ );
3667
+ await this.queryRun(
3668
+ `DELETE FROM ${MySQLDriver.escape(
3669
+ `${this.prefix}tokens_${etype}`,
3670
+ )} WHERE \`name\`=@name;`,
3671
+ {
3672
+ etypes: [etype],
3673
+ params: {
3674
+ name,
3675
+ },
3676
+ },
3677
+ );
3678
+ }
3679
+ } catch (e: any) {
3680
+ this.nymph.config.debugError(
3681
+ 'mysql',
3682
+ `Remove tilmeld rows error: "${e}"`,
3683
+ );
3684
+ await this.rollback('nymph-remove-tilmeld-rows');
3685
+ throw e;
3686
+ }
3687
+
3688
+ await this.commit('nymph-remove-tilmeld-rows');
3689
+ return true;
3690
+ }
3691
+
3692
+ public async needsMigration(): Promise<
3693
+ 'json' | 'tokens' | 'tilmeldColumns' | false
3694
+ > {
3695
+ const table = await this.queryGet(
3696
+ 'SELECT `table_name` AS `table_name` FROM `information_schema`.`tables` WHERE `table_schema`=@db AND `table_name` LIKE @prefix LIMIT 1;',
3697
+ {
3698
+ params: {
3699
+ db: this.config.database,
3700
+ prefix: this.prefix + 'data_' + '%',
3701
+ },
3702
+ },
3703
+ );
3704
+ if (table?.table_name) {
3705
+ const result = await this.queryGet(
3706
+ "SELECT 1 AS `exists` FROM `information_schema`.`columns` WHERE `table_schema`=@db AND `table_name`=@table AND `column_name`='json';",
3707
+ {
3708
+ params: {
3709
+ db: this.config.database,
3710
+ table: table.table_name,
3711
+ },
3712
+ },
3713
+ );
3714
+ if (!result?.exists) {
3715
+ return 'json';
3716
+ }
3717
+ }
3718
+ const table2 = await this.queryGet(
3719
+ 'SELECT `table_name` AS `table_name` FROM `information_schema`.`tables` WHERE `table_schema`=@db AND `table_name` LIKE @tokenTable LIMIT 1;',
3720
+ {
3721
+ params: {
3722
+ db: this.config.database,
3723
+ tokenTable: this.prefix + 'tokens_' + '%',
3724
+ },
3725
+ },
3726
+ );
3727
+ if (!table2 || !table2.table_name) {
3728
+ return 'tokens';
3729
+ }
3730
+ const table3 = await this.queryGet(
3731
+ 'SELECT `table_name` AS `table_name` FROM `information_schema`.`tables` WHERE `table_schema`=@db AND `table_name` LIKE @prefix LIMIT 1;',
3732
+ {
3733
+ params: {
3734
+ db: this.config.database,
3735
+ prefix: this.prefix + 'entities_' + '%',
3736
+ },
3737
+ },
3738
+ );
3739
+ if (table3?.table_name) {
3740
+ const result = await this.queryGet(
3741
+ "SELECT 1 AS `exists` FROM `information_schema`.`columns` WHERE `table_schema`=@db AND `table_name`=@table AND `column_name`='user';",
3742
+ {
3743
+ params: {
3744
+ db: this.config.database,
3745
+ table: table3.table_name,
3746
+ },
3747
+ },
3748
+ );
3749
+ if (!result?.exists) {
3750
+ return 'tilmeldColumns';
3751
+ }
3752
+ }
3753
+ return false;
3754
+ }
3755
+
3756
+ public async liveMigration(
3757
+ migrationType: 'tokenTables' | 'tilmeldColumns' | 'tilmeldRemoveOldRows',
3758
+ ) {
3759
+ if (migrationType === 'tokenTables') {
3760
+ const etypes = await this.getEtypes();
3761
+
3762
+ for (let etype of etypes) {
3763
+ await this.createTokensTable(etype);
3764
+ }
3765
+ } else if (migrationType === 'tilmeldColumns') {
3766
+ const etypes = await this.getEtypes();
3767
+
3768
+ for (let etype of etypes) {
3769
+ await this.addTilmeldColumnsAndIndexes(etype);
3770
+ }
3771
+ } else if (migrationType === 'tilmeldRemoveOldRows') {
3772
+ const etypes = await this.getEtypes();
3773
+
3774
+ for (let etype of etypes) {
3775
+ await this.removeTilmeldOldRows(etype);
3776
+ }
3777
+ }
3778
+ }
2538
3779
  }