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

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