@hedhog/pagination 0.0.16 → 0.0.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. package/README.md +4 -4
  2. package/dist/databases/abstract.database.d.ts +61 -0
  3. package/dist/databases/abstract.database.d.ts.map +1 -0
  4. package/dist/databases/abstract.database.js +643 -0
  5. package/dist/databases/abstract.database.js.map +1 -0
  6. package/dist/databases/database.d.ts +5 -0
  7. package/dist/databases/database.d.ts.map +1 -0
  8. package/dist/databases/database.factory.d.ts +7 -0
  9. package/dist/databases/database.factory.d.ts.map +1 -0
  10. package/dist/databases/database.factory.js +44 -0
  11. package/dist/databases/database.factory.js.map +1 -0
  12. package/dist/databases/database.js +9 -0
  13. package/dist/databases/database.js.map +1 -0
  14. package/dist/databases/index.d.ts +4 -0
  15. package/dist/databases/index.d.ts.map +1 -0
  16. package/dist/databases/index.js +20 -0
  17. package/dist/databases/index.js.map +1 -0
  18. package/dist/databases/mysql.database.d.ts +10 -0
  19. package/dist/databases/mysql.database.d.ts.map +1 -0
  20. package/dist/databases/mysql.database.js +17 -0
  21. package/dist/databases/mysql.database.js.map +1 -0
  22. package/dist/databases/postgres.database.d.ts +10 -0
  23. package/dist/databases/postgres.database.d.ts.map +1 -0
  24. package/dist/databases/postgres.database.js +17 -0
  25. package/dist/databases/postgres.database.js.map +1 -0
  26. package/dist/pagination.service.d.ts +16 -2
  27. package/dist/pagination.service.d.ts.map +1 -1
  28. package/dist/pagination.service.js +131 -5
  29. package/dist/pagination.service.js.map +1 -1
  30. package/dist/types/pagination.types.d.ts +1 -0
  31. package/dist/types/pagination.types.d.ts.map +1 -1
  32. package/dist/types/query-option.d.ts +5 -0
  33. package/dist/types/query-option.d.ts.map +1 -0
  34. package/dist/types/query-option.js +3 -0
  35. package/dist/types/query-option.js.map +1 -0
  36. package/dist/types/relation-n2n-result.d.ts +7 -0
  37. package/dist/types/relation-n2n-result.d.ts.map +1 -0
  38. package/dist/types/relation-n2n-result.js +3 -0
  39. package/dist/types/relation-n2n-result.js.map +1 -0
  40. package/dist/types/transaction-queries.d.ts +7 -0
  41. package/dist/types/transaction-queries.d.ts.map +1 -0
  42. package/dist/types/transaction-queries.js +3 -0
  43. package/dist/types/transaction-queries.js.map +1 -0
  44. package/package.json +6 -2
  45. package/src/databases/abstract.database.ts +833 -0
  46. package/src/databases/database.factory.ts +26 -0
  47. package/src/databases/database.ts +4 -0
  48. package/src/databases/index.ts +3 -0
  49. package/src/databases/mysql.database.ts +14 -0
  50. package/src/databases/postgres.database.ts +14 -0
  51. package/src/pagination.service.ts +216 -12
  52. package/src/types/pagination.types.ts +1 -0
  53. package/src/types/query-option.ts +4 -0
  54. package/src/types/relation-n2n-result.ts +6 -0
  55. package/src/types/transaction-queries.ts +7 -0
