db-model-router 1.0.0

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,666 @@
1
+ const { Pool } = require("pg");
2
+ const { jsonSafeParse } = require("../commons/function");
3
+ const sqlTranslator = require("./sql_translator");
4
+
5
+ let pool = null;
6
+ let dateStringsMode = false;
7
+ const WHERE_INVALID = "Invalid filter object";
8
+ const pkCache = {};
9
+
10
+ function sanitizeValue(v) {
11
+ if (v instanceof Date) return v.toISOString().slice(0, 19).replace("T", " ");
12
+ if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(v)) {
13
+ return v
14
+ .replace("T", " ")
15
+ .replace(/[+-]\d{2}:\d{2}$/, "")
16
+ .slice(0, 19);
17
+ }
18
+ return v;
19
+ }
20
+
21
+ const RETRYABLE_ERRORS = [
22
+ "ECONNREFUSED",
23
+ "ECONNRESET",
24
+ "EPIPE",
25
+ "57P01",
26
+ "57P03",
27
+ ];
28
+
29
+ const ERROR_MAP = {
30
+ 23505: "ER_DUP_ENTRY",
31
+ 23503: "ER_NO_REFERENCED_ROW",
32
+ };
33
+
34
+ function mapPgError(err) {
35
+ const code = err.code || "";
36
+ if (ERROR_MAP[code]) {
37
+ err.code = ERROR_MAP[code];
38
+ err.sqlMessage = err.message;
39
+ return err;
40
+ }
41
+ if (!err.sqlMessage) err.sqlMessage = err.message;
42
+ return err;
43
+ }
44
+
45
+ function isRetryable(err) {
46
+ const code = err.code || "";
47
+ return RETRYABLE_ERRORS.includes(code);
48
+ }
49
+
50
+ /**
51
+ * Quote a PostgreSQL identifier to prevent SQL injection.
52
+ * Doubles any embedded double-quotes, then wraps in double-quotes.
53
+ */
54
+ function escapeId(name) {
55
+ return '"' + String(name).replace(/"/g, '""') + '"';
56
+ }
57
+
58
+ function connect(config) {
59
+ dateStringsMode =
60
+ config.dateStrings === true || process.env.DB_DATE_STRINGS === "true";
61
+
62
+ const poolConfig = {
63
+ host: config.host || process.env.DB_HOST,
64
+ port: parseInt(config.port || process.env.DB_PORT || 5432),
65
+ database: config.database || process.env.DB_NAME,
66
+ user: config.user || process.env.DB_USER,
67
+ password: config.password || process.env.DB_PASS,
68
+ max:
69
+ parseInt(config.connectionLimit) ||
70
+ parseInt(process.env.DB_POOL_MAX) ||
71
+ 50,
72
+ idleTimeoutMillis: 30000,
73
+ connectionTimeoutMillis: 5000,
74
+ };
75
+
76
+ pool = new Pool(poolConfig);
77
+
78
+ pool.on("error", (err) => {
79
+ console.error("Unexpected PG pool error:", err.message);
80
+ });
81
+
82
+ return pool;
83
+ }
84
+
85
+ /**
86
+ * Translate MySQL-style ? placeholders to PostgreSQL $1, $2, ...
87
+ * Also resolves ?? identifier placeholders.
88
+ */
89
+ function translatePlaceholders(sql, params) {
90
+ if (!params || params.length === 0) return { sql, params: [] };
91
+
92
+ const paramsCopy = [...params];
93
+
94
+ // Step 1: resolve ?? identifiers
95
+ while (sql.includes("??")) {
96
+ const val = paramsCopy.shift();
97
+ sql = sql.replace("??", String(val));
98
+ }
99
+
100
+ // Step 2: Check for bulk VALUES ? (array-of-arrays)
101
+ const bulkMatch = sql.match(/VALUES\s+\?/i);
102
+ if (
103
+ bulkMatch &&
104
+ paramsCopy.length === 1 &&
105
+ Array.isArray(paramsCopy[0]) &&
106
+ Array.isArray(paramsCopy[0][0])
107
+ ) {
108
+ return { sql, params: paramsCopy[0], isBulk: true };
109
+ }
110
+
111
+ // Step 3: replace ? with $1, $2, ...
112
+ let bindIndex = 0;
113
+ const pgParams = [];
114
+ sql = sql.replace(/\?/g, () => {
115
+ bindIndex++;
116
+ pgParams.push(paramsCopy.shift());
117
+ return "$" + bindIndex;
118
+ });
119
+
120
+ // Step 4: PG can't infer type for to_jsonb($N) — add explicit cast based on JS type
121
+ sql = sql.replace(/to_jsonb\(\$(\d+)\)/g, (m, idx) => {
122
+ const val = pgParams[parseInt(idx) - 1];
123
+ if (typeof val === "number") return `to_jsonb($${idx}::numeric)`;
124
+ if (typeof val === "boolean") return `to_jsonb($${idx}::boolean)`;
125
+ return `to_jsonb($${idx}::text)`;
126
+ });
127
+
128
+ if (bindIndex === 0 && paramsCopy.length > 0) {
129
+ return { sql, params: paramsCopy };
130
+ }
131
+
132
+ return { sql, params: pgParams };
133
+ }
134
+
135
+ function mapResults(result) {
136
+ if (result.rows) {
137
+ return result.rows.map((row) => {
138
+ if (!dateStringsMode) return row;
139
+ const mapped = {};
140
+ for (const [key, val] of Object.entries(row)) {
141
+ mapped[key] =
142
+ dateStringsMode && val instanceof Date
143
+ ? val.toISOString().slice(0, 19).replace("T", " ")
144
+ : val;
145
+ }
146
+ return mapped;
147
+ });
148
+ }
149
+ return {
150
+ affectedRows: result.rowCount || 0,
151
+ insertId: 0,
152
+ };
153
+ }
154
+
155
+ async function executeWithRetry(fn) {
156
+ try {
157
+ return await fn();
158
+ } catch (err) {
159
+ if (isRetryable(err)) {
160
+ return await fn();
161
+ }
162
+ throw mapPgError(err);
163
+ }
164
+ }
165
+
166
+ // Test hook: capture raw MySQL SQL before translation (only in test mode)
167
+ const _sqlCaptureLog = [];
168
+ function getSqlCaptureLog() {
169
+ return _sqlCaptureLog;
170
+ }
171
+
172
+ async function query(sql, parameter = []) {
173
+ if (process.env.NODE_ENV === "test")
174
+ _sqlCaptureLog.push({ raw: sql, params: parameter });
175
+ const translated = sqlTranslator.translate(sql);
176
+ const {
177
+ sql: pgSql,
178
+ params: pgParams,
179
+ isBulk,
180
+ } = translatePlaceholders(translated, parameter);
181
+
182
+ return executeWithRetry(async () => {
183
+ const client = await pool.connect();
184
+ try {
185
+ if (isBulk) {
186
+ // Bulk insert: build multi-row VALUES
187
+ const rows = pgParams;
188
+ if (rows.length === 0) return { affectedRows: 0, insertId: 0 };
189
+ const colCount = rows[0].length;
190
+ let paramIdx = 0;
191
+ const allParams = [];
192
+ const valuesClauses = rows.map((row) => {
193
+ const placeholders = row.map((v) => {
194
+ paramIdx++;
195
+ allParams.push(v);
196
+ return "$" + paramIdx;
197
+ });
198
+ return "(" + placeholders.join(",") + ")";
199
+ });
200
+ const cleanSql = pgSql.replace(
201
+ /VALUES\s+\?/i,
202
+ "VALUES " + valuesClauses.join(","),
203
+ );
204
+ const result = await client.query(cleanSql, allParams);
205
+ return { affectedRows: result.rowCount || 0, insertId: 0 };
206
+ }
207
+
208
+ const result = await client.query(pgSql, pgParams);
209
+
210
+ const isInsert = /^\s*INSERT\s/i.test(pgSql);
211
+ if (isInsert) {
212
+ // If RETURNING was used, extract the id
213
+ if (result.rows && result.rows.length > 0) {
214
+ const firstRow = result.rows[0];
215
+ const id = firstRow.id || firstRow[Object.keys(firstRow)[0]] || 0;
216
+ return { affectedRows: result.rowCount || 0, insertId: id };
217
+ }
218
+ return { affectedRows: result.rowCount || 0, insertId: 0 };
219
+ }
220
+
221
+ const isUpdate = /^\s*(UPDATE|DELETE|MERGE)\s/i.test(pgSql);
222
+ if (isUpdate) {
223
+ return { affectedRows: result.rowCount || 0, insertId: 0 };
224
+ }
225
+
226
+ return mapResults(result);
227
+ } finally {
228
+ client.release();
229
+ }
230
+ });
231
+ }
232
+
233
+ // WHERE builder
234
+ function where(filter, safeDelete = null) {
235
+ if (filter !== null && filter !== "" && !Array.isArray(filter)) {
236
+ return null;
237
+ }
238
+ try {
239
+ if (
240
+ filter === null ||
241
+ filter === "" ||
242
+ filter.length === 0 ||
243
+ (Array.isArray(filter[0]) && filter[0].length === 0) ||
244
+ (Array.isArray(filter[0]) &&
245
+ Array.isArray(filter[0][0]) &&
246
+ filter[0][0].length === 0)
247
+ ) {
248
+ if (safeDelete === null) {
249
+ return { query: "", value: [] };
250
+ } else {
251
+ filter = [[]];
252
+ }
253
+ }
254
+ } catch (err) {
255
+ return null;
256
+ }
257
+
258
+ if (safeDelete !== null) {
259
+ for (const filterItem of filter) {
260
+ filterItem.push([safeDelete, "=", 0]);
261
+ }
262
+ }
263
+
264
+ const valid_conditionals = [
265
+ "=",
266
+ "like",
267
+ "not like",
268
+ "in",
269
+ "not in",
270
+ "<",
271
+ ">",
272
+ "<=",
273
+ ">=",
274
+ "!=",
275
+ ];
276
+ let conditionOr = [];
277
+ let value = [];
278
+ let bindIdx = 0;
279
+
280
+ for (const i of filter) {
281
+ let conditionAnd = [];
282
+ for (const j of i) {
283
+ if (!valid_conditionals.includes(j[1])) return null;
284
+ if ((j[1] === "in" || j[1] === "not in") && !Array.isArray(j[2]))
285
+ return null;
286
+
287
+ if (j[1] === "in" || j[1] === "not in") {
288
+ const placeholders = j[2]
289
+ .map(() => {
290
+ bindIdx++;
291
+ return "$" + bindIdx;
292
+ })
293
+ .join(",");
294
+ conditionAnd.push(`${escapeId(j[0])} ${j[1]} (${placeholders})`);
295
+ value.push(
296
+ ...j[2].map((v) =>
297
+ typeof v === "boolean" ? (v ? 1 : 0) : v === "" ? null : v,
298
+ ),
299
+ );
300
+ } else if (j[1] === "like" || j[1] === "not like") {
301
+ bindIdx++;
302
+ conditionAnd.push(`${escapeId(j[0])} ${j[1]} $${bindIdx}`);
303
+ value.push("%" + j[2] + "%");
304
+ } else {
305
+ bindIdx++;
306
+ conditionAnd.push(`${escapeId(j[0])} ${j[1]} $${bindIdx}`);
307
+ // Coerce empty string to null (PG won't cast '' to bigint/int)
308
+ // Coerce booleans to 0/1 (PG smallint columns store true/false as 1/0)
309
+ let val = j[2];
310
+ if (val === "") val = null;
311
+ else if (typeof val === "boolean") val = val ? 1 : 0;
312
+ value.push(val);
313
+ }
314
+ }
315
+ conditionOr.push(conditionAnd.join(" AND "));
316
+ }
317
+
318
+ return {
319
+ query: "WHERE ((" + conditionOr.join(") OR (") + "))",
320
+ value,
321
+ };
322
+ }
323
+
324
+ function sort_builder(sort) {
325
+ if (!sort || sort.length < 1) return { query: "", value: [] };
326
+ let items = [];
327
+ for (const item of sort) {
328
+ if (item[0] === "-") {
329
+ items.push(escapeId(item.replace("-", "")) + " DESC");
330
+ } else {
331
+ items.push(escapeId(item) + " ASC");
332
+ }
333
+ }
334
+ return { query: "ORDER BY " + items.join(","), value: [] };
335
+ }
336
+
337
+ async function get(table, filter = [], sort = [], safeDelete = null) {
338
+ const whereData = where(filter, safeDelete);
339
+ const sortData = sort_builder(sort);
340
+ if (whereData == null) throw new Error(WHERE_INVALID);
341
+
342
+ const sql = `SELECT * FROM ${escapeId(table)} ${whereData.query} ${sortData.query}`;
343
+ const rows = await query(`/* PG_NATIVE */ ${sql}`, whereData.value);
344
+ const count = await qcount(table, filter, safeDelete);
345
+ return { data: jsonSafeParse(rows), count };
346
+ }
347
+
348
+ async function list(
349
+ table,
350
+ filter = [],
351
+ sort = [],
352
+ safeDelete = null,
353
+ page = 0,
354
+ limit = 30,
355
+ ) {
356
+ const whereData = where(filter, safeDelete);
357
+ const sortData = sort_builder(sort);
358
+ if (whereData == null) throw new Error(WHERE_INVALID);
359
+
360
+ const offset = page * limit;
361
+ const sql = `SELECT * FROM ${escapeId(table)} ${whereData.query} ${sortData.query} LIMIT ${limit} OFFSET ${offset}`;
362
+ const rows = await query(`/* PG_NATIVE */ ${sql}`, whereData.value);
363
+ const count = await qcount(table, filter, safeDelete);
364
+ return { data: jsonSafeParse(rows), count };
365
+ }
366
+
367
+ async function qcount(table, filter, safeDelete = null) {
368
+ const whereData = where(filter, safeDelete);
369
+ if (whereData == null) return 0;
370
+ const sql = `SELECT count(*) AS number FROM ${escapeId(table)} ${whereData.query}`;
371
+ try {
372
+ const rows = await query(`/* PG_NATIVE */ ${sql}`, whereData.value);
373
+ return parseInt(rows[0]?.number) || 0;
374
+ } catch {
375
+ return 0;
376
+ }
377
+ }
378
+
379
+ async function remove(table, filter, safeDelete = null) {
380
+ const whereData = where(filter);
381
+ if (whereData == null) throw new Error(WHERE_INVALID);
382
+ if (whereData.value.length < 1) {
383
+ throw new Error("unable to remove as there are no filter attributes");
384
+ }
385
+
386
+ let sql;
387
+ let params;
388
+ if (safeDelete != null) {
389
+ sql = `UPDATE ${escapeId(table)} SET ${escapeId(safeDelete)} = 1 ${whereData.query}`;
390
+ params = whereData.value;
391
+ } else {
392
+ sql = `DELETE FROM ${escapeId(table)} ${whereData.query}`;
393
+ params = whereData.value;
394
+ }
395
+
396
+ const result = await query(`/* PG_NATIVE */ ${sql}`, params);
397
+ const rows = result.affectedRows || 0;
398
+ return { message: rows + " " + table + (rows > 1 ? "s" : "") + " removed" };
399
+ }
400
+
401
+ function namify(text) {
402
+ return text
403
+ .replace("_", " ")
404
+ .replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase());
405
+ }
406
+
407
+ async function getPkColumn(table) {
408
+ if (pkCache[table] !== undefined) return pkCache[table];
409
+ try {
410
+ const client = await pool.connect();
411
+ try {
412
+ const r = await client.query(
413
+ `SELECT a.attname AS column_name
414
+ FROM pg_index i
415
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
416
+ WHERE i.indrelid = $1::regclass AND i.indisprimary
417
+ LIMIT 1`,
418
+ [table],
419
+ );
420
+ pkCache[table] = r.rows.length > 0 ? r.rows[0].column_name : null;
421
+ } finally {
422
+ client.release();
423
+ }
424
+ } catch (e) {
425
+ pkCache[table] = null;
426
+ }
427
+ return pkCache[table];
428
+ }
429
+
430
+ async function insert(table, data, uniqueKeys = []) {
431
+ let array = Array.isArray(data) ? data : [data];
432
+ const total = array.length;
433
+ const columns = Object.keys(array[0]);
434
+
435
+ if (uniqueKeys.length > 0) {
436
+ return await _insertOnConflict(table, array, columns, uniqueKeys, total);
437
+ }
438
+
439
+ if (total === 1) {
440
+ const pk = await getPkColumn(table);
441
+ const row = array[0];
442
+ const vals = columns.map((c) => sanitizeValue(row[c]));
443
+ const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
444
+ let sql = `INSERT INTO ${escapeId(table)} (${columns.map(escapeId).join(",")}) VALUES (${placeholders})`;
445
+ if (pk) sql += ` RETURNING ${escapeId(pk)}`;
446
+
447
+ const client = await pool.connect();
448
+ try {
449
+ const result = await client.query(sql, vals);
450
+ const insertId =
451
+ pk && result.rows && result.rows[0] ? result.rows[0][pk] : 0;
452
+ return {
453
+ rows: 1,
454
+ message: `1 ${namify(table)} is saved`,
455
+ type: "success",
456
+ id: insertId,
457
+ };
458
+ } catch (e) {
459
+ throw mapPgError(e);
460
+ } finally {
461
+ client.release();
462
+ }
463
+ }
464
+
465
+ // Bulk insert via multi-row VALUES
466
+ const client = await pool.connect();
467
+ try {
468
+ let paramIdx = 0;
469
+ const allParams = [];
470
+ const valuesClauses = array.map((row) => {
471
+ const placeholders = columns.map((c) => {
472
+ paramIdx++;
473
+ allParams.push(sanitizeValue(row[c]));
474
+ return "$" + paramIdx;
475
+ });
476
+ return "(" + placeholders.join(",") + ")";
477
+ });
478
+ const sql = `INSERT INTO ${escapeId(table)} (${columns.map(escapeId).join(",")}) VALUES ${valuesClauses.join(",")}`;
479
+ await client.query(sql, allParams);
480
+ } catch (e) {
481
+ throw mapPgError(e);
482
+ } finally {
483
+ client.release();
484
+ }
485
+
486
+ return {
487
+ rows: total,
488
+ message:
489
+ (total === 1
490
+ ? `1 ${namify(table)} is `
491
+ : `${total} ${namify(table)}s are `) + "saved",
492
+ type: "success",
493
+ id: 0,
494
+ };
495
+ }
496
+
497
+ async function _insertOnConflict(table, array, columns, uniqueKeys, total) {
498
+ let lastId = 0;
499
+ const pk = await getPkColumn(table);
500
+ const conflictCols = uniqueKeys.map(escapeId).join(",");
501
+ const colList = columns.map(escapeId).join(",");
502
+
503
+ const client = await pool.connect();
504
+ try {
505
+ await client.query("BEGIN");
506
+
507
+ for (const row of array) {
508
+ const vals = columns.map((c) => sanitizeValue(row[c]));
509
+ const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
510
+ let sql = `INSERT INTO ${escapeId(table)} (${colList}) VALUES (${placeholders}) ON CONFLICT (${conflictCols}) DO NOTHING`;
511
+ if (pk) sql += ` RETURNING ${escapeId(pk)}`;
512
+
513
+ const result = await client.query(sql, vals);
514
+ if (result.rows && result.rows.length > 0 && pk) {
515
+ lastId = result.rows[0][pk] || 0;
516
+ } else if (total === 1 && pk) {
517
+ // Row already existed, fetch its PK
518
+ const whereClauses = uniqueKeys
519
+ .map((k, i) => `${escapeId(k)} = ${i + 1}`)
520
+ .join(" AND ");
521
+ const whereVals = uniqueKeys.map((k) => row[k]);
522
+ const fetched = await client.query(
523
+ `SELECT ${escapeId(pk)} FROM ${escapeId(table)} WHERE ${whereClauses}`,
524
+ whereVals,
525
+ );
526
+ if (fetched.rows.length > 0) lastId = fetched.rows[0][pk] || 0;
527
+ }
528
+ }
529
+
530
+ await client.query("COMMIT");
531
+ } catch (e) {
532
+ await client.query("ROLLBACK").catch(() => {});
533
+ throw mapPgError(e);
534
+ } finally {
535
+ client.release();
536
+ }
537
+
538
+ return {
539
+ rows: total,
540
+ message:
541
+ (total === 1
542
+ ? `1 ${namify(table)} is `
543
+ : `${total} ${namify(table)}s are `) + "saved",
544
+ type: "success",
545
+ id: lastId,
546
+ };
547
+ }
548
+
549
+ async function upsert(table, data, uniqueKeys = []) {
550
+ if (!uniqueKeys || !uniqueKeys.length) {
551
+ return insert(table, data);
552
+ }
553
+ let array = Array.isArray(data) ? data : [data];
554
+ const total = array.length;
555
+ const columns = Object.keys(array[0]);
556
+
557
+ // If unique keys aren't all in the data, fall back to UPDATE using PK or available unique key
558
+ const missingKeys = uniqueKeys.filter((k) => !columns.includes(k));
559
+ if (missingKeys.length > 0) {
560
+ const pk = await getPkColumn(table);
561
+ const keyCol =
562
+ pk && columns.includes(pk)
563
+ ? pk
564
+ : columns.find((c) => uniqueKeys.includes(c));
565
+ if (keyCol) {
566
+ let lastId = 0;
567
+ for (const row of array) {
568
+ const updateCols = columns.filter((c) => c !== keyCol);
569
+ if (updateCols.length === 0) continue;
570
+ const setClause = updateCols
571
+ .map((c, i) => `${escapeId(c)} = $${i + 1}`)
572
+ .join(", ");
573
+ const vals = [
574
+ ...updateCols.map((c) => sanitizeValue(row[c])),
575
+ row[keyCol],
576
+ ];
577
+ const sql = `UPDATE ${escapeId(table)} SET ${setClause} WHERE ${escapeId(keyCol)} = $${updateCols.length + 1}`;
578
+ await query(`/* PG_NATIVE */ ${sql}`, vals);
579
+ if (row[keyCol]) lastId = row[keyCol];
580
+ }
581
+ const response = {
582
+ rows: total,
583
+ message:
584
+ (total === 1
585
+ ? `1 ${namify(table)} is `
586
+ : `${total} ${namify(table)}s are `) + "saved",
587
+ type: "success",
588
+ };
589
+ if (total === 1) response.id = lastId;
590
+ return response;
591
+ }
592
+ }
593
+
594
+ const nonUniqueColumns = columns.filter((c) => !uniqueKeys.includes(c));
595
+ const pk = await getPkColumn(table);
596
+ let lastId = 0;
597
+
598
+ const client = await pool.connect();
599
+ try {
600
+ await client.query("BEGIN");
601
+
602
+ for (const row of array) {
603
+ const vals = columns.map((c) => sanitizeValue(row[c]));
604
+ const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
605
+ const conflictCols = uniqueKeys.map(escapeId).join(",");
606
+ const updateSetSql = nonUniqueColumns
607
+ .map((c) => `${escapeId(c)} = EXCLUDED.${escapeId(c)}`)
608
+ .join(", ");
609
+
610
+ let sql = `INSERT INTO ${escapeId(table)} (${columns.map(escapeId).join(",")}) VALUES (${placeholders}) ON CONFLICT (${conflictCols})`;
611
+ if (updateSetSql) {
612
+ sql += ` DO UPDATE SET ${updateSetSql}`;
613
+ } else {
614
+ sql += ` DO NOTHING`;
615
+ }
616
+ if (pk) sql += ` RETURNING ${escapeId(pk)}`;
617
+
618
+ const result = await client.query(sql, vals);
619
+ if (result.rows && result.rows.length > 0 && pk) {
620
+ lastId = result.rows[0][pk] || 0;
621
+ }
622
+ }
623
+
624
+ await client.query("COMMIT");
625
+ } catch (e) {
626
+ await client.query("ROLLBACK").catch(() => {});
627
+ throw mapPgError(e);
628
+ } finally {
629
+ client.release();
630
+ }
631
+
632
+ const response = {
633
+ rows: total,
634
+ message:
635
+ (total === 1
636
+ ? `1 ${namify(table)} is `
637
+ : `${total} ${namify(table)}s are `) + "saved",
638
+ type: "success",
639
+ };
640
+ if (total === 1) response.id = lastId;
641
+ return response;
642
+ }
643
+
644
+ async function disconnect() {
645
+ if (pool) {
646
+ await pool.end();
647
+ pool = null;
648
+ }
649
+ }
650
+
651
+ module.exports = {
652
+ connect,
653
+ get,
654
+ list,
655
+ where,
656
+ query,
657
+ qcount,
658
+ remove,
659
+ upsert,
660
+ change: upsert,
661
+ insert,
662
+ disconnect,
663
+ close: disconnect,
664
+ pool: null,
665
+ getSqlCaptureLog,
666
+ };