@nymphjs/driver-sqlite3 1.0.0-beta.13 → 1.0.0-beta.130

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