@hedhog/pagination 0.0.16 → 0.0.18

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.
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
+ }