@nymphjs/driver-mysql 1.0.0-beta.12 → 1.0.0-beta.121

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