@nymphjs/driver-postgresql 1.0.0-alpha.2

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.
@@ -0,0 +1,2646 @@
1
+ import cp from 'child_process';
2
+ import { Pool, PoolClient, PoolConfig, QueryResult } from 'pg';
3
+ import format from 'pg-format';
4
+ import { customAlphabet } from 'nanoid';
5
+ import {
6
+ NymphDriver,
7
+ EntityConstructor,
8
+ EntityData,
9
+ EntityInterface,
10
+ SerializedEntityData,
11
+ InvalidParametersError,
12
+ NotConfiguredError,
13
+ QueryFailedError,
14
+ UnableToConnectError,
15
+ FormattedSelector,
16
+ Options,
17
+ Selector,
18
+ xor,
19
+ } from '@nymphjs/nymph';
20
+
21
+ import {
22
+ PostgreSQLDriverConfig,
23
+ PostgreSQLDriverConfigDefaults as defaults,
24
+ } from './conf';
25
+
26
+ type PostgreSQLDriverConnection = {
27
+ client: PoolClient;
28
+ done: () => void;
29
+ };
30
+
31
+ type PostgreSQLDriverTransaction = {
32
+ connection: PostgreSQLDriverConnection | null;
33
+ count: number;
34
+ };
35
+
36
+ const makeTableSuffix = customAlphabet(
37
+ '0123456789abcdefghijklmnopqrstuvwxyz',
38
+ 20
39
+ );
40
+
41
+ /**
42
+ * The PostgreSQL Nymph database driver.
43
+ */
44
+ export default class PostgreSQLDriver extends NymphDriver {
45
+ public config: PostgreSQLDriverConfig;
46
+ private postgresqlConfig: PoolConfig;
47
+ protected prefix: string;
48
+ protected connected: boolean = false;
49
+ // @ts-ignore: this is assigned in connect(), which is called by the constructor.
50
+ protected link: Pool;
51
+ protected transaction: PostgreSQLDriverTransaction | null = null;
52
+
53
+ static escape(input: string) {
54
+ return format.ident(input);
55
+ }
56
+
57
+ static escapeValue(input: string) {
58
+ return format.literal(input);
59
+ }
60
+
61
+ constructor(
62
+ config: Partial<PostgreSQLDriverConfig>,
63
+ link?: Pool,
64
+ transaction?: PostgreSQLDriverTransaction
65
+ ) {
66
+ super();
67
+ this.config = { ...defaults, ...config };
68
+ const { host, user, password, database, port, customPoolConfig } =
69
+ this.config;
70
+ this.postgresqlConfig = customPoolConfig ?? {
71
+ host,
72
+ user,
73
+ password,
74
+ database,
75
+ port,
76
+ };
77
+ this.prefix = this.config.prefix;
78
+ if (link != null) {
79
+ this.link = link;
80
+ this.connected = true;
81
+ }
82
+ if (transaction != null) {
83
+ this.transaction = transaction;
84
+ }
85
+ if (link == null) {
86
+ this.connect();
87
+ }
88
+ }
89
+
90
+ private getConnection(): Promise<PostgreSQLDriverConnection> {
91
+ if (this.transaction != null && this.transaction.connection != null) {
92
+ return Promise.resolve(this.transaction.connection);
93
+ }
94
+ return new Promise((resolve, reject) =>
95
+ this.link.connect((err, client, done) =>
96
+ err ? reject(err) : resolve({ client, done })
97
+ )
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Connect to the PostgreSQL database.
103
+ *
104
+ * @returns Whether this instance is connected to a PostgreSQL database.
105
+ */
106
+ public async connect() {
107
+ // If we think we're connected, try pinging the server.
108
+ try {
109
+ if (this.connected) {
110
+ const connection: PostgreSQLDriverConnection = await new Promise(
111
+ (resolve, reject) =>
112
+ this.link.connect((err, client, done) =>
113
+ err ? reject(err) : resolve({ client, done })
114
+ )
115
+ );
116
+ await new Promise((resolve, reject) =>
117
+ connection.client.query('SELECT 1;', [], (err, res) => {
118
+ if (err) {
119
+ reject(err);
120
+ }
121
+ resolve(0);
122
+ })
123
+ );
124
+ connection.done();
125
+ }
126
+ } catch (e: any) {
127
+ this.connected = false;
128
+ }
129
+
130
+ // Connecting, selecting database
131
+ if (!this.connected) {
132
+ try {
133
+ this.link = new Pool(this.postgresqlConfig);
134
+ this.connected = true;
135
+ } catch (e: any) {
136
+ if (
137
+ this.postgresqlConfig.host === 'localhost' &&
138
+ this.postgresqlConfig.user === 'nymph' &&
139
+ this.postgresqlConfig.password === 'password' &&
140
+ this.postgresqlConfig.database === 'nymph'
141
+ ) {
142
+ throw new NotConfiguredError(
143
+ "It seems the config hasn't been set up correctly."
144
+ );
145
+ } else {
146
+ throw new UnableToConnectError('Could not connect: ' + e?.message);
147
+ }
148
+ }
149
+ }
150
+ return this.connected;
151
+ }
152
+
153
+ /**
154
+ * Disconnect from the PostgreSQL database.
155
+ *
156
+ * @returns Whether this instance is connected to a PostgreSQL database.
157
+ */
158
+ public async disconnect() {
159
+ if (this.connected) {
160
+ await new Promise((resolve) => this.link.end(() => resolve(0)));
161
+ this.connected = false;
162
+ }
163
+ return this.connected;
164
+ }
165
+
166
+ public async inTransaction() {
167
+ return !!this.transaction;
168
+ }
169
+
170
+ /**
171
+ * Check connection status.
172
+ *
173
+ * @returns Whether this instance is connected to a PostgreSQL database.
174
+ */
175
+ public isConnected() {
176
+ return this.connected;
177
+ }
178
+
179
+ /**
180
+ * Create entity tables in the database.
181
+ *
182
+ * @param etype The entity type to create a table for. If this is blank, the default tables are created.
183
+ * @returns True on success, false on failure.
184
+ */
185
+ private createTables(etype: string | null = null) {
186
+ if (etype != null) {
187
+ // Create the entity table.
188
+ this.queryRunSync(
189
+ `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
190
+ `${this.prefix}entities_${etype}`
191
+ )} (
192
+ "guid" BYTEA NOT NULL,
193
+ "tags" TEXT[],
194
+ "cdate" DOUBLE PRECISION NOT NULL,
195
+ "mdate" DOUBLE PRECISION NOT NULL,
196
+ PRIMARY KEY ("guid")
197
+ ) WITH ( OIDS=FALSE );`
198
+ );
199
+ this.queryRunSync(
200
+ `ALTER TABLE ${PostgreSQLDriver.escape(
201
+ `${this.prefix}entities_${etype}`
202
+ )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`
203
+ );
204
+ this.queryRunSync(
205
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
206
+ `${this.prefix}entities_${etype}_id_cdate`
207
+ )};`
208
+ );
209
+ this.queryRunSync(
210
+ `CREATE INDEX ${PostgreSQLDriver.escape(
211
+ `${this.prefix}entities_${etype}_id_cdate`
212
+ )} ON ${PostgreSQLDriver.escape(
213
+ `${this.prefix}entities_${etype}`
214
+ )} USING btree ("cdate");`
215
+ );
216
+ this.queryRunSync(
217
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
218
+ `${this.prefix}entities_${etype}_id_mdate`
219
+ )};`
220
+ );
221
+ this.queryRunSync(
222
+ `CREATE INDEX ${PostgreSQLDriver.escape(
223
+ `${this.prefix}entities_${etype}_id_mdate`
224
+ )} ON ${PostgreSQLDriver.escape(
225
+ `${this.prefix}entities_${etype}`
226
+ )} USING btree ("mdate");`
227
+ );
228
+ this.queryRunSync(
229
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
230
+ `${this.prefix}entities_${etype}_id_tags`
231
+ )};`
232
+ );
233
+ this.queryRunSync(
234
+ `CREATE INDEX ${PostgreSQLDriver.escape(
235
+ `${this.prefix}entities_${etype}_id_tags`
236
+ )} ON ${PostgreSQLDriver.escape(
237
+ `${this.prefix}entities_${etype}`
238
+ )} USING gin ("tags");`
239
+ );
240
+ // Create the data table.
241
+ this.queryRunSync(
242
+ `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
243
+ `${this.prefix}data_${etype}`
244
+ )} (
245
+ "guid" BYTEA NOT NULL,
246
+ "name" TEXT NOT NULL,
247
+ "value" TEXT NOT NULL,
248
+ PRIMARY KEY ("guid", "name"),
249
+ FOREIGN KEY ("guid")
250
+ REFERENCES ${PostgreSQLDriver.escape(
251
+ `${this.prefix}entities_${etype}`
252
+ )} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
253
+ ) WITH ( OIDS=FALSE );`
254
+ );
255
+ this.queryRunSync(
256
+ `ALTER TABLE ${PostgreSQLDriver.escape(
257
+ `${this.prefix}data_${etype}`
258
+ )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`
259
+ );
260
+ this.queryRunSync(
261
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
262
+ `${this.prefix}data_${etype}_id_guid`
263
+ )};`
264
+ );
265
+ this.queryRunSync(
266
+ `CREATE INDEX ${PostgreSQLDriver.escape(
267
+ `${this.prefix}data_${etype}_id_guid`
268
+ )} ON ${PostgreSQLDriver.escape(
269
+ `${this.prefix}data_${etype}`
270
+ )} USING btree ("guid");`
271
+ );
272
+ this.queryRunSync(
273
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
274
+ `${this.prefix}data_${etype}_id_name`
275
+ )};`
276
+ );
277
+ this.queryRunSync(
278
+ `CREATE INDEX ${PostgreSQLDriver.escape(
279
+ `${this.prefix}data_${etype}_id_name`
280
+ )} ON ${PostgreSQLDriver.escape(
281
+ `${this.prefix}data_${etype}`
282
+ )} USING btree ("name");`
283
+ );
284
+ this.queryRunSync(
285
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
286
+ `${this.prefix}data_${etype}_id_guid_name__user`
287
+ )};`
288
+ );
289
+ this.queryRunSync(
290
+ `CREATE INDEX ${PostgreSQLDriver.escape(
291
+ `${this.prefix}data_${etype}_id_guid_name__user`
292
+ )} ON ${PostgreSQLDriver.escape(
293
+ `${this.prefix}data_${etype}`
294
+ )} USING btree ("guid") WHERE "name" = 'user'::text;`
295
+ );
296
+ this.queryRunSync(
297
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
298
+ `${this.prefix}data_${etype}_id_guid_name__group`
299
+ )};`
300
+ );
301
+ this.queryRunSync(
302
+ `CREATE INDEX ${PostgreSQLDriver.escape(
303
+ `${this.prefix}data_${etype}_id_guid_name__group`
304
+ )} ON ${PostgreSQLDriver.escape(
305
+ `${this.prefix}data_${etype}`
306
+ )} USING btree ("guid") WHERE "name" = 'group'::text;`
307
+ );
308
+ // Create the data comparisons table.
309
+ this.queryRunSync(
310
+ `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
311
+ `${this.prefix}comparisons_${etype}`
312
+ )} (
313
+ "guid" BYTEA NOT NULL,
314
+ "name" TEXT NOT NULL,
315
+ "truthy" BOOLEAN,
316
+ "string" TEXT,
317
+ "number" DOUBLE PRECISION,
318
+ PRIMARY KEY ("guid", "name"),
319
+ FOREIGN KEY ("guid")
320
+ REFERENCES ${PostgreSQLDriver.escape(
321
+ `${this.prefix}entities_${etype}`
322
+ )} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
323
+ ) WITH ( OIDS=FALSE );`
324
+ );
325
+ this.queryRunSync(
326
+ `ALTER TABLE ${PostgreSQLDriver.escape(
327
+ `${this.prefix}comparisons_${etype}`
328
+ )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`
329
+ );
330
+ this.queryRunSync(
331
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
332
+ `${this.prefix}comparisons_${etype}_id_guid`
333
+ )};`
334
+ );
335
+ this.queryRunSync(
336
+ `CREATE INDEX ${PostgreSQLDriver.escape(
337
+ `${this.prefix}comparisons_${etype}_id_guid`
338
+ )} ON ${PostgreSQLDriver.escape(
339
+ `${this.prefix}comparisons_${etype}`
340
+ )} USING btree ("guid");`
341
+ );
342
+ this.queryRunSync(
343
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
344
+ `${this.prefix}comparisons_${etype}_id_name`
345
+ )};`
346
+ );
347
+ this.queryRunSync(
348
+ `CREATE INDEX ${PostgreSQLDriver.escape(
349
+ `${this.prefix}comparisons_${etype}_id_name`
350
+ )} ON ${PostgreSQLDriver.escape(
351
+ `${this.prefix}comparisons_${etype}`
352
+ )} USING btree ("name");`
353
+ );
354
+ this.queryRunSync(
355
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
356
+ `${this.prefix}comparisons_${etype}_id_guid_name_truthy`
357
+ )};`
358
+ );
359
+ this.queryRunSync(
360
+ `CREATE INDEX ${PostgreSQLDriver.escape(
361
+ `${this.prefix}comparisons_${etype}_id_guid_name_truthy`
362
+ )} ON ${PostgreSQLDriver.escape(
363
+ `${this.prefix}comparisons_${etype}`
364
+ )} USING btree ("guid", "name") WHERE "truthy" = TRUE;`
365
+ );
366
+ this.queryRunSync(
367
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
368
+ `${this.prefix}comparisons_${etype}_id_guid_name_falsy`
369
+ )};`
370
+ );
371
+ this.queryRunSync(
372
+ `CREATE INDEX ${PostgreSQLDriver.escape(
373
+ `${this.prefix}comparisons_${etype}_id_guid_name_falsy`
374
+ )} ON ${PostgreSQLDriver.escape(
375
+ `${this.prefix}comparisons_${etype}`
376
+ )} USING btree ("guid", "name") WHERE "truthy" <> TRUE;`
377
+ );
378
+ // Create the references table.
379
+ this.queryRunSync(
380
+ `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
381
+ `${this.prefix}references_${etype}`
382
+ )} (
383
+ "guid" BYTEA NOT NULL,
384
+ "name" TEXT NOT NULL,
385
+ "reference" BYTEA NOT NULL,
386
+ PRIMARY KEY ("guid", "name", "reference"),
387
+ FOREIGN KEY ("guid")
388
+ REFERENCES ${PostgreSQLDriver.escape(
389
+ `${this.prefix}entities_${etype}`
390
+ )} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
391
+ ) WITH ( OIDS=FALSE );`
392
+ );
393
+ this.queryRunSync(
394
+ `ALTER TABLE ${PostgreSQLDriver.escape(
395
+ `${this.prefix}references_${etype}`
396
+ )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`
397
+ );
398
+ this.queryRunSync(
399
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
400
+ `${this.prefix}references_${etype}_id_guid`
401
+ )};`
402
+ );
403
+ this.queryRunSync(
404
+ `CREATE INDEX ${PostgreSQLDriver.escape(
405
+ `${this.prefix}references_${etype}_id_guid`
406
+ )} ON ${PostgreSQLDriver.escape(
407
+ `${this.prefix}references_${etype}`
408
+ )} USING btree ("guid");`
409
+ );
410
+ this.queryRunSync(
411
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
412
+ `${this.prefix}references_${etype}_id_name`
413
+ )};`
414
+ );
415
+ this.queryRunSync(
416
+ `CREATE INDEX ${PostgreSQLDriver.escape(
417
+ `${this.prefix}references_${etype}_id_name`
418
+ )} ON ${PostgreSQLDriver.escape(
419
+ `${this.prefix}references_${etype}`
420
+ )} USING btree ("name");`
421
+ );
422
+ this.queryRunSync(
423
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
424
+ `${this.prefix}references_${etype}_id_reference`
425
+ )};`
426
+ );
427
+ this.queryRunSync(
428
+ `CREATE INDEX ${PostgreSQLDriver.escape(
429
+ `${this.prefix}references_${etype}_id_reference`
430
+ )} ON ${PostgreSQLDriver.escape(
431
+ `${this.prefix}references_${etype}`
432
+ )} USING btree ("reference");`
433
+ );
434
+ } else {
435
+ // Create the UID table.
436
+ this.queryRunSync(
437
+ `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
438
+ `${this.prefix}uids`
439
+ )} (
440
+ "name" TEXT NOT NULL,
441
+ "cur_uid" BIGINT NOT NULL,
442
+ PRIMARY KEY ("name")
443
+ ) WITH ( OIDS = FALSE );`
444
+ );
445
+ this.queryRunSync(
446
+ `ALTER TABLE ${PostgreSQLDriver.escape(
447
+ `${this.prefix}uids`
448
+ )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`
449
+ );
450
+ }
451
+ return true;
452
+ }
453
+
454
+ private translateQuery(
455
+ origQuery: string,
456
+ origParams: { [k: string]: any }
457
+ ): { query: string; params: any[] } {
458
+ const params: any[] = [];
459
+ let query = origQuery;
460
+
461
+ let paramRegex = /@[a-zA-Z0-9]+/;
462
+ let match;
463
+ let i = 1;
464
+ while ((match = query.match(paramRegex))) {
465
+ const param = match[0].substr(1);
466
+ params.push(origParams[param]);
467
+ query = query.replace(paramRegex, () => '$' + i++);
468
+ }
469
+
470
+ return { query, params };
471
+ }
472
+
473
+ private async query<T extends () => any>(
474
+ runQuery: T,
475
+ query: string,
476
+ etypes: string[] = []
477
+ // @ts-ignore: The return type of T is a promise.
478
+ ): ReturnType<T> {
479
+ try {
480
+ return await runQuery();
481
+ } catch (e: any) {
482
+ const errorCode = e?.code;
483
+ if (errorCode === '42P01' && this.createTables()) {
484
+ // If the tables don't exist yet, create them.
485
+ for (let etype of etypes) {
486
+ this.createTables(etype);
487
+ }
488
+ try {
489
+ return await runQuery();
490
+ } catch (e2: any) {
491
+ throw new QueryFailedError(
492
+ 'Query failed: ' + e2?.code + ' - ' + e2?.message,
493
+ query
494
+ );
495
+ }
496
+ } else {
497
+ throw e;
498
+ }
499
+ }
500
+ }
501
+
502
+ private querySync<T extends () => any>(
503
+ runQuery: T,
504
+ query: string,
505
+ etypes: string[] = []
506
+ ): ReturnType<T> {
507
+ try {
508
+ return runQuery();
509
+ } catch (e: any) {
510
+ const errorCode = e?.code;
511
+ if (errorCode === '42P01' && this.createTables()) {
512
+ // If the tables don't exist yet, create them.
513
+ for (let etype of etypes) {
514
+ this.createTables(etype);
515
+ }
516
+ try {
517
+ return runQuery();
518
+ } catch (e2: any) {
519
+ throw new QueryFailedError(
520
+ 'Query failed: ' + e2?.code + ' - ' + e2?.message,
521
+ query
522
+ );
523
+ }
524
+ } else {
525
+ throw e;
526
+ }
527
+ }
528
+ }
529
+
530
+ private queryIter(
531
+ query: string,
532
+ {
533
+ etypes = [],
534
+ params = {},
535
+ }: {
536
+ etypes?: string[];
537
+ params?: { [k: string]: any };
538
+ } = {}
539
+ ) {
540
+ const { query: newQuery, params: newParams } = this.translateQuery(
541
+ query,
542
+ params
543
+ );
544
+ return this.query(
545
+ async () => {
546
+ const results: QueryResult<any> = await new Promise(
547
+ (resolve, reject) => {
548
+ try {
549
+ (this.transaction?.connection?.client ?? this.link)
550
+ .query(newQuery, newParams)
551
+ .then(
552
+ (results) => resolve(results),
553
+ (error) => reject(error)
554
+ );
555
+ } catch (e) {
556
+ reject(e);
557
+ }
558
+ }
559
+ );
560
+ return results.rows;
561
+ },
562
+ `${query} -- ${JSON.stringify(params)}`,
563
+ etypes
564
+ );
565
+ }
566
+
567
+ private queryIterSync(
568
+ query: string,
569
+ {
570
+ etypes = [],
571
+ params = {},
572
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {}
573
+ ) {
574
+ const { query: newQuery, params: newParams } = this.translateQuery(
575
+ query,
576
+ params
577
+ );
578
+ return this.querySync(
579
+ () => {
580
+ const output = cp.spawnSync(
581
+ process.argv0,
582
+ [__dirname + '/runPostgresqlSync.js'],
583
+ {
584
+ input: JSON.stringify({
585
+ postgresqlConfig: this.postgresqlConfig,
586
+ query: newQuery,
587
+ params: newParams,
588
+ }),
589
+ timeout: 30000,
590
+ maxBuffer: 100 * 1024 * 1024,
591
+ encoding: 'utf8',
592
+ windowsHide: true,
593
+ }
594
+ );
595
+ try {
596
+ return JSON.parse(output.stdout).rows;
597
+ } catch (e) {
598
+ // Do nothing.
599
+ }
600
+ if (output.status === 0) {
601
+ throw new Error('Unknown parse error.');
602
+ }
603
+ const err = JSON.parse(output.stderr);
604
+ const e = new Error(err.name);
605
+ for (const name in err) {
606
+ (e as any)[name] = err[name];
607
+ }
608
+ },
609
+ `${query} -- ${JSON.stringify(params)}`,
610
+ etypes
611
+ );
612
+ }
613
+
614
+ private queryGet(
615
+ query: string,
616
+ {
617
+ etypes = [],
618
+ params = {},
619
+ }: {
620
+ etypes?: string[];
621
+ params?: { [k: string]: any };
622
+ } = {}
623
+ ) {
624
+ const { query: newQuery, params: newParams } = this.translateQuery(
625
+ query,
626
+ params
627
+ );
628
+ return this.query(
629
+ async () => {
630
+ const results: QueryResult<any> = await new Promise(
631
+ (resolve, reject) => {
632
+ try {
633
+ (this.transaction?.connection?.client ?? this.link)
634
+ .query(newQuery, newParams)
635
+ .then(
636
+ (results) => resolve(results),
637
+ (error) => reject(error)
638
+ );
639
+ } catch (e) {
640
+ reject(e);
641
+ }
642
+ }
643
+ );
644
+ return results.rows[0];
645
+ },
646
+ `${query} -- ${JSON.stringify(params)}`,
647
+ etypes
648
+ );
649
+ }
650
+
651
+ private queryRun(
652
+ query: string,
653
+ {
654
+ etypes = [],
655
+ params = {},
656
+ }: {
657
+ etypes?: string[];
658
+ params?: { [k: string]: any };
659
+ } = {}
660
+ ) {
661
+ const { query: newQuery, params: newParams } = this.translateQuery(
662
+ query,
663
+ params
664
+ );
665
+ return this.query(
666
+ async () => {
667
+ const results: QueryResult<any> = await new Promise(
668
+ (resolve, reject) => {
669
+ try {
670
+ (this.transaction?.connection?.client ?? this.link)
671
+ .query(newQuery, newParams)
672
+ .then(
673
+ (results) => resolve(results),
674
+ (error) => reject(error)
675
+ );
676
+ } catch (e) {
677
+ reject(e);
678
+ }
679
+ }
680
+ );
681
+ return { rowCount: results.rowCount ?? 0 };
682
+ },
683
+ `${query} -- ${JSON.stringify(params)}`,
684
+ etypes
685
+ );
686
+ }
687
+
688
+ private queryRunSync(
689
+ query: string,
690
+ {
691
+ etypes = [],
692
+ params = {},
693
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {}
694
+ ) {
695
+ const { query: newQuery, params: newParams } = this.translateQuery(
696
+ query,
697
+ params
698
+ );
699
+ return this.querySync(
700
+ () => {
701
+ const output = cp.spawnSync(
702
+ process.argv0,
703
+ [__dirname + '/runPostgresqlSync.js'],
704
+ {
705
+ input: JSON.stringify({
706
+ postgresqlConfig: this.postgresqlConfig,
707
+ query: newQuery,
708
+ params: newParams,
709
+ }),
710
+ timeout: 30000,
711
+ maxBuffer: 100 * 1024 * 1024,
712
+ encoding: 'utf8',
713
+ windowsHide: true,
714
+ }
715
+ );
716
+ try {
717
+ const results = JSON.parse(output.stdout);
718
+ return { rowCount: results.rowCount ?? 0 };
719
+ } catch (e) {
720
+ // Do nothing.
721
+ }
722
+ if (output.status === 0) {
723
+ throw new Error('Unknown parse error.');
724
+ }
725
+ const err = JSON.parse(output.stderr);
726
+ const e = new Error(err.name);
727
+ for (const name in err) {
728
+ (e as any)[name] = err[name];
729
+ }
730
+ },
731
+ `${query} -- ${JSON.stringify(params)}`,
732
+ etypes
733
+ );
734
+ }
735
+
736
+ public async commit(name: string) {
737
+ if (name == null || typeof name !== 'string' || name.length === 0) {
738
+ throw new InvalidParametersError(
739
+ 'Transaction commit attempted without a name.'
740
+ );
741
+ }
742
+ if (!this.transaction || this.transaction.count === 0) {
743
+ this.transaction = null;
744
+ return true;
745
+ }
746
+ await this.queryRun(`RELEASE SAVEPOINT ${PostgreSQLDriver.escape(name)};`);
747
+ this.transaction.count--;
748
+ if (this.transaction.count === 0) {
749
+ await this.queryRun('COMMIT;');
750
+ this.transaction.connection?.done();
751
+ this.transaction.connection = null;
752
+ this.transaction = null;
753
+ }
754
+ return true;
755
+ }
756
+
757
+ public async deleteEntityByID(
758
+ guid: string,
759
+ className?: EntityConstructor | string | null
760
+ ) {
761
+ let EntityClass: EntityConstructor;
762
+ if (typeof className === 'string' || className == null) {
763
+ const GetEntityClass = this.nymph.getEntityClass(className ?? 'Entity');
764
+ EntityClass = GetEntityClass;
765
+ } else {
766
+ EntityClass = className;
767
+ }
768
+ const etype = EntityClass.ETYPE;
769
+ await this.internalTransaction('nymph-delete');
770
+ try {
771
+ await this.queryRun(
772
+ `DELETE FROM ${PostgreSQLDriver.escape(
773
+ `${this.prefix}entities_${etype}`
774
+ )} WHERE "guid"=decode(@guid, 'hex');`,
775
+ {
776
+ etypes: [etype],
777
+ params: {
778
+ guid,
779
+ },
780
+ }
781
+ );
782
+ await this.queryRun(
783
+ `DELETE FROM ${PostgreSQLDriver.escape(
784
+ `${this.prefix}data_${etype}`
785
+ )} WHERE "guid"=decode(@guid, 'hex');`,
786
+ {
787
+ etypes: [etype],
788
+ params: {
789
+ guid,
790
+ },
791
+ }
792
+ );
793
+ await this.queryRun(
794
+ `DELETE FROM ${PostgreSQLDriver.escape(
795
+ `${this.prefix}comparisons_${etype}`
796
+ )} WHERE "guid"=decode(@guid, 'hex');`,
797
+ {
798
+ etypes: [etype],
799
+ params: {
800
+ guid,
801
+ },
802
+ }
803
+ );
804
+ await this.queryRun(
805
+ `DELETE FROM ${PostgreSQLDriver.escape(
806
+ `${this.prefix}references_${etype}`
807
+ )} WHERE "guid"=decode(@guid, 'hex');`,
808
+ {
809
+ etypes: [etype],
810
+ params: {
811
+ guid,
812
+ },
813
+ }
814
+ );
815
+ await this.commit('nymph-delete');
816
+ // Remove any cached versions of this entity.
817
+ if (this.nymph.config.cache) {
818
+ this.cleanCache(guid);
819
+ }
820
+ return true;
821
+ } catch (e: any) {
822
+ await this.rollback('nymph-delete');
823
+ throw e;
824
+ }
825
+ }
826
+
827
+ public async deleteUID(name: string) {
828
+ if (!name) {
829
+ throw new InvalidParametersError('Name not given for UID');
830
+ }
831
+ await this.queryRun(
832
+ `DELETE FROM ${PostgreSQLDriver.escape(
833
+ `${this.prefix}uids`
834
+ )} WHERE "name"=@name;`,
835
+ {
836
+ params: {
837
+ name,
838
+ },
839
+ }
840
+ );
841
+ return true;
842
+ }
843
+
844
+ protected async exportEntities(writeLine: (line: string) => void) {
845
+ writeLine('#nex2');
846
+ writeLine('# Nymph Entity Exchange v2');
847
+ writeLine('# http://nymph.io');
848
+ writeLine('#');
849
+ writeLine('# Generation Time: ' + new Date().toLocaleString());
850
+ writeLine('');
851
+
852
+ writeLine('#');
853
+ writeLine('# UIDs');
854
+ writeLine('#');
855
+ writeLine('');
856
+
857
+ // Export UIDs.
858
+ let uids = await this.queryIter(
859
+ `SELECT * FROM ${PostgreSQLDriver.escape(
860
+ `${this.prefix}uids`
861
+ )} ORDER BY "name";`
862
+ );
863
+ for (const uid of uids) {
864
+ writeLine(`<${uid.name}>[${uid.cur_uid}]`);
865
+ }
866
+
867
+ writeLine('');
868
+ writeLine('#');
869
+ writeLine('# Entities');
870
+ writeLine('#');
871
+ writeLine('');
872
+
873
+ // Get the etypes.
874
+ const tables = await this.queryIter(
875
+ 'SELECT relname FROM pg_stat_user_tables ORDER BY relname;'
876
+ );
877
+ const etypes = [];
878
+ for (const tableRow of tables) {
879
+ const table = tableRow.relname;
880
+ if (table.startsWith(this.prefix + 'entities_')) {
881
+ etypes.push(table.substr((this.prefix + 'entities_').length));
882
+ }
883
+ }
884
+
885
+ for (const etype of etypes) {
886
+ // Export entities.
887
+ const dataIterator = (
888
+ await this.queryIter(
889
+ `SELECT encode(e."guid", 'hex') as "guid", e."tags", e."cdate", e."mdate", d."name" AS "dname", d."value" AS "dvalue", c."string", c."number"
890
+ FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} e
891
+ LEFT JOIN ${PostgreSQLDriver.escape(
892
+ `${this.prefix}data_${etype}`
893
+ )} d ON e."guid"=d."guid"
894
+ INNER JOIN ${PostgreSQLDriver.escape(
895
+ `${this.prefix}comparisons_${etype}`
896
+ )} c ON d."guid"=c."guid" AND d."name"=c."name"
897
+ ORDER BY e."guid";`
898
+ )
899
+ )[Symbol.iterator]();
900
+ let datum = dataIterator.next();
901
+ while (!datum.done) {
902
+ const guid = datum.value.guid;
903
+ const tags = datum.value.tags.join(',');
904
+ const cdate = datum.value.cdate;
905
+ const mdate = datum.value.mdate;
906
+ writeLine(`{${guid}}<${etype}>[${tags}]`);
907
+ writeLine(`\tcdate=${JSON.stringify(cdate)}`);
908
+ writeLine(`\tmdate=${JSON.stringify(mdate)}`);
909
+ if (datum.value.dname != null) {
910
+ // This do will keep going and adding the data until the
911
+ // next entity is reached. $row will end on the next entity.
912
+ do {
913
+ const value =
914
+ datum.value.dvalue === 'N'
915
+ ? JSON.stringify(Number(datum.value.number))
916
+ : datum.value.dvalue === 'S'
917
+ ? JSON.stringify(datum.value.string)
918
+ : datum.value.dvalue;
919
+ writeLine(`\t${datum.value.dname}=${value}`);
920
+ datum = dataIterator.next();
921
+ } while (!datum.done && datum.value.guid === guid);
922
+ } else {
923
+ // Make sure that datum is incremented :)
924
+ datum = dataIterator.next();
925
+ }
926
+ }
927
+ }
928
+
929
+ return;
930
+ }
931
+
932
+ /**
933
+ * Generate the PostgreSQL query.
934
+ * @param options The options array.
935
+ * @param formattedSelectors The formatted selector array.
936
+ * @param etype
937
+ * @param count Used to track internal params.
938
+ * @param params Used to store internal params.
939
+ * @param subquery Whether only a subquery should be returned.
940
+ * @returns The SQL query.
941
+ */
942
+ private makeEntityQuery(
943
+ options: Options,
944
+ formattedSelectors: FormattedSelector[],
945
+ etype: string,
946
+ count = { i: 0 },
947
+ params: { [k: string]: any } = {},
948
+ subquery = false,
949
+ tableSuffix = '',
950
+ etypes: string[] = []
951
+ ) {
952
+ if (typeof options.class?.alterOptions === 'function') {
953
+ options = options.class.alterOptions(options);
954
+ }
955
+ const eTable = `e${tableSuffix}`;
956
+ const dTable = `d${tableSuffix}`;
957
+ const cTable = `c${tableSuffix}`;
958
+ const fTable = `f${tableSuffix}`;
959
+ const ieTable = `ie${tableSuffix}`;
960
+ const sort = options.sort ?? 'cdate';
961
+ const queryParts = this.iterateSelectorsForQuery(
962
+ formattedSelectors,
963
+ (key, value, typeIsOr, typeIsNot) => {
964
+ const clauseNot = key.startsWith('!');
965
+ let curQuery = '';
966
+ for (const curValue of value) {
967
+ switch (key) {
968
+ case 'guid':
969
+ case '!guid':
970
+ for (const curGuid of curValue) {
971
+ if (curQuery) {
972
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
973
+ }
974
+ const guid = `param${++count.i}`;
975
+ curQuery +=
976
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
977
+ ieTable +
978
+ '."guid"=decode(@' +
979
+ guid +
980
+ ", 'hex')";
981
+ params[guid] = curGuid;
982
+ }
983
+ break;
984
+ case 'tag':
985
+ case '!tag':
986
+ for (const curTag of curValue) {
987
+ if (curQuery) {
988
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
989
+ }
990
+ const tag = `param${++count.i}`;
991
+ curQuery +=
992
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
993
+ '@' +
994
+ tag +
995
+ ' <@ ie."tags"';
996
+ params[tag] = [curTag];
997
+ }
998
+ break;
999
+ case 'defined':
1000
+ case '!defined':
1001
+ for (const curVar of curValue) {
1002
+ if (curQuery) {
1003
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1004
+ }
1005
+ const name = `param${++count.i}`;
1006
+ curQuery +=
1007
+ ieTable +
1008
+ '."guid" ' +
1009
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1010
+ 'IN (SELECT "guid" FROM ' +
1011
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1012
+ ' WHERE "name"=@' +
1013
+ name +
1014
+ ')';
1015
+ params[name] = curVar;
1016
+ }
1017
+ break;
1018
+ case 'truthy':
1019
+ case '!truthy':
1020
+ for (const curVar of curValue) {
1021
+ if (curQuery) {
1022
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1023
+ }
1024
+ if (curVar === 'cdate') {
1025
+ curQuery +=
1026
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1027
+ '(' +
1028
+ ieTable +
1029
+ '."cdate" NOT NULL)';
1030
+ break;
1031
+ } else if (curVar === 'mdate') {
1032
+ curQuery +=
1033
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1034
+ '(' +
1035
+ ieTable +
1036
+ '."mdate" NOT NULL)';
1037
+ break;
1038
+ } else {
1039
+ const name = `param${++count.i}`;
1040
+ curQuery +=
1041
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1042
+ ieTable +
1043
+ '."guid" IN (SELECT "guid" FROM ' +
1044
+ PostgreSQLDriver.escape(
1045
+ this.prefix + 'comparisons_' + etype
1046
+ ) +
1047
+ ' WHERE "name"=@' +
1048
+ name +
1049
+ ' AND "truthy"=TRUE)';
1050
+ params[name] = curVar;
1051
+ }
1052
+ }
1053
+ break;
1054
+ case 'equal':
1055
+ case '!equal':
1056
+ if (curValue[0] === 'cdate') {
1057
+ if (curQuery) {
1058
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1059
+ }
1060
+ const cdate = `param${++count.i}`;
1061
+ curQuery +=
1062
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1063
+ ieTable +
1064
+ '."cdate"=@' +
1065
+ cdate;
1066
+ params[cdate] = isNaN(Number(curValue[1]))
1067
+ ? null
1068
+ : Number(curValue[1]);
1069
+ break;
1070
+ } else if (curValue[0] === 'mdate') {
1071
+ if (curQuery) {
1072
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1073
+ }
1074
+ const mdate = `param${++count.i}`;
1075
+ curQuery +=
1076
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1077
+ ieTable +
1078
+ '."mdate"=@' +
1079
+ mdate;
1080
+ params[mdate] = isNaN(Number(curValue[1]))
1081
+ ? null
1082
+ : Number(curValue[1]);
1083
+ break;
1084
+ } else if (typeof curValue[1] === 'number') {
1085
+ if (curQuery) {
1086
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1087
+ }
1088
+ const name = `param${++count.i}`;
1089
+ const value = `param${++count.i}`;
1090
+ curQuery +=
1091
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1092
+ ieTable +
1093
+ '."guid" IN (SELECT "guid" FROM ' +
1094
+ PostgreSQLDriver.escape(
1095
+ this.prefix + 'comparisons_' + etype
1096
+ ) +
1097
+ ' WHERE "name"=@' +
1098
+ name +
1099
+ ' AND "number"=@' +
1100
+ value +
1101
+ ')';
1102
+ params[name] = curValue[0];
1103
+ params[value] = curValue[1];
1104
+ } else if (typeof curValue[1] === 'string') {
1105
+ if (curQuery) {
1106
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1107
+ }
1108
+ const name = `param${++count.i}`;
1109
+ const value = `param${++count.i}`;
1110
+ curQuery +=
1111
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1112
+ ieTable +
1113
+ '."guid" IN (SELECT "guid" FROM ' +
1114
+ PostgreSQLDriver.escape(
1115
+ this.prefix + 'comparisons_' + etype
1116
+ ) +
1117
+ ' WHERE "name"=@' +
1118
+ name +
1119
+ ' AND "string"=@' +
1120
+ value +
1121
+ ')';
1122
+ params[name] = curValue[0];
1123
+ params[value] = curValue[1];
1124
+ } else {
1125
+ if (curQuery) {
1126
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1127
+ }
1128
+ let svalue: string;
1129
+ if (
1130
+ curValue[1] instanceof Object &&
1131
+ typeof curValue[1].toReference === 'function'
1132
+ ) {
1133
+ svalue = JSON.stringify(curValue[1].toReference());
1134
+ } else {
1135
+ svalue = JSON.stringify(curValue[1]);
1136
+ }
1137
+ const name = `param${++count.i}`;
1138
+ const value = `param${++count.i}`;
1139
+ curQuery +=
1140
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1141
+ ieTable +
1142
+ '."guid" IN (SELECT "guid" FROM ' +
1143
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1144
+ ' WHERE "name"=@' +
1145
+ name +
1146
+ ' AND "value"=@' +
1147
+ value +
1148
+ ')';
1149
+ params[name] = curValue[0];
1150
+ params[value] = svalue;
1151
+ }
1152
+ break;
1153
+ case 'contain':
1154
+ case '!contain':
1155
+ if (curValue[0] === 'cdate') {
1156
+ if (curQuery) {
1157
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1158
+ }
1159
+ const cdate = `param${++count.i}`;
1160
+ curQuery +=
1161
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1162
+ ieTable +
1163
+ '."cdate"=' +
1164
+ cdate;
1165
+ params[cdate] = isNaN(Number(curValue[1]))
1166
+ ? null
1167
+ : Number(curValue[1]);
1168
+ break;
1169
+ } else if (curValue[0] === 'mdate') {
1170
+ if (curQuery) {
1171
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1172
+ }
1173
+ const mdate = `param${++count.i}`;
1174
+ curQuery +=
1175
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1176
+ ieTable +
1177
+ '."mdate"=' +
1178
+ mdate;
1179
+ params[mdate] = isNaN(Number(curValue[1]))
1180
+ ? null
1181
+ : Number(curValue[1]);
1182
+ break;
1183
+ } else {
1184
+ if (curQuery) {
1185
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1186
+ }
1187
+ let svalue: string;
1188
+ let stringValue: string;
1189
+ if (
1190
+ curValue[1] instanceof Object &&
1191
+ typeof curValue[1].toReference === 'function'
1192
+ ) {
1193
+ svalue = JSON.stringify(curValue[1].toReference());
1194
+ stringValue = `${curValue[1].toReference()}`;
1195
+ } else {
1196
+ svalue = JSON.stringify(curValue[1]);
1197
+ stringValue = `${curValue[1]}`;
1198
+ }
1199
+ const name = `param${++count.i}`;
1200
+ const value = `param${++count.i}`;
1201
+ if (typeof curValue[1] === 'string') {
1202
+ const stringParam = `param${++count.i}`;
1203
+ curQuery +=
1204
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1205
+ '(' +
1206
+ ieTable +
1207
+ '."guid" IN (SELECT "guid" FROM ' +
1208
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1209
+ ' WHERE "name"=@' +
1210
+ name +
1211
+ ' AND position(@' +
1212
+ value +
1213
+ ' IN "value")>0) OR ' +
1214
+ ieTable +
1215
+ '."guid" IN (SELECT "guid" FROM ' +
1216
+ PostgreSQLDriver.escape(
1217
+ this.prefix + 'comparisons_' + etype
1218
+ ) +
1219
+ ' WHERE "name"=@' +
1220
+ name +
1221
+ ' AND "string"=@' +
1222
+ stringParam +
1223
+ '))';
1224
+ params[stringParam] = stringValue;
1225
+ } else {
1226
+ curQuery +=
1227
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1228
+ ieTable +
1229
+ '."guid" IN (SELECT "guid" FROM ' +
1230
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1231
+ ' WHERE "name"=@' +
1232
+ name +
1233
+ ' AND position(@' +
1234
+ value +
1235
+ ' IN "value")>0)';
1236
+ }
1237
+ params[name] = curValue[0];
1238
+ params[value] = svalue;
1239
+ }
1240
+ break;
1241
+ case 'match':
1242
+ case '!match':
1243
+ if (curValue[0] === 'cdate') {
1244
+ if (curQuery) {
1245
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1246
+ }
1247
+ const cdate = `param${++count.i}`;
1248
+ curQuery +=
1249
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1250
+ '(' +
1251
+ ieTable +
1252
+ '."cdate" ~ @' +
1253
+ cdate +
1254
+ ')';
1255
+ params[cdate] = curValue[1];
1256
+ break;
1257
+ } else if (curValue[0] === 'mdate') {
1258
+ if (curQuery) {
1259
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1260
+ }
1261
+ const mdate = `param${++count.i}`;
1262
+ curQuery +=
1263
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1264
+ '(' +
1265
+ ieTable +
1266
+ '."mdate" ~ @' +
1267
+ mdate +
1268
+ ')';
1269
+ params[mdate] = curValue[1];
1270
+ break;
1271
+ } else {
1272
+ if (curQuery) {
1273
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1274
+ }
1275
+ const name = `param${++count.i}`;
1276
+ const value = `param${++count.i}`;
1277
+ curQuery +=
1278
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1279
+ ieTable +
1280
+ '."guid" IN (SELECT "guid" FROM ' +
1281
+ PostgreSQLDriver.escape(
1282
+ this.prefix + 'comparisons_' + etype
1283
+ ) +
1284
+ ' WHERE "name"=@' +
1285
+ name +
1286
+ ' AND "string" ~ @' +
1287
+ value +
1288
+ ')';
1289
+ params[name] = curValue[0];
1290
+ params[value] = curValue[1];
1291
+ }
1292
+ break;
1293
+ case 'imatch':
1294
+ case '!imatch':
1295
+ if (curValue[0] === 'cdate') {
1296
+ if (curQuery) {
1297
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1298
+ }
1299
+ const cdate = `param${++count.i}`;
1300
+ curQuery +=
1301
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1302
+ '(' +
1303
+ ieTable +
1304
+ '."cdate" ~* @' +
1305
+ cdate +
1306
+ ')';
1307
+ params[cdate] = curValue[1];
1308
+ break;
1309
+ } else if (curValue[0] === 'mdate') {
1310
+ if (curQuery) {
1311
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1312
+ }
1313
+ const mdate = `param${++count.i}`;
1314
+ curQuery +=
1315
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1316
+ '(' +
1317
+ ieTable +
1318
+ '."mdate" ~* @' +
1319
+ mdate +
1320
+ ')';
1321
+ params[mdate] = curValue[1];
1322
+ break;
1323
+ } else {
1324
+ if (curQuery) {
1325
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1326
+ }
1327
+ const name = `param${++count.i}`;
1328
+ const value = `param${++count.i}`;
1329
+ curQuery +=
1330
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1331
+ ieTable +
1332
+ '."guid" IN (SELECT "guid" FROM ' +
1333
+ PostgreSQLDriver.escape(
1334
+ this.prefix + 'comparisons_' + etype
1335
+ ) +
1336
+ ' WHERE "name"=@' +
1337
+ name +
1338
+ ' AND "string" ~* @' +
1339
+ value +
1340
+ ')';
1341
+ params[name] = curValue[0];
1342
+ params[value] = curValue[1];
1343
+ }
1344
+ break;
1345
+ case 'like':
1346
+ case '!like':
1347
+ if (curValue[0] === 'cdate') {
1348
+ if (curQuery) {
1349
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1350
+ }
1351
+ const cdate = `param${++count.i}`;
1352
+ curQuery +=
1353
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1354
+ '(' +
1355
+ ieTable +
1356
+ '."cdate" LIKE @' +
1357
+ cdate +
1358
+ ')';
1359
+ params[cdate] = curValue[1];
1360
+ break;
1361
+ } else if (curValue[0] === 'mdate') {
1362
+ if (curQuery) {
1363
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1364
+ }
1365
+ const mdate = `param${++count.i}`;
1366
+ curQuery +=
1367
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1368
+ '(' +
1369
+ ieTable +
1370
+ '."mdate" LIKE @' +
1371
+ mdate +
1372
+ ')';
1373
+ params[mdate] = curValue[1];
1374
+ break;
1375
+ } else {
1376
+ if (curQuery) {
1377
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1378
+ }
1379
+ const name = `param${++count.i}`;
1380
+ const value = `param${++count.i}`;
1381
+ curQuery +=
1382
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1383
+ ieTable +
1384
+ '."guid" IN (SELECT "guid" FROM ' +
1385
+ PostgreSQLDriver.escape(
1386
+ this.prefix + 'comparisons_' + etype
1387
+ ) +
1388
+ ' WHERE "name"=@' +
1389
+ name +
1390
+ ' AND "string" LIKE @' +
1391
+ value +
1392
+ ')';
1393
+ params[name] = curValue[0];
1394
+ params[value] = curValue[1];
1395
+ }
1396
+ break;
1397
+ case 'ilike':
1398
+ case '!ilike':
1399
+ if (curValue[0] === 'cdate') {
1400
+ if (curQuery) {
1401
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1402
+ }
1403
+ const cdate = `param${++count.i}`;
1404
+ curQuery +=
1405
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1406
+ '(' +
1407
+ ieTable +
1408
+ '."cdate" ILIKE @' +
1409
+ cdate +
1410
+ ')';
1411
+ params[cdate] = curValue[1];
1412
+ break;
1413
+ } else if (curValue[0] === 'mdate') {
1414
+ if (curQuery) {
1415
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1416
+ }
1417
+ const mdate = `param${++count.i}`;
1418
+ curQuery +=
1419
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1420
+ '(' +
1421
+ ieTable +
1422
+ '."mdate" ILIKE @' +
1423
+ mdate +
1424
+ ')';
1425
+ params[mdate] = curValue[1];
1426
+ break;
1427
+ } else {
1428
+ if (curQuery) {
1429
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1430
+ }
1431
+ const name = `param${++count.i}`;
1432
+ const value = `param${++count.i}`;
1433
+ curQuery +=
1434
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1435
+ ieTable +
1436
+ '."guid" IN (SELECT "guid" FROM ' +
1437
+ PostgreSQLDriver.escape(
1438
+ this.prefix + 'comparisons_' + etype
1439
+ ) +
1440
+ ' WHERE "name"=@' +
1441
+ name +
1442
+ ' AND "string" ILIKE @' +
1443
+ value +
1444
+ ')';
1445
+ params[name] = curValue[0];
1446
+ params[value] = curValue[1];
1447
+ }
1448
+ break;
1449
+ case 'gt':
1450
+ case '!gt':
1451
+ if (curValue[0] === 'cdate') {
1452
+ if (curQuery) {
1453
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1454
+ }
1455
+ const cdate = `param${++count.i}`;
1456
+ curQuery +=
1457
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1458
+ ieTable +
1459
+ '."cdate">@' +
1460
+ cdate;
1461
+ params[cdate] = isNaN(Number(curValue[1]))
1462
+ ? null
1463
+ : Number(curValue[1]);
1464
+ break;
1465
+ } else if (curValue[0] === 'mdate') {
1466
+ if (curQuery) {
1467
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1468
+ }
1469
+ const mdate = `param${++count.i}`;
1470
+ curQuery +=
1471
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1472
+ ieTable +
1473
+ '."mdate">@' +
1474
+ mdate;
1475
+ params[mdate] = isNaN(Number(curValue[1]))
1476
+ ? null
1477
+ : Number(curValue[1]);
1478
+ break;
1479
+ } else {
1480
+ if (curQuery) {
1481
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1482
+ }
1483
+ const name = `param${++count.i}`;
1484
+ const value = `param${++count.i}`;
1485
+ curQuery +=
1486
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1487
+ ieTable +
1488
+ '."guid" IN (SELECT "guid" FROM ' +
1489
+ PostgreSQLDriver.escape(
1490
+ this.prefix + 'comparisons_' + etype
1491
+ ) +
1492
+ ' WHERE "name"=@' +
1493
+ name +
1494
+ ' AND "number">@' +
1495
+ value +
1496
+ ')';
1497
+ params[name] = curValue[0];
1498
+ params[value] = isNaN(Number(curValue[1]))
1499
+ ? null
1500
+ : Number(curValue[1]);
1501
+ }
1502
+ break;
1503
+ case 'gte':
1504
+ case '!gte':
1505
+ if (curValue[0] === 'cdate') {
1506
+ if (curQuery) {
1507
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1508
+ }
1509
+ const cdate = `param${++count.i}`;
1510
+ curQuery +=
1511
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1512
+ ieTable +
1513
+ '."cdate">=@' +
1514
+ cdate;
1515
+ params[cdate] = isNaN(Number(curValue[1]))
1516
+ ? null
1517
+ : Number(curValue[1]);
1518
+ break;
1519
+ } else if (curValue[0] === 'mdate') {
1520
+ if (curQuery) {
1521
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1522
+ }
1523
+ const mdate = `param${++count.i}`;
1524
+ curQuery +=
1525
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1526
+ ieTable +
1527
+ '."mdate">=@' +
1528
+ mdate;
1529
+ params[mdate] = isNaN(Number(curValue[1]))
1530
+ ? null
1531
+ : Number(curValue[1]);
1532
+ break;
1533
+ } else {
1534
+ if (curQuery) {
1535
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1536
+ }
1537
+ const name = `param${++count.i}`;
1538
+ const value = `param${++count.i}`;
1539
+ curQuery +=
1540
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1541
+ ieTable +
1542
+ '."guid" IN (SELECT "guid" FROM ' +
1543
+ PostgreSQLDriver.escape(
1544
+ this.prefix + 'comparisons_' + etype
1545
+ ) +
1546
+ ' WHERE "name"=@' +
1547
+ name +
1548
+ ' AND "number">=@' +
1549
+ value +
1550
+ ')';
1551
+ params[name] = curValue[0];
1552
+ params[value] = isNaN(Number(curValue[1]))
1553
+ ? null
1554
+ : Number(curValue[1]);
1555
+ }
1556
+ break;
1557
+ case 'lt':
1558
+ case '!lt':
1559
+ if (curValue[0] === 'cdate') {
1560
+ if (curQuery) {
1561
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1562
+ }
1563
+ const cdate = `param${++count.i}`;
1564
+ curQuery +=
1565
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1566
+ ieTable +
1567
+ '."cdate"<@' +
1568
+ cdate;
1569
+ params[cdate] = isNaN(Number(curValue[1]))
1570
+ ? null
1571
+ : Number(curValue[1]);
1572
+ break;
1573
+ } else if (curValue[0] === 'mdate') {
1574
+ if (curQuery) {
1575
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1576
+ }
1577
+ const mdate = `param${++count.i}`;
1578
+ curQuery +=
1579
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1580
+ ieTable +
1581
+ '."mdate"<@' +
1582
+ mdate;
1583
+ params[mdate] = isNaN(Number(curValue[1]))
1584
+ ? null
1585
+ : Number(curValue[1]);
1586
+ break;
1587
+ } else {
1588
+ if (curQuery) {
1589
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1590
+ }
1591
+ const name = `param${++count.i}`;
1592
+ const value = `param${++count.i}`;
1593
+ curQuery +=
1594
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1595
+ ieTable +
1596
+ '."guid" IN (SELECT "guid" FROM ' +
1597
+ PostgreSQLDriver.escape(
1598
+ this.prefix + 'comparisons_' + etype
1599
+ ) +
1600
+ ' WHERE "name"=@' +
1601
+ name +
1602
+ ' AND "number"<@' +
1603
+ value +
1604
+ ')';
1605
+ params[name] = curValue[0];
1606
+ params[value] = isNaN(Number(curValue[1]))
1607
+ ? null
1608
+ : Number(curValue[1]);
1609
+ }
1610
+ break;
1611
+ case 'lte':
1612
+ case '!lte':
1613
+ if (curValue[0] === 'cdate') {
1614
+ if (curQuery) {
1615
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1616
+ }
1617
+ const cdate = `param${++count.i}`;
1618
+ curQuery +=
1619
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1620
+ ieTable +
1621
+ '."cdate"<=@' +
1622
+ cdate;
1623
+ params[cdate] = isNaN(Number(curValue[1]))
1624
+ ? null
1625
+ : Number(curValue[1]);
1626
+ break;
1627
+ } else if (curValue[0] === 'mdate') {
1628
+ if (curQuery) {
1629
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1630
+ }
1631
+ const mdate = `param${++count.i}`;
1632
+ curQuery +=
1633
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1634
+ ieTable +
1635
+ '."mdate"<=@' +
1636
+ mdate;
1637
+ params[mdate] = isNaN(Number(curValue[1]))
1638
+ ? null
1639
+ : Number(curValue[1]);
1640
+ break;
1641
+ } else {
1642
+ if (curQuery) {
1643
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1644
+ }
1645
+ const name = `param${++count.i}`;
1646
+ const value = `param${++count.i}`;
1647
+ curQuery +=
1648
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1649
+ ieTable +
1650
+ '."guid" IN (SELECT "guid" FROM ' +
1651
+ PostgreSQLDriver.escape(
1652
+ this.prefix + 'comparisons_' + etype
1653
+ ) +
1654
+ ' WHERE "name"=@' +
1655
+ name +
1656
+ ' AND "number"<=@' +
1657
+ value +
1658
+ ')';
1659
+ params[name] = curValue[0];
1660
+ params[value] = isNaN(Number(curValue[1]))
1661
+ ? null
1662
+ : Number(curValue[1]);
1663
+ }
1664
+ break;
1665
+ case 'ref':
1666
+ case '!ref':
1667
+ let curQguid: string;
1668
+ if (typeof curValue[1] === 'string') {
1669
+ curQguid = curValue[1];
1670
+ } else if ('guid' in curValue[1]) {
1671
+ curQguid = curValue[1].guid;
1672
+ } else {
1673
+ curQguid = `${curValue[1]}`;
1674
+ }
1675
+ if (curQuery) {
1676
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1677
+ }
1678
+ const name = `param${++count.i}`;
1679
+ const guid = `param${++count.i}`;
1680
+ curQuery +=
1681
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1682
+ ieTable +
1683
+ '."guid" IN (SELECT "guid" FROM ' +
1684
+ PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1685
+ ' WHERE "name"=@' +
1686
+ name +
1687
+ ' AND "reference"=decode(@' +
1688
+ guid +
1689
+ ", 'hex'))";
1690
+ params[name] = curValue[0];
1691
+ params[guid] = curQguid;
1692
+ break;
1693
+ case 'selector':
1694
+ case '!selector':
1695
+ const subquery = this.makeEntityQuery(
1696
+ options,
1697
+ [curValue],
1698
+ etype,
1699
+ count,
1700
+ params,
1701
+ true,
1702
+ tableSuffix,
1703
+ etypes
1704
+ );
1705
+ if (curQuery) {
1706
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1707
+ }
1708
+ curQuery +=
1709
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1710
+ '(' +
1711
+ subquery.query +
1712
+ ')';
1713
+ break;
1714
+ case 'qref':
1715
+ case '!qref':
1716
+ const [qrefOptions, ...qrefSelectors] = curValue[1] as [
1717
+ Options,
1718
+ ...FormattedSelector[]
1719
+ ];
1720
+ const QrefEntityClass = qrefOptions.class as EntityConstructor;
1721
+ etypes.push(QrefEntityClass.ETYPE);
1722
+ const qrefQuery = this.makeEntityQuery(
1723
+ { ...qrefOptions, return: 'guid', class: QrefEntityClass },
1724
+ qrefSelectors,
1725
+ QrefEntityClass.ETYPE,
1726
+ count,
1727
+ params,
1728
+ false,
1729
+ makeTableSuffix(),
1730
+ etypes
1731
+ );
1732
+ if (curQuery) {
1733
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1734
+ }
1735
+ const qrefName = `param${++count.i}`;
1736
+ curQuery +=
1737
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1738
+ ieTable +
1739
+ '."guid" IN (SELECT "guid" FROM ' +
1740
+ PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1741
+ ' WHERE "name"=@' +
1742
+ qrefName +
1743
+ ' AND "reference" IN (' +
1744
+ qrefQuery.query +
1745
+ '))';
1746
+ params[qrefName] = curValue[0];
1747
+ break;
1748
+ }
1749
+ }
1750
+ return curQuery;
1751
+ }
1752
+ );
1753
+
1754
+ let sortBy: string;
1755
+ switch (sort) {
1756
+ case 'mdate':
1757
+ sortBy = '"mdate"';
1758
+ break;
1759
+ case 'cdate':
1760
+ default:
1761
+ sortBy = '"cdate"';
1762
+ break;
1763
+ }
1764
+ if (options.reverse) {
1765
+ sortBy += ' DESC';
1766
+ }
1767
+
1768
+ let query: string;
1769
+ if (queryParts.length) {
1770
+ if (subquery) {
1771
+ query = '(' + queryParts.join(') AND (') + ')';
1772
+ } else {
1773
+ let limit = '';
1774
+ if ('limit' in options) {
1775
+ limit = ` LIMIT ${Math.floor(
1776
+ isNaN(Number(options.limit)) ? 0 : Number(options.limit)
1777
+ )}`;
1778
+ }
1779
+ let offset = '';
1780
+ if ('offset' in options) {
1781
+ offset = ` OFFSET ${Math.floor(
1782
+ isNaN(Number(options.offset)) ? 0 : Number(options.offset)
1783
+ )}`;
1784
+ }
1785
+ const whereClause = queryParts.join(') AND (');
1786
+ if (options.return === 'guid') {
1787
+ const guidColumn =
1788
+ tableSuffix === ''
1789
+ ? `encode(${ieTable}."guid", 'hex')`
1790
+ : `${ieTable}."guid"`;
1791
+ query = `SELECT ${guidColumn} as "guid"
1792
+ FROM ${PostgreSQLDriver.escape(
1793
+ `${this.prefix}entities_${etype}`
1794
+ )} ${ieTable}
1795
+ WHERE (${whereClause})
1796
+ ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1797
+ } else {
1798
+ query = `SELECT
1799
+ encode(${eTable}."guid", 'hex') as "guid",
1800
+ ${eTable}."tags",
1801
+ ${eTable}."cdate",
1802
+ ${eTable}."mdate",
1803
+ ${dTable}."name",
1804
+ ${dTable}."value",
1805
+ ${cTable}."string",
1806
+ ${cTable}."number"
1807
+ FROM ${PostgreSQLDriver.escape(
1808
+ `${this.prefix}entities_${etype}`
1809
+ )} ${eTable}
1810
+ LEFT JOIN ${PostgreSQLDriver.escape(
1811
+ `${this.prefix}data_${etype}`
1812
+ )} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1813
+ INNER JOIN ${PostgreSQLDriver.escape(
1814
+ `${this.prefix}comparisons_${etype}`
1815
+ )} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1816
+ INNER JOIN (
1817
+ SELECT ${ieTable}."guid"
1818
+ FROM ${PostgreSQLDriver.escape(
1819
+ `${this.prefix}entities_${etype}`
1820
+ )} ${ieTable}
1821
+ WHERE (${whereClause})
1822
+ ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1823
+ ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1824
+ ORDER BY ${eTable}.${sortBy}`;
1825
+ }
1826
+ }
1827
+ } else {
1828
+ if (subquery) {
1829
+ query = '';
1830
+ } else {
1831
+ let limit = '';
1832
+ if ('limit' in options) {
1833
+ limit = ` LIMIT ${Math.floor(
1834
+ isNaN(Number(options.limit)) ? 0 : Number(options.limit)
1835
+ )}`;
1836
+ }
1837
+ let offset = '';
1838
+ if ('offset' in options) {
1839
+ offset = ` OFFSET ${Math.floor(
1840
+ isNaN(Number(options.offset)) ? 0 : Number(options.offset)
1841
+ )}`;
1842
+ }
1843
+ if (options.return === 'guid') {
1844
+ const guidColumn =
1845
+ tableSuffix === ''
1846
+ ? `encode(${ieTable}."guid", 'hex')`
1847
+ : `${ieTable}."guid"`;
1848
+ query = `SELECT ${guidColumn} as "guid"
1849
+ FROM ${PostgreSQLDriver.escape(
1850
+ `${this.prefix}entities_${etype}`
1851
+ )} ${ieTable}
1852
+ ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1853
+ } else {
1854
+ if (limit || offset) {
1855
+ query = `SELECT
1856
+ encode(${eTable}."guid", 'hex') as "guid",
1857
+ ${eTable}."tags",
1858
+ ${eTable}."cdate",
1859
+ ${eTable}."mdate",
1860
+ ${dTable}."name",
1861
+ ${dTable}."value",
1862
+ ${cTable}."string",
1863
+ ${cTable}."number"
1864
+ FROM ${PostgreSQLDriver.escape(
1865
+ `${this.prefix}entities_${etype}`
1866
+ )} ${eTable}
1867
+ LEFT JOIN ${PostgreSQLDriver.escape(
1868
+ `${this.prefix}data_${etype}`
1869
+ )} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1870
+ INNER JOIN ${PostgreSQLDriver.escape(
1871
+ `${this.prefix}comparisons_${etype}`
1872
+ )} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1873
+ INNER JOIN (
1874
+ SELECT ${ieTable}."guid"
1875
+ FROM ${PostgreSQLDriver.escape(
1876
+ `${this.prefix}entities_${etype}`
1877
+ )} ${ieTable}
1878
+ ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1879
+ ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1880
+ ORDER BY ${eTable}.${sortBy}`;
1881
+ } else {
1882
+ query = `SELECT
1883
+ encode(${eTable}."guid", 'hex') as "guid",
1884
+ ${eTable}."tags",
1885
+ ${eTable}."cdate",
1886
+ ${eTable}."mdate",
1887
+ ${dTable}."name",
1888
+ ${dTable}."value",
1889
+ ${cTable}."string",
1890
+ ${cTable}."number"
1891
+ FROM ${PostgreSQLDriver.escape(
1892
+ `${this.prefix}entities_${etype}`
1893
+ )} ${eTable}
1894
+ LEFT JOIN ${PostgreSQLDriver.escape(
1895
+ `${this.prefix}data_${etype}`
1896
+ )} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1897
+ INNER JOIN ${PostgreSQLDriver.escape(
1898
+ `${this.prefix}comparisons_${etype}`
1899
+ )} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1900
+ ORDER BY ${eTable}.${sortBy}`;
1901
+ }
1902
+ }
1903
+ }
1904
+ }
1905
+
1906
+ if (etypes.indexOf(etype) === -1) {
1907
+ etypes.push(etype);
1908
+ }
1909
+
1910
+ return {
1911
+ query,
1912
+ params,
1913
+ etypes,
1914
+ };
1915
+ }
1916
+
1917
+ protected performQuery(
1918
+ options: Options,
1919
+ formattedSelectors: FormattedSelector[],
1920
+ etype: string
1921
+ ): {
1922
+ result: any;
1923
+ } {
1924
+ const { query, params, etypes } = this.makeEntityQuery(
1925
+ options,
1926
+ formattedSelectors,
1927
+ etype
1928
+ );
1929
+ const result = this.queryIter(query, { etypes, params }).then((val) =>
1930
+ val[Symbol.iterator]()
1931
+ );
1932
+ return {
1933
+ result,
1934
+ };
1935
+ }
1936
+
1937
+ protected performQuerySync(
1938
+ options: Options,
1939
+ formattedSelectors: FormattedSelector[],
1940
+ etype: string
1941
+ ): {
1942
+ result: any;
1943
+ } {
1944
+ const { query, params, etypes } = this.makeEntityQuery(
1945
+ options,
1946
+ formattedSelectors,
1947
+ etype
1948
+ );
1949
+ const result = this.queryIterSync(query, { etypes, params })[
1950
+ Symbol.iterator
1951
+ ]();
1952
+ return {
1953
+ result,
1954
+ };
1955
+ }
1956
+
1957
+ public async getEntities<T extends EntityConstructor = EntityConstructor>(
1958
+ options: Options<T> & { return: 'guid' },
1959
+ ...selectors: Selector[]
1960
+ ): Promise<string[]>;
1961
+ public async getEntities<T extends EntityConstructor = EntityConstructor>(
1962
+ options?: Options<T>,
1963
+ ...selectors: Selector[]
1964
+ ): Promise<ReturnType<T['factorySync']>[]>;
1965
+ public async getEntities<T extends EntityConstructor = EntityConstructor>(
1966
+ options: Options<T> = {},
1967
+ ...selectors: Selector[]
1968
+ ): Promise<ReturnType<T['factorySync']>[] | string[]> {
1969
+ const { result: resultPromise, process } = this.getEntitesRowLike<T>(
1970
+ // @ts-ignore: options is correct here.
1971
+ options,
1972
+ selectors,
1973
+ (options, formattedSelectors, etype) =>
1974
+ this.performQuery(options, formattedSelectors, etype),
1975
+ () => {
1976
+ const next: any = result.next();
1977
+ return next.done ? null : next.value;
1978
+ },
1979
+ () => undefined,
1980
+ (row) => row.guid,
1981
+ (row) => ({
1982
+ tags: row.tags,
1983
+ cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
1984
+ mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
1985
+ }),
1986
+ (row) => ({
1987
+ name: row.name,
1988
+ svalue:
1989
+ row.value === 'N'
1990
+ ? JSON.stringify(Number(row.number))
1991
+ : row.value === 'S'
1992
+ ? JSON.stringify(row.string)
1993
+ : row.value,
1994
+ })
1995
+ );
1996
+
1997
+ const result = await resultPromise;
1998
+ return process();
1999
+ }
2000
+
2001
+ protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
2002
+ options: Options<T> & { return: 'guid' },
2003
+ ...selectors: Selector[]
2004
+ ): string[];
2005
+ protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
2006
+ options?: Options<T>,
2007
+ ...selectors: Selector[]
2008
+ ): ReturnType<T['factorySync']>[];
2009
+ protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
2010
+ options: Options<T> = {},
2011
+ ...selectors: Selector[]
2012
+ ): ReturnType<T['factorySync']>[] | string[] {
2013
+ const { result, process } = this.getEntitesRowLike<T>(
2014
+ // @ts-ignore: options is correct here.
2015
+ options,
2016
+ selectors,
2017
+ (options, formattedSelectors, etype) =>
2018
+ this.performQuerySync(options, formattedSelectors, etype),
2019
+ () => {
2020
+ const next: any = result.next();
2021
+ return next.done ? null : next.value;
2022
+ },
2023
+ () => undefined,
2024
+ (row) => row.guid,
2025
+ (row) => ({
2026
+ tags: row.tags,
2027
+ cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
2028
+ mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
2029
+ }),
2030
+ (row) => ({
2031
+ name: row.name,
2032
+ svalue:
2033
+ row.value === 'N'
2034
+ ? JSON.stringify(Number(row.number))
2035
+ : row.value === 'S'
2036
+ ? JSON.stringify(row.string)
2037
+ : row.value,
2038
+ })
2039
+ );
2040
+ return process();
2041
+ }
2042
+
2043
+ public async getUID(name: string) {
2044
+ if (name == null) {
2045
+ throw new InvalidParametersError('Name not given for UID.');
2046
+ }
2047
+ const result = await this.queryGet(
2048
+ `SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(
2049
+ `${this.prefix}uids`
2050
+ )} WHERE "name"=@name;`,
2051
+ {
2052
+ params: {
2053
+ name: name,
2054
+ },
2055
+ }
2056
+ );
2057
+ return result?.cur_uid == null ? null : Number(result.cur_uid);
2058
+ }
2059
+
2060
+ public async import(filename: string) {
2061
+ try {
2062
+ const result = await this.importFromFile(
2063
+ filename,
2064
+ async (guid, tags, sdata, etype) => {
2065
+ await this.queryRun(
2066
+ `DELETE FROM ${PostgreSQLDriver.escape(
2067
+ `${this.prefix}entities_${etype}`
2068
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2069
+ {
2070
+ etypes: [etype],
2071
+ params: {
2072
+ guid,
2073
+ },
2074
+ }
2075
+ );
2076
+ await this.queryRun(
2077
+ `INSERT INTO ${PostgreSQLDriver.escape(
2078
+ `${this.prefix}entities_${etype}`
2079
+ )} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @mdate);`,
2080
+ {
2081
+ etypes: [etype],
2082
+ params: {
2083
+ guid,
2084
+ tags,
2085
+ cdate: isNaN(Number(JSON.parse(sdata.cdate)))
2086
+ ? null
2087
+ : Number(JSON.parse(sdata.cdate)),
2088
+ mdate: isNaN(Number(JSON.parse(sdata.mdate)))
2089
+ ? null
2090
+ : Number(JSON.parse(sdata.mdate)),
2091
+ },
2092
+ }
2093
+ );
2094
+ const promises = [];
2095
+ promises.push(
2096
+ this.queryRun(
2097
+ `DELETE FROM ${PostgreSQLDriver.escape(
2098
+ `${this.prefix}data_${etype}`
2099
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2100
+ {
2101
+ etypes: [etype],
2102
+ params: {
2103
+ guid,
2104
+ },
2105
+ }
2106
+ )
2107
+ );
2108
+ promises.push(
2109
+ this.queryRun(
2110
+ `DELETE FROM ${PostgreSQLDriver.escape(
2111
+ `${this.prefix}comparisons_${etype}`
2112
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2113
+ {
2114
+ etypes: [etype],
2115
+ params: {
2116
+ guid,
2117
+ },
2118
+ }
2119
+ )
2120
+ );
2121
+ promises.push(
2122
+ this.queryRun(
2123
+ `DELETE FROM ${PostgreSQLDriver.escape(
2124
+ `${this.prefix}references_${etype}`
2125
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2126
+ {
2127
+ etypes: [etype],
2128
+ params: {
2129
+ guid,
2130
+ },
2131
+ }
2132
+ )
2133
+ );
2134
+ await Promise.all(promises);
2135
+ delete sdata.cdate;
2136
+ delete sdata.mdate;
2137
+ for (const name in sdata) {
2138
+ const value = sdata[name];
2139
+ const uvalue = JSON.parse(value);
2140
+ if (value === undefined) {
2141
+ continue;
2142
+ }
2143
+ const storageValue =
2144
+ typeof uvalue === 'number'
2145
+ ? 'N'
2146
+ : typeof uvalue === 'string'
2147
+ ? 'S'
2148
+ : value;
2149
+ const promises = [];
2150
+ promises.push(
2151
+ this.queryRun(
2152
+ `INSERT INTO ${PostgreSQLDriver.escape(
2153
+ `${this.prefix}data_${etype}`
2154
+ )} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`,
2155
+ {
2156
+ etypes: [etype],
2157
+ params: {
2158
+ guid,
2159
+ name,
2160
+ storageValue,
2161
+ },
2162
+ }
2163
+ )
2164
+ );
2165
+ promises.push(
2166
+ this.queryRun(
2167
+ `INSERT INTO ${PostgreSQLDriver.escape(
2168
+ `${this.prefix}comparisons_${etype}`
2169
+ )} ("guid", "name", "truthy", "string", "number") VALUES (decode(@guid, 'hex'), @name, @truthy, @string, @number);`,
2170
+ {
2171
+ etypes: [etype],
2172
+ params: {
2173
+ guid,
2174
+ name,
2175
+ truthy: !!uvalue,
2176
+ string: `${uvalue}`,
2177
+ number: isNaN(Number(uvalue)) ? null : Number(uvalue),
2178
+ },
2179
+ }
2180
+ )
2181
+ );
2182
+ const references = this.findReferences(value);
2183
+ for (const reference of references) {
2184
+ promises.push(
2185
+ this.queryRun(
2186
+ `INSERT INTO ${PostgreSQLDriver.escape(
2187
+ `${this.prefix}references_${etype}`
2188
+ )} ("guid", "name", "reference") VALUES (decode(@guid, 'hex'), @name, decode(@reference, 'hex'));`,
2189
+ {
2190
+ etypes: [etype],
2191
+ params: {
2192
+ guid,
2193
+ name,
2194
+ reference,
2195
+ },
2196
+ }
2197
+ )
2198
+ );
2199
+ }
2200
+ }
2201
+ await Promise.all(promises);
2202
+ },
2203
+ async (name, curUid) => {
2204
+ await this.queryRun(
2205
+ `DELETE FROM ${PostgreSQLDriver.escape(
2206
+ `${this.prefix}uids`
2207
+ )} WHERE "name"=@name;`,
2208
+ {
2209
+ params: {
2210
+ name,
2211
+ },
2212
+ }
2213
+ );
2214
+ await this.queryRun(
2215
+ `INSERT INTO ${PostgreSQLDriver.escape(
2216
+ `${this.prefix}uids`
2217
+ )} ("name", "cur_uid") VALUES (@name, @curUid);`,
2218
+ {
2219
+ params: {
2220
+ name,
2221
+ curUid,
2222
+ },
2223
+ }
2224
+ );
2225
+ },
2226
+ async () => {
2227
+ await this.internalTransaction('nymph-import');
2228
+ },
2229
+ async () => {
2230
+ await this.commit('nymph-import');
2231
+ }
2232
+ );
2233
+
2234
+ return result;
2235
+ } catch (e: any) {
2236
+ await this.rollback('nymph-import');
2237
+ return false;
2238
+ }
2239
+ }
2240
+
2241
+ public async newUID(name: string) {
2242
+ if (name == null) {
2243
+ throw new InvalidParametersError('Name not given for UID.');
2244
+ }
2245
+ await this.internalTransaction('nymph-newuid');
2246
+ try {
2247
+ const lock = await this.queryGet(
2248
+ `SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(
2249
+ `${this.prefix}uids`
2250
+ )} WHERE "name"=@name FOR UPDATE;`,
2251
+ {
2252
+ params: {
2253
+ name,
2254
+ },
2255
+ }
2256
+ );
2257
+ let curUid: number | undefined =
2258
+ lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
2259
+ if (curUid == null) {
2260
+ curUid = 1;
2261
+ await this.queryRun(
2262
+ `INSERT INTO ${PostgreSQLDriver.escape(
2263
+ `${this.prefix}uids`
2264
+ )} ("name", "cur_uid") VALUES (@name, @curUid);`,
2265
+ {
2266
+ params: {
2267
+ name,
2268
+ curUid,
2269
+ },
2270
+ }
2271
+ );
2272
+ } else {
2273
+ curUid++;
2274
+ await this.queryRun(
2275
+ `UPDATE ${PostgreSQLDriver.escape(
2276
+ `${this.prefix}uids`
2277
+ )} SET "cur_uid"=@curUid WHERE "name"=@name;`,
2278
+ {
2279
+ params: {
2280
+ name,
2281
+ curUid,
2282
+ },
2283
+ }
2284
+ );
2285
+ }
2286
+ await this.commit('nymph-newuid');
2287
+ return curUid;
2288
+ } catch (e: any) {
2289
+ await this.rollback('nymph-newuid');
2290
+ throw e;
2291
+ }
2292
+ }
2293
+
2294
+ public async renameUID(oldName: string, newName: string) {
2295
+ if (oldName == null || newName == null) {
2296
+ throw new InvalidParametersError('Name not given for UID.');
2297
+ }
2298
+ await this.queryRun(
2299
+ `UPDATE ${PostgreSQLDriver.escape(
2300
+ `${this.prefix}uids`
2301
+ )} SET "name"=@newName WHERE "name"=@oldName;`,
2302
+ {
2303
+ params: {
2304
+ newName,
2305
+ oldName,
2306
+ },
2307
+ }
2308
+ );
2309
+ return true;
2310
+ }
2311
+
2312
+ public async rollback(name: string) {
2313
+ if (name == null || typeof name !== 'string' || name.length === 0) {
2314
+ throw new InvalidParametersError(
2315
+ 'Transaction rollback attempted without a name.'
2316
+ );
2317
+ }
2318
+ if (!this.transaction || this.transaction.count === 0) {
2319
+ this.transaction = null;
2320
+ return true;
2321
+ }
2322
+ await this.queryRun(
2323
+ `ROLLBACK TO SAVEPOINT ${PostgreSQLDriver.escape(name)};`
2324
+ );
2325
+ this.transaction.count--;
2326
+ if (this.transaction.count === 0) {
2327
+ await this.queryRun('ROLLBACK;');
2328
+ this.transaction.connection?.done();
2329
+ this.transaction.connection = null;
2330
+ this.transaction = null;
2331
+ }
2332
+ return true;
2333
+ }
2334
+
2335
+ public async saveEntity(entity: EntityInterface) {
2336
+ const insertData = async (
2337
+ guid: string,
2338
+ data: EntityData,
2339
+ sdata: SerializedEntityData,
2340
+ etype: string
2341
+ ) => {
2342
+ const runInsertQuery = async (
2343
+ name: string,
2344
+ value: any,
2345
+ svalue: string
2346
+ ) => {
2347
+ if (value === undefined) {
2348
+ return;
2349
+ }
2350
+ const storageValue =
2351
+ typeof value === 'number'
2352
+ ? 'N'
2353
+ : typeof value === 'string'
2354
+ ? 'S'
2355
+ : svalue;
2356
+ const promises = [];
2357
+ promises.push(
2358
+ this.queryRun(
2359
+ `INSERT INTO ${PostgreSQLDriver.escape(
2360
+ `${this.prefix}data_${etype}`
2361
+ )} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`,
2362
+ {
2363
+ etypes: [etype],
2364
+ params: {
2365
+ guid,
2366
+ name,
2367
+ storageValue,
2368
+ },
2369
+ }
2370
+ )
2371
+ );
2372
+ promises.push(
2373
+ this.queryRun(
2374
+ `INSERT INTO ${PostgreSQLDriver.escape(
2375
+ `${this.prefix}comparisons_${etype}`
2376
+ )} ("guid", "name", "truthy", "string", "number") VALUES (decode(@guid, 'hex'), @name, @truthy, @string, @number);`,
2377
+ {
2378
+ etypes: [etype],
2379
+ params: {
2380
+ guid,
2381
+ name,
2382
+ truthy: !!value,
2383
+ string: `${value}`,
2384
+ number: isNaN(Number(value)) ? null : Number(value),
2385
+ },
2386
+ }
2387
+ )
2388
+ );
2389
+ const references = this.findReferences(svalue);
2390
+ for (const reference of references) {
2391
+ promises.push(
2392
+ this.queryRun(
2393
+ `INSERT INTO ${PostgreSQLDriver.escape(
2394
+ `${this.prefix}references_${etype}`
2395
+ )} ("guid", "name", "reference") VALUES (decode(@guid, 'hex'), @name, decode(@reference, 'hex'));`,
2396
+ {
2397
+ etypes: [etype],
2398
+ params: {
2399
+ guid,
2400
+ name,
2401
+ reference,
2402
+ },
2403
+ }
2404
+ )
2405
+ );
2406
+ }
2407
+ await Promise.all(promises);
2408
+ };
2409
+ for (const name in data) {
2410
+ await runInsertQuery(name, data[name], JSON.stringify(data[name]));
2411
+ }
2412
+ for (const name in sdata) {
2413
+ await runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
2414
+ }
2415
+ };
2416
+ try {
2417
+ const result = await this.saveEntityRowLike(
2418
+ entity,
2419
+ async (_entity, guid, tags, data, sdata, cdate, etype) => {
2420
+ await this.queryRun(
2421
+ `INSERT INTO ${PostgreSQLDriver.escape(
2422
+ `${this.prefix}entities_${etype}`
2423
+ )} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @cdate);`,
2424
+ {
2425
+ etypes: [etype],
2426
+ params: {
2427
+ guid,
2428
+ tags,
2429
+ cdate,
2430
+ },
2431
+ }
2432
+ );
2433
+ await insertData(guid, data, sdata, etype);
2434
+ return true;
2435
+ },
2436
+ async (entity, guid, tags, data, sdata, mdate, etype) => {
2437
+ const promises = [];
2438
+ promises.push(
2439
+ this.queryRun(
2440
+ `SELECT 1 FROM ${PostgreSQLDriver.escape(
2441
+ `${this.prefix}entities_${etype}`
2442
+ )} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`,
2443
+ {
2444
+ etypes: [etype],
2445
+ params: {
2446
+ guid,
2447
+ },
2448
+ }
2449
+ )
2450
+ );
2451
+ promises.push(
2452
+ this.queryRun(
2453
+ `SELECT 1 FROM ${PostgreSQLDriver.escape(
2454
+ `${this.prefix}data_${etype}`
2455
+ )} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`,
2456
+ {
2457
+ etypes: [etype],
2458
+ params: {
2459
+ guid,
2460
+ },
2461
+ }
2462
+ )
2463
+ );
2464
+ promises.push(
2465
+ this.queryRun(
2466
+ `SELECT 1 FROM ${PostgreSQLDriver.escape(
2467
+ `${this.prefix}comparisons_${etype}`
2468
+ )} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`,
2469
+ {
2470
+ etypes: [etype],
2471
+ params: {
2472
+ guid,
2473
+ },
2474
+ }
2475
+ )
2476
+ );
2477
+ promises.push(
2478
+ this.queryRun(
2479
+ `SELECT 1 FROM ${PostgreSQLDriver.escape(
2480
+ `${this.prefix}references_${etype}`
2481
+ )} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`,
2482
+ {
2483
+ etypes: [etype],
2484
+ params: {
2485
+ guid,
2486
+ },
2487
+ }
2488
+ )
2489
+ );
2490
+ await Promise.all(promises);
2491
+ const info = await this.queryRun(
2492
+ `UPDATE ${PostgreSQLDriver.escape(
2493
+ `${this.prefix}entities_${etype}`
2494
+ )} SET "tags"=@tags, "mdate"=@mdate WHERE "guid"=decode(@guid, 'hex') AND "mdate" <= @emdate;`,
2495
+ {
2496
+ etypes: [etype],
2497
+ params: {
2498
+ tags,
2499
+ mdate,
2500
+ guid,
2501
+ emdate: isNaN(Number(entity.mdate)) ? 0 : Number(entity.mdate),
2502
+ },
2503
+ }
2504
+ );
2505
+ let success = false;
2506
+ if (info.rowCount === 1) {
2507
+ const promises = [];
2508
+ promises.push(
2509
+ this.queryRun(
2510
+ `DELETE FROM ${PostgreSQLDriver.escape(
2511
+ `${this.prefix}data_${etype}`
2512
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2513
+ {
2514
+ etypes: [etype],
2515
+ params: {
2516
+ guid,
2517
+ },
2518
+ }
2519
+ )
2520
+ );
2521
+ promises.push(
2522
+ this.queryRun(
2523
+ `DELETE FROM ${PostgreSQLDriver.escape(
2524
+ `${this.prefix}comparisons_${etype}`
2525
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2526
+ {
2527
+ etypes: [etype],
2528
+ params: {
2529
+ guid,
2530
+ },
2531
+ }
2532
+ )
2533
+ );
2534
+ promises.push(
2535
+ this.queryRun(
2536
+ `DELETE FROM ${PostgreSQLDriver.escape(
2537
+ `${this.prefix}references_${etype}`
2538
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2539
+ {
2540
+ etypes: [etype],
2541
+ params: {
2542
+ guid,
2543
+ },
2544
+ }
2545
+ )
2546
+ );
2547
+ await Promise.all(promises);
2548
+ await insertData(guid, data, sdata, etype);
2549
+ success = true;
2550
+ }
2551
+ return success;
2552
+ },
2553
+ async () => {
2554
+ await this.internalTransaction('nymph-save');
2555
+ },
2556
+ async (success) => {
2557
+ if (success) {
2558
+ await this.commit('nymph-save');
2559
+ } else {
2560
+ await this.rollback('nymph-save');
2561
+ }
2562
+ return success;
2563
+ }
2564
+ );
2565
+
2566
+ return result;
2567
+ } catch (e: any) {
2568
+ await this.rollback('nymph-save');
2569
+ throw e;
2570
+ }
2571
+ }
2572
+
2573
+ public async setUID(name: string, curUid: number) {
2574
+ if (name == null) {
2575
+ throw new InvalidParametersError('Name not given for UID.');
2576
+ }
2577
+ await this.internalTransaction('nymph-setuid');
2578
+ try {
2579
+ await this.queryRun(
2580
+ `DELETE FROM ${PostgreSQLDriver.escape(
2581
+ `${this.prefix}uids`
2582
+ )} WHERE "name"=@name;`,
2583
+ {
2584
+ params: {
2585
+ name,
2586
+ curUid,
2587
+ },
2588
+ }
2589
+ );
2590
+ await this.queryRun(
2591
+ `INSERT INTO ${PostgreSQLDriver.escape(
2592
+ `${this.prefix}uids`
2593
+ )} ("name", "cur_uid") VALUES (@name, @curUid);`,
2594
+ {
2595
+ params: {
2596
+ name,
2597
+ curUid,
2598
+ },
2599
+ }
2600
+ );
2601
+ await this.commit('nymph-setuid');
2602
+ return true;
2603
+ } catch (e: any) {
2604
+ await this.rollback('nymph-setuid');
2605
+ throw e;
2606
+ }
2607
+ }
2608
+
2609
+ private async internalTransaction(name: string) {
2610
+ if (name == null || typeof name !== 'string' || name.length === 0) {
2611
+ throw new InvalidParametersError(
2612
+ 'Transaction start attempted without a name.'
2613
+ );
2614
+ }
2615
+
2616
+ if (!this.transaction || this.transaction.count === 0) {
2617
+ // Lock to one connection.
2618
+ this.transaction = {
2619
+ count: 0,
2620
+ connection: await this.getConnection(),
2621
+ };
2622
+ // We're not in a transaction yet, so start one.
2623
+ await this.queryRun('BEGIN;');
2624
+ }
2625
+
2626
+ await this.queryRun(`SAVEPOINT ${PostgreSQLDriver.escape(name)};`);
2627
+
2628
+ this.transaction.count++;
2629
+
2630
+ return this.transaction;
2631
+ }
2632
+
2633
+ public async startTransaction(name: string) {
2634
+ const inTransaction = this.inTransaction();
2635
+ const transaction = await this.internalTransaction(name);
2636
+ if (!inTransaction) {
2637
+ this.transaction = null;
2638
+ }
2639
+
2640
+ const nymph = this.nymph.clone();
2641
+ nymph.driver = new PostgreSQLDriver(this.config, this.link, transaction);
2642
+ nymph.driver.init(nymph);
2643
+
2644
+ return nymph;
2645
+ }
2646
+ }