@@ -0,0 +1,833 @@
1
+ import { Connection } from 'mysql2/promise';
2
+ import { Client } from 'pg';
3
+ import { DataSource } from 'typeorm';
4
+ import { QueryOption } from '../types/query-option';
5
+ import { RelationN2NResult } from '../types/relation-n2n-result';
6
+ import { TransactionQueries } from '../types/transaction-queries';
7
+ import { Database } from './database';
8
+ import EventEmitter = require('events');
9
+
10
+ export class AbstractDatabase {
11
+ private client: Client | Connection | null = null;
12
+ private foreignKeys: any = {};
13
+ private foreignKeysByTable: any = {};
14
+ private primaryKeys: any = {};
15
+ private columnNameFromRelation: any = {};
16
+ private relationN2N: any = {};
17
+ private relation1N: any = {};
18
+ private columnComment: any = {};
19
+ private tableHasColumnOrder: any = {};
20
+ private eventEmitter = new EventEmitter();
21
+ private autoClose = true;
22
+
23
+ constructor(
24
+ protected type: Database,
25
+ protected host: string,
26
+ protected user: string,
27
+ protected password: string,
28
+ protected database: string,
29
+ protected port: number,
30
+ ) {}
31
+
32
+ getDataSource() {
33
+ return new DataSource({
34
+ type: this.type,
35
+ host: this.host,
36
+ port: this.port,
37
+ username: this.user,
38
+ password: this.password,
39
+ database: this.database,
40
+ synchronize: true,
41
+ logging: false,
42
+ entities: [],
43
+ subscribers: [],
44
+ migrations: [],
45
+ });
46
+ }
47
+
48
+ disableAutoClose() {
49
+ this.autoClose = false;
50
+ }
51
+
52
+ close() {
53
+ return this.client?.end();
54
+ }
55
+
56
+ on(event: string, listener: (...args: any[]) => void) {
57
+ return this.eventEmitter.on(event, listener);
58
+ }
59
+
60
+ getArrayType(values: any[]) {
61
+ return [...new Set(values.map((value) => typeof value))][0];
62
+ }
63
+
64
+ getWhereWithIn(
65
+ columnName: string,
66
+ operator: 'in' | 'nin',
67
+ values: string[] | number[],
68
+ ) {
69
+ switch (this.type) {
70
+ case Database.POSTGRES:
71
+ if (operator === 'in') {
72
+ return `${this.getColumnNameWithScaping(columnName)} = ANY(?::${this.getArrayType(values) === 'number' ? 'int' : 'text'}[])`;
73
+ } else {
74
+ return `${this.getColumnNameWithScaping(columnName)} <> ALL(?::${this.getArrayType(values) === 'number' ? 'int' : 'text'}[])`;
75
+ }
76
+ case Database.MYSQL:
77
+ return `${this.getColumnNameWithScaping(columnName)} ${operator === 'in' ? 'IN' : 'NOT IN'}(${values.map((value) => AbstractDatabase.addSimpleQuotes(value)).join(', ')})`;
78
+ }
79
+ }
80
+
81
+ static addSimpleQuotes(value: any): string {
82
+ if (typeof value === 'string') {
83
+ return `'${value}'`;
84
+ }
85
+
86
+ return value;
87
+ }
88
+
89
+ private replacePlaceholders(query: string): string {
90
+ let index = 1;
91
+ return query.replace(/\?/g, () => {
92
+ return `$${index++}`;
93
+ });
94
+ }
95
+
96
+ getColumnNameWithScaping(columnName: string) {
97
+ switch (this.type) {
98
+ case Database.POSTGRES:
99
+ return `"${columnName}"`;
100
+
101
+ case Database.MYSQL:
102
+ return `\`${columnName}\``;
103
+ }
104
+ }
105
+
106
+ getLimit(offset: number, limit: number) {
107
+ switch (this.type) {
108
+ case Database.POSTGRES:
109
+ return `LIMIT ${offset} OFFSET ${limit}`;
110
+
111
+ case Database.MYSQL:
112
+ return `LIMIT ${offset}, ${limit}`;
113
+ }
114
+ }
115
+
116
+ getTableNameFromQuery(query: string): string | null {
117
+ const match = query.match(/INSERT INTO\s+([`"]?[\w-]+[`"]?)/i);
118
+ if (match && match[1]) {
119
+ return match[1].replace(/[`"]/g, '');
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ async hasTableColumnOrder(tableName: string) {
126
+ if (this.tableHasColumnOrder[tableName]) {
127
+ return this.tableHasColumnOrder[tableName];
128
+ }
129
+
130
+ return (this.tableHasColumnOrder[tableName] =
131
+ (await this.getColumnComment(tableName, 'order')) === 'order');
132
+ }
133
+
134
+ async getColumnComment(tableName: string, columnName: string) {
135
+ if (this.columnComment[`${tableName}.${columnName}`]) {
136
+ return this.columnComment[`${tableName}.${columnName}`];
137
+ }
138
+
139
+ switch (this.type) {
140
+ case Database.POSTGRES:
141
+ const resultPg = await this.query(
142
+ `SELECT a.attname AS column_name,
143
+ col_description(a.attrelid, a.attnum) AS column_comment
144
+ FROM pg_class AS c
145
+ JOIN pg_attribute AS a ON a.attrelid = c.oid
146
+ WHERE c.relname = ?
147
+ AND a.attname = ?;`,
148
+ [tableName, columnName],
149
+ );
150
+
151
+ return resultPg.length > 0
152
+ ? (this.columnComment[`${tableName}.${columnName}`] =
153
+ resultPg[0].column_comment)
154
+ : '';
155
+
156
+ case Database.MYSQL:
157
+ const resultMysql = await this.query(
158
+ `SELECT COLUMN_NAME, COLUMN_COMMENT
159
+ FROM information_schema.COLUMNS
160
+ WHERE TABLE_NAME = ?
161
+ AND COLUMN_NAME = ?;`,
162
+ [tableName, columnName],
163
+ );
164
+
165
+ return resultMysql.length > 0
166
+ ? (this.columnComment[`${tableName}.${columnName}`] =
167
+ resultMysql[0].COLUMN_COMMENT)
168
+ : '';
169
+ }
170
+ }
171
+
172
+ async getTableNameFromForeignKey(
173
+ tableName: string,
174
+ foreignKey: string,
175
+ ): Promise<string> {
176
+ if (this.foreignKeys[`${tableName}.${foreignKey}`]) {
177
+ return this.foreignKeys[`${tableName}.${foreignKey}`];
178
+ }
179
+
180
+ switch (this.type) {
181
+ case Database.POSTGRES:
182
+ const resultPg = await this.query(
183
+ `SELECT
184
+ ccu.table_name
185
+ FROM
186
+ information_schema.table_constraints AS tc
187
+ JOIN information_schema.key_column_usage AS kcu
188
+ ON tc.constraint_name = kcu.constraint_name
189
+ AND tc.table_schema = kcu.table_schema
190
+ JOIN information_schema.constraint_column_usage AS ccu
191
+ ON ccu.constraint_name = tc.constraint_name
192
+ AND ccu.table_schema = tc.table_schema
193
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = ? AND kcu.column_name = ?;`,
194
+ [tableName, foreignKey],
195
+ );
196
+
197
+ if (resultPg.length === 0) {
198
+ throw new Error(
199
+ `Foreign key ${tableName}.${foreignKey} not found in database.`,
200
+ );
201
+ }
202
+
203
+ return (this.foreignKeys[`${tableName}.${foreignKey}`] =
204
+ resultPg[0].table_name);
205
+
206
+ case Database.MYSQL:
207
+ const resultMysql = await this.query(
208
+ `SELECT kcu.REFERENCED_TABLE_NAME as table_name
209
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
210
+ JOIN information_schema.table_constraints AS tc
211
+ ON tc.constraint_name = kcu.constraint_name
212
+ AND tc.table_schema = kcu.table_schema
213
+ WHERE kcu.TABLE_NAME = ? AND kcu.COLUMN_NAME = ? AND tc.CONSTRAINT_TYPE = 'FOREIGN KEY'`,
214
+ [tableName, foreignKey],
215
+ );
216
+
217
+ if (resultMysql.length === 0) {
218
+ throw new Error(
219
+ `Foreign key ${tableName}.${foreignKey} not found in database.`,
220
+ );
221
+ }
222
+
223
+ return (this.foreignKeys[`${tableName}.${foreignKey}`] =
224
+ resultMysql[0].table_name);
225
+ }
226
+ }
227
+
228
+ private shouldHandleReturning(options?: QueryOption): boolean {
229
+ return options?.returning !== undefined;
230
+ }
231
+
232
+ private isReturningSingleField(options?: QueryOption): boolean {
233
+ return (
234
+ options?.returning instanceof Array && options.returning.length === 1
235
+ );
236
+ }
237
+
238
+ private isReturningIdWithoutPrimaryKeys(options?: QueryOption): boolean {
239
+ return options?.returning === 'id' && !options.primaryKeys;
240
+ }
241
+
242
+ private isMissingPrimaryKeys(options?: QueryOption): boolean {
243
+ return !options?.primaryKeys;
244
+ }
245
+
246
+ private hasPrimaryKeys(options?: QueryOption): boolean {
247
+ return typeof options?.primaryKeys === 'string';
248
+ }
249
+
250
+ private hasReturning(options?: QueryOption): boolean {
251
+ return typeof options?.returning === 'string';
252
+ }
253
+
254
+ private formatOptions(options?: QueryOption) {
255
+ if (options && this.shouldHandleReturning(options)) {
256
+ if (this.isReturningSingleField(options)) {
257
+ options.returning = (options.returning as any)[0];
258
+ }
259
+ if (this.isReturningIdWithoutPrimaryKeys(options)) {
260
+ options.primaryKeys = options.returning;
261
+ }
262
+
263
+ if (this.isMissingPrimaryKeys(options)) {
264
+ throw new Error('Primary key is required when using returning.');
265
+ }
266
+
267
+ if (this.hasPrimaryKeys(options)) {
268
+ options.primaryKeys = [options.primaryKeys as string];
269
+ }
270
+ if (this.hasReturning(options)) {
271
+ options.returning = [options.returning as string];
272
+ }
273
+ }
274
+
275
+ return options;
276
+ }
277
+
278
+ private addReturningToQuery(query: string, options?: QueryOption): string {
279
+ if (
280
+ this.type === Database.POSTGRES &&
281
+ this.shouldHandleReturning(options)
282
+ ) {
283
+ return `${query} RETURNING ${(options?.returning as string[]).join(', ')}`;
284
+ }
285
+ return query;
286
+ }
287
+
288
+ private async getResult(query: string, result: any, options?: QueryOption) {
289
+ switch (this.type) {
290
+ case Database.POSTGRES:
291
+ return result.rows;
292
+
293
+ case Database.MYSQL:
294
+ result = result[0] as any[];
295
+
296
+ if (this.shouldHandleReturning(options)) {
297
+ const resultArray = [
298
+ {
299
+ id: (result as any).insertId,
300
+ },
301
+ ];
302
+
303
+ result = resultArray;
304
+
305
+ if (
306
+ (Array.isArray(options?.returning) &&
307
+ options.returning.length > 1) ||
308
+ (options?.returning?.length === 1 &&
309
+ options?.primaryKeys &&
310
+ options?.returning[0] !== options?.primaryKeys[0])
311
+ ) {
312
+ const where = ((options?.primaryKeys as string[]) ?? [])
313
+ .map((pk) => `${pk} = ?`)
314
+ .join(' AND ');
315
+
316
+ const selectReturningQuery = `SELECT ${(options?.returning as string[]).join(', ')} FROM ${this.getTableNameFromQuery(query)} WHERE ${where}`;
317
+ const returningResult = await (
318
+ this.client as unknown as Connection
319
+ ).query(selectReturningQuery, [resultArray[0].id]);
320
+ result = returningResult;
321
+ }
322
+ } else if (result?.insertId) {
323
+ result = [
324
+ {
325
+ id: (result as any).insertId,
326
+ },
327
+ ];
328
+ }
329
+
330
+ return result;
331
+ }
332
+ }
333
+
334
+ async getClient() {
335
+ switch (this.type) {
336
+ case Database.POSTGRES:
337
+ const { Client } = await import('pg');
338
+ this.client = new Client({
339
+ host: this.host,
340
+ user: this.user,
341
+ password: this.password,
342
+ database: this.database,
343
+ port: this.port,
344
+ });
345
+ await this.client.connect();
346
+ return this.client;
347
+
348
+ case Database.MYSQL:
349
+ const mysql = await import('mysql2/promise');
350
+ this.client = await mysql.createConnection({
351
+ host: this.host,
352
+ user: this.user,
353
+ password: this.password,
354
+ database: this.database,
355
+ port: this.port,
356
+ });
357
+ return this.client;
358
+ }
359
+ }
360
+
361
+ async testDatabaseConnection(): Promise<boolean> {
362
+ try {
363
+ switch (this.type) {
364
+ case Database.POSTGRES:
365
+ case Database.MYSQL:
366
+ await this.query('SELECT NOW()');
367
+ break;
368
+ }
369
+ } catch (error) {
370
+ return false;
371
+ }
372
+ return true;
373
+ }
374
+
375
+ async getPrimaryKeys(tableName: string): Promise<string[]> {
376
+ if (this.primaryKeys[tableName]) {
377
+ return this.primaryKeys[tableName];
378
+ }
379
+
380
+ let primaryKeys: string[] = [];
381
+
382
+ switch (this.type) {
383
+ case Database.POSTGRES:
384
+ const resultPg = await this.query(
385
+ `SELECT column_name
386
+ FROM information_schema.table_constraints tc
387
+ JOIN information_schema.key_column_usage kcu
388
+ ON tc.constraint_name = kcu.constraint_name
389
+ WHERE constraint_type = 'PRIMARY KEY'
390
+ AND tc.table_name = ?`,
391
+ [tableName],
392
+ );
393
+
394
+ primaryKeys = resultPg.map((row: any) => row.column_name);
395
+
396
+ if (primaryKeys.length > 0) {
397
+ this.primaryKeys[tableName] = primaryKeys;
398
+ }
399
+
400
+ return primaryKeys;
401
+
402
+ case Database.MYSQL:
403
+ const resultMysql = await this.query(
404
+ `SELECT COLUMN_NAME
405
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
406
+ WHERE TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'`,
407
+ [tableName],
408
+ );
409
+
410
+ primaryKeys = resultMysql.map((row: any) => row.COLUMN_NAME);
411
+
412
+ if (primaryKeys.length > 0) {
413
+ this.primaryKeys[tableName] = primaryKeys;
414
+ }
415
+
416
+ return primaryKeys;
417
+ }
418
+ }
419
+
420
+ async getForeignKeys(tableName: string): Promise<string[]> {
421
+ if (this.foreignKeysByTable[tableName]) {
422
+ return this.foreignKeysByTable[tableName];
423
+ }
424
+
425
+ switch (this.type) {
426
+ case Database.POSTGRES:
427
+ const resultPg = await this.query(
428
+ `SELECT kcu.column_name
429
+ FROM
430
+ information_schema.table_constraints AS tc
431
+ JOIN information_schema.key_column_usage AS kcu
432
+ ON tc.constraint_name = kcu.constraint_name
433
+ AND tc.table_schema = kcu.table_schema
434
+ JOIN information_schema.constraint_column_usage AS ccu
435
+ ON ccu.constraint_name = tc.constraint_name
436
+ AND ccu.table_schema = tc.table_schema
437
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = ?;`,
438
+ [tableName],
439
+ );
440
+ return (this.foreignKeysByTable[tableName] = resultPg.map(
441
+ (row: any) => row.column_name,
442
+ ));
443
+
444
+ case Database.MYSQL:
445
+ const resultMysql = await this.query(
446
+ `SELECT COLUMN_NAME
447
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
448
+ WHERE TABLE_NAME = ? AND CONSTRAINT_NAME != 'PRIMARY'`,
449
+ [tableName],
450
+ );
451
+ return (this.foreignKeysByTable[tableName] = resultMysql.map(
452
+ (row: any) => row.COLUMN_NAME,
453
+ ));
454
+ }
455
+ }
456
+
457
+ async getColumnNameFromRelation(
458
+ tableNameOrigin: string,
459
+ tableNameDestination: string,
460
+ ) {
461
+ if (
462
+ this.columnNameFromRelation[`${tableNameOrigin}.${tableNameDestination}`]
463
+ ) {
464
+ return this.columnNameFromRelation[
465
+ `${tableNameOrigin}.${tableNameDestination}`
466
+ ];
467
+ }
468
+
469
+ switch (this.type) {
470
+ case Database.POSTGRES:
471
+ const resultPg = await this.query(
472
+ `SELECT
473
+ tc.table_name, kcu.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name
474
+ FROM
475
+ information_schema.table_constraints AS tc
476
+ JOIN information_schema.key_column_usage AS kcu
477
+ ON tc.constraint_name = kcu.constraint_name
478
+ AND tc.table_schema = kcu.table_schema
479
+ JOIN information_schema.constraint_column_usage AS ccu
480
+ ON ccu.constraint_name = tc.constraint_name
481
+ AND ccu.table_schema = tc.table_schema
482
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND ccu.table_name = ? AND tc.table_name = ?;`,
483
+ [tableNameOrigin, tableNameDestination],
484
+ );
485
+
486
+ if (!resultPg.length) {
487
+ throw new Error(
488
+ `Foreign key ${tableNameOrigin}.${tableNameDestination} not found in database. [getColumnNameFromRelation]`,
489
+ );
490
+ }
491
+
492
+ return (this.columnNameFromRelation[
493
+ `${tableNameOrigin}.${tableNameDestination}`
494
+ ] = resultPg[0].column_name);
495
+
496
+ case Database.MYSQL:
497
+ const resultMysql = await this.query(
498
+ `SELECT
499
+ TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
500
+ FROM
501
+ INFORMATION_SCHEMA.KEY_COLUMN_USAGE
502
+ WHERE TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME = ? AND TABLE_NAME = ?;`,
503
+ [tableNameOrigin, tableNameDestination],
504
+ );
505
+
506
+ if (!resultMysql.length) {
507
+ throw new Error(
508
+ `Foreign key ${tableNameOrigin}.${tableNameDestination} not found in database. [getColumnNameFromRelation]`,
509
+ );
510
+ }
511
+
512
+ return (this.columnNameFromRelation[
513
+ `${tableNameOrigin}.${tableNameDestination}`
514
+ ] = resultMysql[0].COLUMN_NAME);
515
+
516
+ default:
517
+ throw new Error(`Unsupported database type: ${this.type}`);
518
+ }
519
+ }
520
+
521
+ async getRelation1N(
522
+ tableNameOrigin: string,
523
+ tableNameDestination: string,
524
+ ): Promise<string> {
525
+ if (this.relation1N[`${tableNameOrigin}.${tableNameDestination}`]) {
526
+ return this.relation1N[`${tableNameOrigin}.${tableNameDestination}`];
527
+ }
528
+
529
+ switch (this.type) {
530
+ case Database.POSTGRES:
531
+ const resultPg = await this.query(
532
+ `SELECT
533
+ tc.table_name, kcu.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name
534
+ FROM
535
+ information_schema.table_constraints AS tc
536
+ JOIN information_schema.key_column_usage AS kcu
537
+ ON tc.constraint_name = kcu.constraint_name
538
+ AND tc.table_schema = kcu.table_schema
539
+ JOIN information_schema.constraint_column_usage AS ccu
540
+ ON ccu.constraint_name = tc.constraint_name
541
+ AND ccu.table_schema = tc.table_schema
542
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND ccu.table_name = ? AND tc.table_name = ?;`,
543
+ [tableNameOrigin, tableNameDestination],
544
+ );
545
+
546
+ if (!resultPg.length) {
547
+ throw new Error(
548
+ `Foreign key ${tableNameOrigin}.${tableNameDestination} not found in database. [getRelation1N]`,
549
+ );
550
+ }
551
+
552
+ return (this.relation1N[`${tableNameOrigin}.${tableNameDestination}`] =
553
+ resultPg[0].column_name);
554
+
555
+ case Database.MYSQL:
556
+ const resultMysql = await this.query(
557
+ `SELECT
558
+ TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
559
+ FROM
560
+ INFORMATION_SCHEMA.KEY_COLUMN_USAGE
561
+ WHERE TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME = ? AND TABLE_NAME = ?;`,
562
+ [tableNameOrigin, tableNameDestination],
563
+ );
564
+
565
+ if (!resultMysql.length) {
566
+ throw new Error(
567
+ `Foreign key ${tableNameOrigin}.${tableNameDestination} not found in database. [getRelation1N]`,
568
+ );
569
+ }
570
+
571
+ return (this.relation1N[`${tableNameOrigin}.${tableNameDestination}`] =
572
+ resultMysql[0].COLUMN_NAME);
573
+ }
574
+ }
575
+
576
+ async getRelationN2N(
577
+ tableNameOrigin: string,
578
+ tableNameDestination: string,
579
+ ): Promise<RelationN2NResult> {
580
+ if (this.relationN2N[`${tableNameOrigin}.${tableNameDestination}`]) {
581
+ return this.relationN2N[`${tableNameOrigin}.${tableNameDestination}`];
582
+ }
583
+
584
+ let tableNameIntermediate = '';
585
+ let columnNameOrigin = '';
586
+ let columnNameDestination = '';
587
+ let primaryKeyDestination = '';
588
+
589
+ switch (this.type) {
590
+ case Database.POSTGRES:
591
+ const resultPg1 = await this.query(
592
+ `SELECT
593
+ tc.table_name, kcu.column_name
594
+ FROM
595
+ information_schema.table_constraints AS tc
596
+ JOIN information_schema.key_column_usage AS kcu
597
+ ON tc.constraint_name = kcu.constraint_name
598
+ AND tc.table_schema = kcu.table_schema
599
+ JOIN information_schema.constraint_column_usage AS ccu
600
+ ON ccu.constraint_name = tc.constraint_name
601
+ AND ccu.table_schema = tc.table_schema
602
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND ccu.table_name = ?;`,
603
+ [tableNameOrigin],
604
+ );
605
+
606
+ for (const row of resultPg1) {
607
+ const resultPg2 = await this.query(
608
+ `SELECT
609
+ tc.table_name, kcu.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name
610
+ FROM
611
+ information_schema.table_constraints AS tc
612
+ JOIN information_schema.key_column_usage AS kcu
613
+ ON tc.constraint_name = kcu.constraint_name
614
+ AND tc.table_schema = kcu.table_schema
615
+ JOIN information_schema.constraint_column_usage AS ccu
616
+ ON ccu.constraint_name = tc.constraint_name
617
+ AND ccu.table_schema = tc.table_schema
618
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = ?;`,
619
+ [row['table_name']],
620
+ );
621
+
622
+ for (const row2 of resultPg2) {
623
+ if (row2['foreign_table_name'] === tableNameDestination) {
624
+ tableNameIntermediate = row['table_name'];
625
+ columnNameOrigin = row['column_name'];
626
+ columnNameDestination = row2['column_name'];
627
+ primaryKeyDestination = row2['foreign_column_name'];
628
+ break;
629
+ }
630
+ }
631
+ }
632
+
633
+ return (this.relationN2N[`${tableNameOrigin}.${tableNameDestination}`] =
634
+ {
635
+ tableNameIntermediate,
636
+ columnNameOrigin,
637
+ columnNameDestination,
638
+ primaryKeyDestination,
639
+ });
640
+
641
+ case Database.MYSQL:
642
+ const resultMysql1 = await this.query(
643
+ `SELECT
644
+ kcu.TABLE_NAME,
645
+ kcu.COLUMN_NAME,
646
+ kcu.REFERENCED_TABLE_NAME AS foreign_table_name,
647
+ kcu.REFERENCED_COLUMN_NAME AS foreign_column_name
648
+ FROM
649
+ information_schema.KEY_COLUMN_USAGE AS kcu
650
+ WHERE
651
+ kcu.REFERENCED_TABLE_NAME = ?
652
+ AND kcu.TABLE_SCHEMA = DATABASE();`,
653
+ [tableNameOrigin],
654
+ );
655
+
656
+ for (const row of resultMysql1) {
657
+ const resultMysql2 = await this.query(
658
+ `SELECT
659
+ kcu.TABLE_NAME,
660
+ kcu.COLUMN_NAME,
661
+ kcu.REFERENCED_TABLE_NAME AS foreign_table_name,
662
+ kcu.REFERENCED_COLUMN_NAME AS foreign_column_name
663
+ FROM
664
+ information_schema.KEY_COLUMN_USAGE AS kcu
665
+ WHERE
666
+ kcu.TABLE_NAME = ?
667
+ AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
668
+ AND kcu.TABLE_SCHEMA = DATABASE();`,
669
+ [row['TABLE_NAME']],
670
+ );
671
+
672
+ for (const row2 of resultMysql2) {
673
+ if (row2['foreign_table_name'] === tableNameDestination) {
674
+ tableNameIntermediate = row['TABLE_NAME'];
675
+ columnNameOrigin = row['COLUMN_NAME'];
676
+ columnNameDestination = row2['COLUMN_NAME'];
677
+ primaryKeyDestination = row2['foreign_column_name'];
678
+ break;
679
+ }
680
+ }
681
+ }
682
+
683
+ return (this.relationN2N[`${tableNameOrigin}.${tableNameDestination}`] =
684
+ {
685
+ tableNameIntermediate,
686
+ columnNameOrigin,
687
+ columnNameDestination,
688
+ primaryKeyDestination,
689
+ });
690
+
691
+ default:
692
+ throw new Error(`Unsupported database type: ${this.type}`);
693
+ }
694
+ }
695
+
696
+ static parseQueryValue(value: any) {
697
+ switch (typeof value) {
698
+ case 'number':
699
+ case 'boolean':
700
+ return value;
701
+
702
+ default:
703
+ return `'${value}'`;
704
+ }
705
+ }
706
+
707
+ static objectToWhereClause(obj: any) {
708
+ let whereClause = '';
709
+
710
+ for (const key in obj) {
711
+ if (typeof obj[key] === 'object') {
712
+ whereClause += `${key} ${obj[key].operator} ${AbstractDatabase.parseQueryValue(obj[key].value)}`;
713
+ } else {
714
+ whereClause += `${key} = ${AbstractDatabase.parseQueryValue(obj[key])}`;
715
+ }
716
+ }
717
+
718
+ return whereClause;
719
+ }
720
+
721
+ async transaction(queries: TransactionQueries[]) {
722
+ this.eventEmitter.emit('transaction', { queries });
723
+
724
+ if (!this.client) {
725
+ await this.getClient();
726
+ }
727
+
728
+ const results: any[] = [];
729
+
730
+ for (let i = 0; i < queries.length; i++) {
731
+ queries[i].options = this.formatOptions(queries[i].options);
732
+ queries[i].query = this.addReturningToQuery(
733
+ queries[i].query,
734
+ queries[i].options,
735
+ );
736
+ }
737
+
738
+ try {
739
+ switch (this.type) {
740
+ case Database.POSTGRES:
741
+ await (this.client as Client).query('BEGIN');
742
+ for (const { query, values, options } of queries) {
743
+ const resultPg = await (this.client as Client).query(
744
+ this.replacePlaceholders(query),
745
+ values,
746
+ );
747
+ results.push(this.getResult(query, resultPg, options));
748
+ }
749
+ await (this.client as Client).query('COMMIT');
750
+ break;
751
+
752
+ case Database.MYSQL:
753
+ await (this.client as Connection).beginTransaction();
754
+ for (const { query, values, options } of queries) {
755
+ const resultMySQL = await (
756
+ this.client as unknown as Connection
757
+ ).query(query, values);
758
+ results.push(this.getResult(query, resultMySQL, options));
759
+ }
760
+ await (this.client as Connection).commit();
761
+ break;
762
+ }
763
+ } catch (error) {
764
+ switch (this.type) {
765
+ case Database.POSTGRES:
766
+ await (this.client as Client).query('ROLLBACK');
767
+ break;
768
+
769
+ case Database.MYSQL:
770
+ await (this.client as Connection).rollback();
771
+ break;
772
+ }
773
+ throw error;
774
+ } finally {
775
+ if (this.autoClose) {
776
+ await this.client?.end();
777
+ this.client = null;
778
+ }
779
+ }
780
+
781
+ return results;
782
+ }
783
+
784
+ async query(query: string, values?: any[], options?: QueryOption) {
785
+ this.eventEmitter.emit('query', { query, values, options });
786
+ if (!this.client) {
787
+ await this.getClient();
788
+ }
789
+ let result;
790
+
791
+ options = this.formatOptions(options);
792
+ query = this.addReturningToQuery(query, options);
793
+
794
+ try {
795
+ switch (this.type) {
796
+ case Database.POSTGRES:
797
+ result = await (this.client as Client).query(
798
+ this.replacePlaceholders(query),
799
+ values,
800
+ );
801
+
802
+ break;
803
+
804
+ case Database.MYSQL:
805
+ result = await (this.client as unknown as Connection).query(
806
+ query,
807
+ values,
808
+ );
809
+
810
+ break;
811
+ }
812
+ } catch (error) {
813
+ console.error({
814
+ error,
815
+ query,
816
+ values,
817
+ options,
818
+ });
819
+ this.eventEmitter.emit('error', { error, query, values, options });
820
+ }
821
+
822
+ result = await this.getResult(query, result, options);
823
+
824
+ this.eventEmitter.emit('query', { result });
825
+
826
+ if (this.autoClose) {
827
+ await this.client?.end();
828
+ this.client = null;
829
+ }
830
+
831
+ return result;
832
+ }
833
+ }