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.
- package/.env +7 -0
- package/LICENSE +201 -0
- package/README.md +505 -0
- package/docker-compose.yml +141 -0
- package/docs/README.md +208 -0
- package/docs/SKILL.md +202 -0
- package/docs/adapters/cockroachdb.md +49 -0
- package/docs/adapters/dynamodb.md +53 -0
- package/docs/adapters/mongodb.md +56 -0
- package/docs/adapters/mssql.md +55 -0
- package/docs/adapters/oracle.md +52 -0
- package/docs/adapters/postgres.md +50 -0
- package/docs/adapters/redis.md +53 -0
- package/docs/adapters/sqlite3.md +43 -0
- package/package.json +109 -0
- package/src/cli/generate-app.js +359 -0
- package/src/cli/generate-model.js +760 -0
- package/src/cli/generate-openapi.js +237 -0
- package/src/cli/generate-route.js +346 -0
- package/src/cockroachdb/db.js +563 -0
- package/src/commons/function.js +165 -0
- package/src/commons/model.js +444 -0
- package/src/commons/route.js +214 -0
- package/src/commons/validator.js +172 -0
- package/src/dynamodb/db.js +552 -0
- package/src/index.js +57 -0
- package/src/mongodb/db.js +381 -0
- package/src/mssql/db.js +461 -0
- package/src/mysql/db.js +527 -0
- package/src/oracle/db.js +855 -0
- package/src/oracle/sql_translator.js +406 -0
- package/src/postgres/db.js +666 -0
- package/src/postgres/ddl_translator.js +69 -0
- package/src/postgres/sql_translator.js +396 -0
- package/src/redis/db.js +448 -0
- package/src/serve.js +90 -0
- package/src/sqlite3/db.js +346 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
const { Pool, types } = require("pg");
|
|
2
|
+
const { jsonSafeParse } = require("../commons/function");
|
|
3
|
+
|
|
4
|
+
// CockroachDB SERIAL uses unique_rowid() which generates INT8 values
|
|
5
|
+
// that often exceed Number.MAX_SAFE_INTEGER. Keep them as strings to
|
|
6
|
+
// preserve precision; convert small values to numbers for convenience.
|
|
7
|
+
types.setTypeParser(20, (val) => {
|
|
8
|
+
if (val === null) return null;
|
|
9
|
+
const n = Number(val);
|
|
10
|
+
return Number.isSafeInteger(n) ? n : val;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
let pool = null;
|
|
14
|
+
let dateStringsMode = false;
|
|
15
|
+
const WHERE_INVALID = "Invalid filter object";
|
|
16
|
+
const pkCache = {};
|
|
17
|
+
|
|
18
|
+
function sanitizeValue(v) {
|
|
19
|
+
if (v instanceof Date) return v.toISOString().slice(0, 19).replace("T", " ");
|
|
20
|
+
if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(v)) {
|
|
21
|
+
return v
|
|
22
|
+
.replace("T", " ")
|
|
23
|
+
.replace(/[+-]\d{2}:\d{2}$/, "")
|
|
24
|
+
.slice(0, 19);
|
|
25
|
+
}
|
|
26
|
+
return v;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const RETRYABLE_ERRORS = [
|
|
30
|
+
"ECONNREFUSED",
|
|
31
|
+
"ECONNRESET",
|
|
32
|
+
"EPIPE",
|
|
33
|
+
"57P01",
|
|
34
|
+
"57P03",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const ERROR_MAP = {
|
|
38
|
+
23505: "ER_DUP_ENTRY",
|
|
39
|
+
23503: "ER_NO_REFERENCED_ROW",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function mapPgError(err) {
|
|
43
|
+
const code = err.code || "";
|
|
44
|
+
if (ERROR_MAP[code]) {
|
|
45
|
+
err.code = ERROR_MAP[code];
|
|
46
|
+
err.sqlMessage = err.message;
|
|
47
|
+
return err;
|
|
48
|
+
}
|
|
49
|
+
if (!err.sqlMessage) err.sqlMessage = err.message;
|
|
50
|
+
return err;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isRetryable(err) {
|
|
54
|
+
const code = err.code || "";
|
|
55
|
+
return RETRYABLE_ERRORS.includes(code);
|
|
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 || 26257),
|
|
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 CockroachDB pool error:", err.message);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return pool;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mapResults(result) {
|
|
86
|
+
if (result.rows) {
|
|
87
|
+
return result.rows.map((row) => {
|
|
88
|
+
if (!dateStringsMode) return row;
|
|
89
|
+
const mapped = {};
|
|
90
|
+
for (const [key, val] of Object.entries(row)) {
|
|
91
|
+
mapped[key] =
|
|
92
|
+
dateStringsMode && val instanceof Date
|
|
93
|
+
? val.toISOString().slice(0, 19).replace("T", " ")
|
|
94
|
+
: val;
|
|
95
|
+
}
|
|
96
|
+
return mapped;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
affectedRows: result.rowCount || 0,
|
|
101
|
+
insertId: 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function executeWithRetry(fn) {
|
|
106
|
+
try {
|
|
107
|
+
return await fn();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (isRetryable(err)) {
|
|
110
|
+
return await fn();
|
|
111
|
+
}
|
|
112
|
+
throw mapPgError(err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function query(sql, parameter = []) {
|
|
117
|
+
return executeWithRetry(async () => {
|
|
118
|
+
const client = await pool.connect();
|
|
119
|
+
try {
|
|
120
|
+
const result = await client.query(sql, parameter);
|
|
121
|
+
|
|
122
|
+
const isInsert = /^\s*INSERT\s/i.test(sql);
|
|
123
|
+
if (isInsert) {
|
|
124
|
+
if (result.rows && result.rows.length > 0) {
|
|
125
|
+
const firstRow = result.rows[0];
|
|
126
|
+
const id = firstRow.id || firstRow[Object.keys(firstRow)[0]] || 0;
|
|
127
|
+
return { affectedRows: result.rowCount || 0, insertId: id };
|
|
128
|
+
}
|
|
129
|
+
return { affectedRows: result.rowCount || 0, insertId: 0 };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isUpdate = /^\s*(UPDATE|DELETE|MERGE)\s/i.test(sql);
|
|
133
|
+
if (isUpdate) {
|
|
134
|
+
return { affectedRows: result.rowCount || 0, insertId: 0 };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return mapResults(result);
|
|
138
|
+
} finally {
|
|
139
|
+
client.release();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// WHERE builder — generates PostgreSQL-compatible WHERE clauses with $N bind params
|
|
145
|
+
function where(filter, safeDelete = null) {
|
|
146
|
+
if (filter !== null && filter !== "" && !Array.isArray(filter)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
if (
|
|
151
|
+
filter === null ||
|
|
152
|
+
filter === "" ||
|
|
153
|
+
filter.length === 0 ||
|
|
154
|
+
(Array.isArray(filter[0]) && filter[0].length === 0) ||
|
|
155
|
+
(Array.isArray(filter[0]) &&
|
|
156
|
+
Array.isArray(filter[0][0]) &&
|
|
157
|
+
filter[0][0].length === 0)
|
|
158
|
+
) {
|
|
159
|
+
if (safeDelete === null) {
|
|
160
|
+
return { query: "", value: [] };
|
|
161
|
+
} else {
|
|
162
|
+
filter = [[]];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (safeDelete !== null) {
|
|
170
|
+
for (const filterItem of filter) {
|
|
171
|
+
filterItem.push([safeDelete, "=", 0]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const valid_conditionals = [
|
|
176
|
+
"=",
|
|
177
|
+
"like",
|
|
178
|
+
"not like",
|
|
179
|
+
"in",
|
|
180
|
+
"not in",
|
|
181
|
+
"<",
|
|
182
|
+
">",
|
|
183
|
+
"<=",
|
|
184
|
+
">=",
|
|
185
|
+
"!=",
|
|
186
|
+
];
|
|
187
|
+
let conditionOr = [];
|
|
188
|
+
let value = [];
|
|
189
|
+
let bindIdx = 0;
|
|
190
|
+
|
|
191
|
+
for (const i of filter) {
|
|
192
|
+
let conditionAnd = [];
|
|
193
|
+
for (const j of i) {
|
|
194
|
+
if (!valid_conditionals.includes(j[1])) return null;
|
|
195
|
+
if ((j[1] === "in" || j[1] === "not in") && !Array.isArray(j[2]))
|
|
196
|
+
return null;
|
|
197
|
+
|
|
198
|
+
if (j[1] === "in" || j[1] === "not in") {
|
|
199
|
+
const placeholders = j[2]
|
|
200
|
+
.map(() => {
|
|
201
|
+
bindIdx++;
|
|
202
|
+
return "$" + bindIdx;
|
|
203
|
+
})
|
|
204
|
+
.join(",");
|
|
205
|
+
conditionAnd.push(`${j[0]} ${j[1]} (${placeholders})`);
|
|
206
|
+
value.push(
|
|
207
|
+
...j[2].map((v) =>
|
|
208
|
+
typeof v === "boolean" ? (v ? 1 : 0) : v === "" ? null : v,
|
|
209
|
+
),
|
|
210
|
+
);
|
|
211
|
+
} else if (j[1] === "like" || j[1] === "not like") {
|
|
212
|
+
bindIdx++;
|
|
213
|
+
conditionAnd.push(`${j[0]} ${j[1]} $${bindIdx}`);
|
|
214
|
+
value.push("%" + j[2] + "%");
|
|
215
|
+
} else {
|
|
216
|
+
bindIdx++;
|
|
217
|
+
conditionAnd.push(`${j[0]} ${j[1]} $${bindIdx}`);
|
|
218
|
+
let val = j[2];
|
|
219
|
+
if (val === "") val = null;
|
|
220
|
+
else if (typeof val === "boolean") val = val ? 1 : 0;
|
|
221
|
+
value.push(val);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
conditionOr.push(conditionAnd.join(" AND "));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
query: "WHERE ((" + conditionOr.join(") OR (") + "))",
|
|
229
|
+
value,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function sort_builder(sort) {
|
|
234
|
+
if (!sort || sort.length < 1) return { query: "", value: [] };
|
|
235
|
+
let items = [];
|
|
236
|
+
for (const item of sort) {
|
|
237
|
+
if (item[0] === "-") {
|
|
238
|
+
items.push(item.replace("-", "") + " DESC");
|
|
239
|
+
} else {
|
|
240
|
+
items.push(item + " ASC");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { query: "ORDER BY " + items.join(","), value: [] };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function get(table, filter = [], sort = [], safeDelete = null) {
|
|
247
|
+
const whereData = where(filter, safeDelete);
|
|
248
|
+
const sortData = sort_builder(sort);
|
|
249
|
+
if (whereData == null) throw new Error(WHERE_INVALID);
|
|
250
|
+
|
|
251
|
+
const sql = `SELECT * FROM ${table} ${whereData.query} ${sortData.query}`;
|
|
252
|
+
const rows = await query(sql, whereData.value);
|
|
253
|
+
const count = await qcount(table, filter, safeDelete);
|
|
254
|
+
return { data: jsonSafeParse(rows), count };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function list(
|
|
258
|
+
table,
|
|
259
|
+
filter = [],
|
|
260
|
+
sort = [],
|
|
261
|
+
safeDelete = null,
|
|
262
|
+
page = 0,
|
|
263
|
+
limit = 30,
|
|
264
|
+
) {
|
|
265
|
+
const whereData = where(filter, safeDelete);
|
|
266
|
+
const sortData = sort_builder(sort);
|
|
267
|
+
if (whereData == null) throw new Error(WHERE_INVALID);
|
|
268
|
+
|
|
269
|
+
const offset = page * limit;
|
|
270
|
+
const sql = `SELECT * FROM ${table} ${whereData.query} ${sortData.query} LIMIT ${limit} OFFSET ${offset}`;
|
|
271
|
+
const rows = await query(sql, whereData.value);
|
|
272
|
+
const count = await qcount(table, filter, safeDelete);
|
|
273
|
+
return { data: jsonSafeParse(rows), count };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function qcount(table, filter, safeDelete = null) {
|
|
277
|
+
const whereData = where(filter, safeDelete);
|
|
278
|
+
if (whereData == null) return 0;
|
|
279
|
+
const sql = `SELECT count(*) AS number FROM ${table} ${whereData.query}`;
|
|
280
|
+
try {
|
|
281
|
+
const rows = await query(sql, whereData.value);
|
|
282
|
+
return parseInt(rows[0]?.number) || 0;
|
|
283
|
+
} catch {
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function remove(table, filter, safeDelete = null) {
|
|
289
|
+
const whereData = where(filter);
|
|
290
|
+
if (whereData == null) throw new Error(WHERE_INVALID);
|
|
291
|
+
if (whereData.value.length < 1) {
|
|
292
|
+
throw new Error("unable to remove as there are no filter attributes");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let sql;
|
|
296
|
+
let params;
|
|
297
|
+
if (safeDelete != null) {
|
|
298
|
+
sql = `UPDATE ${table} SET ${safeDelete} = 1 ${whereData.query}`;
|
|
299
|
+
params = whereData.value;
|
|
300
|
+
} else {
|
|
301
|
+
sql = `DELETE FROM ${table} ${whereData.query}`;
|
|
302
|
+
params = whereData.value;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const result = await query(sql, params);
|
|
306
|
+
const rows = result.affectedRows || 0;
|
|
307
|
+
return { message: rows + " " + table + (rows > 1 ? "s" : "") + " removed" };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function namify(text) {
|
|
311
|
+
return text
|
|
312
|
+
.replace("_", " ")
|
|
313
|
+
.replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase());
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function getPkColumn(table) {
|
|
317
|
+
if (pkCache[table] !== undefined) return pkCache[table];
|
|
318
|
+
try {
|
|
319
|
+
const client = await pool.connect();
|
|
320
|
+
try {
|
|
321
|
+
const r = await client.query(
|
|
322
|
+
`SELECT a.attname AS column_name
|
|
323
|
+
FROM pg_index i
|
|
324
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
325
|
+
WHERE i.indrelid = $1::regclass AND i.indisprimary
|
|
326
|
+
LIMIT 1`,
|
|
327
|
+
[table],
|
|
328
|
+
);
|
|
329
|
+
pkCache[table] = r.rows.length > 0 ? r.rows[0].column_name : null;
|
|
330
|
+
} finally {
|
|
331
|
+
client.release();
|
|
332
|
+
}
|
|
333
|
+
} catch (e) {
|
|
334
|
+
pkCache[table] = null;
|
|
335
|
+
}
|
|
336
|
+
return pkCache[table];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function insert(table, data, uniqueKeys = []) {
|
|
340
|
+
let array = Array.isArray(data) ? data : [data];
|
|
341
|
+
const total = array.length;
|
|
342
|
+
const columns = Object.keys(array[0]);
|
|
343
|
+
|
|
344
|
+
if (uniqueKeys.length > 0) {
|
|
345
|
+
return await _insertOnConflict(table, array, columns, uniqueKeys, total);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (total === 1) {
|
|
349
|
+
const pk = await getPkColumn(table);
|
|
350
|
+
const row = array[0];
|
|
351
|
+
const vals = columns.map((c) => sanitizeValue(row[c]));
|
|
352
|
+
const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
|
|
353
|
+
let sql = `INSERT INTO ${table} (${columns.join(",")}) VALUES (${placeholders})`;
|
|
354
|
+
if (pk) sql += ` RETURNING ${pk}`;
|
|
355
|
+
|
|
356
|
+
const client = await pool.connect();
|
|
357
|
+
try {
|
|
358
|
+
const result = await client.query(sql, vals);
|
|
359
|
+
const insertId =
|
|
360
|
+
pk && result.rows && result.rows[0] ? result.rows[0][pk] : 0;
|
|
361
|
+
return {
|
|
362
|
+
rows: 1,
|
|
363
|
+
message: `1 ${namify(table)} is saved`,
|
|
364
|
+
type: "success",
|
|
365
|
+
id: insertId,
|
|
366
|
+
};
|
|
367
|
+
} catch (e) {
|
|
368
|
+
throw mapPgError(e);
|
|
369
|
+
} finally {
|
|
370
|
+
client.release();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Bulk insert via multi-row VALUES
|
|
375
|
+
const client = await pool.connect();
|
|
376
|
+
try {
|
|
377
|
+
let paramIdx = 0;
|
|
378
|
+
const allParams = [];
|
|
379
|
+
const valuesClauses = array.map((row) => {
|
|
380
|
+
const placeholders = columns.map((c) => {
|
|
381
|
+
paramIdx++;
|
|
382
|
+
allParams.push(sanitizeValue(row[c]));
|
|
383
|
+
return "$" + paramIdx;
|
|
384
|
+
});
|
|
385
|
+
return "(" + placeholders.join(",") + ")";
|
|
386
|
+
});
|
|
387
|
+
const sql = `INSERT INTO ${table} (${columns.join(",")}) VALUES ${valuesClauses.join(",")}`;
|
|
388
|
+
await client.query(sql, allParams);
|
|
389
|
+
} catch (e) {
|
|
390
|
+
throw mapPgError(e);
|
|
391
|
+
} finally {
|
|
392
|
+
client.release();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
rows: total,
|
|
397
|
+
message:
|
|
398
|
+
(total === 1
|
|
399
|
+
? `1 ${namify(table)} is `
|
|
400
|
+
: `${total} ${namify(table)}s are `) + "saved",
|
|
401
|
+
type: "success",
|
|
402
|
+
id: 0,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function _insertOnConflict(table, array, columns, uniqueKeys, total) {
|
|
407
|
+
let lastId = 0;
|
|
408
|
+
const pk = await getPkColumn(table);
|
|
409
|
+
const conflictCols = uniqueKeys.join(",");
|
|
410
|
+
|
|
411
|
+
for (const row of array) {
|
|
412
|
+
const vals = columns.map((c) => sanitizeValue(row[c]));
|
|
413
|
+
const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
|
|
414
|
+
let sql = `INSERT INTO ${table} (${columns.join(",")}) VALUES (${placeholders}) ON CONFLICT (${conflictCols}) DO NOTHING`;
|
|
415
|
+
if (pk) sql += ` RETURNING ${pk}`;
|
|
416
|
+
|
|
417
|
+
const client = await pool.connect();
|
|
418
|
+
try {
|
|
419
|
+
const result = await client.query(sql, vals);
|
|
420
|
+
if (result.rows && result.rows.length > 0 && pk) {
|
|
421
|
+
lastId = result.rows[0][pk] || 0;
|
|
422
|
+
} else if (total === 1 && pk) {
|
|
423
|
+
// Row already existed, fetch its PK
|
|
424
|
+
const whereClauses = uniqueKeys
|
|
425
|
+
.map((k, i) => `${k} = $${i + 1}`)
|
|
426
|
+
.join(" AND ");
|
|
427
|
+
const whereVals = uniqueKeys.map((k) => row[k]);
|
|
428
|
+
const fetched = await client.query(
|
|
429
|
+
`SELECT ${pk} FROM ${table} WHERE ${whereClauses}`,
|
|
430
|
+
whereVals,
|
|
431
|
+
);
|
|
432
|
+
if (fetched.rows.length > 0) lastId = fetched.rows[0][pk] || 0;
|
|
433
|
+
}
|
|
434
|
+
} catch (e) {
|
|
435
|
+
throw mapPgError(e);
|
|
436
|
+
} finally {
|
|
437
|
+
client.release();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
rows: total,
|
|
443
|
+
message:
|
|
444
|
+
(total === 1
|
|
445
|
+
? `1 ${namify(table)} is `
|
|
446
|
+
: `${total} ${namify(table)}s are `) + "saved",
|
|
447
|
+
type: "success",
|
|
448
|
+
id: lastId,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function upsert(table, data, uniqueKeys = []) {
|
|
453
|
+
if (!uniqueKeys || !uniqueKeys.length) {
|
|
454
|
+
return insert(table, data);
|
|
455
|
+
}
|
|
456
|
+
let array = Array.isArray(data) ? data : [data];
|
|
457
|
+
const total = array.length;
|
|
458
|
+
const columns = Object.keys(array[0]);
|
|
459
|
+
|
|
460
|
+
// If unique keys aren't all in the data, fall back to UPDATE using PK or available unique key
|
|
461
|
+
const missingKeys = uniqueKeys.filter((k) => !columns.includes(k));
|
|
462
|
+
if (missingKeys.length > 0) {
|
|
463
|
+
const pk = await getPkColumn(table);
|
|
464
|
+
const keyCol =
|
|
465
|
+
pk && columns.includes(pk)
|
|
466
|
+
? pk
|
|
467
|
+
: columns.find((c) => uniqueKeys.includes(c));
|
|
468
|
+
if (keyCol) {
|
|
469
|
+
let lastId = 0;
|
|
470
|
+
for (const row of array) {
|
|
471
|
+
const updateCols = columns.filter((c) => c !== keyCol);
|
|
472
|
+
if (updateCols.length === 0) continue;
|
|
473
|
+
const setClause = updateCols
|
|
474
|
+
.map((c, i) => `${c} = $${i + 1}`)
|
|
475
|
+
.join(", ");
|
|
476
|
+
const vals = [
|
|
477
|
+
...updateCols.map((c) => sanitizeValue(row[c])),
|
|
478
|
+
row[keyCol],
|
|
479
|
+
];
|
|
480
|
+
const sql = `UPDATE ${table} SET ${setClause} WHERE ${keyCol} = $${updateCols.length + 1}`;
|
|
481
|
+
await query(sql, vals);
|
|
482
|
+
if (row[keyCol]) lastId = row[keyCol];
|
|
483
|
+
}
|
|
484
|
+
const response = {
|
|
485
|
+
rows: total,
|
|
486
|
+
message:
|
|
487
|
+
(total === 1
|
|
488
|
+
? `1 ${namify(table)} is `
|
|
489
|
+
: `${total} ${namify(table)}s are `) + "saved",
|
|
490
|
+
type: "success",
|
|
491
|
+
};
|
|
492
|
+
if (total === 1) response.id = lastId;
|
|
493
|
+
return response;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const nonUniqueColumns = columns.filter((c) => !uniqueKeys.includes(c));
|
|
498
|
+
const pk = await getPkColumn(table);
|
|
499
|
+
let lastId = 0;
|
|
500
|
+
|
|
501
|
+
for (const row of array) {
|
|
502
|
+
const vals = columns.map((c) => sanitizeValue(row[c]));
|
|
503
|
+
const placeholders = columns.map((_, i) => "$" + (i + 1)).join(",");
|
|
504
|
+
const conflictCols = uniqueKeys.join(",");
|
|
505
|
+
const updateSetSql = nonUniqueColumns
|
|
506
|
+
.map((c) => `${c} = EXCLUDED.${c}`)
|
|
507
|
+
.join(", ");
|
|
508
|
+
|
|
509
|
+
let sql = `INSERT INTO ${table} (${columns.join(",")}) VALUES (${placeholders}) ON CONFLICT (${conflictCols})`;
|
|
510
|
+
if (updateSetSql) {
|
|
511
|
+
sql += ` DO UPDATE SET ${updateSetSql}`;
|
|
512
|
+
} else {
|
|
513
|
+
sql += ` DO NOTHING`;
|
|
514
|
+
}
|
|
515
|
+
if (pk) sql += ` RETURNING ${pk}`;
|
|
516
|
+
|
|
517
|
+
const client = await pool.connect();
|
|
518
|
+
try {
|
|
519
|
+
const result = await client.query(sql, vals);
|
|
520
|
+
if (result.rows && result.rows.length > 0 && pk) {
|
|
521
|
+
lastId = result.rows[0][pk] || 0;
|
|
522
|
+
}
|
|
523
|
+
} catch (e) {
|
|
524
|
+
throw mapPgError(e);
|
|
525
|
+
} finally {
|
|
526
|
+
client.release();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const response = {
|
|
531
|
+
rows: total,
|
|
532
|
+
message:
|
|
533
|
+
(total === 1
|
|
534
|
+
? `1 ${namify(table)} is `
|
|
535
|
+
: `${total} ${namify(table)}s are `) + "saved",
|
|
536
|
+
type: "success",
|
|
537
|
+
};
|
|
538
|
+
if (total === 1) response.id = lastId;
|
|
539
|
+
return response;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function disconnect() {
|
|
543
|
+
if (pool) {
|
|
544
|
+
await pool.end();
|
|
545
|
+
pool = null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
module.exports = {
|
|
550
|
+
connect,
|
|
551
|
+
get,
|
|
552
|
+
list,
|
|
553
|
+
where,
|
|
554
|
+
query,
|
|
555
|
+
qcount,
|
|
556
|
+
remove,
|
|
557
|
+
upsert,
|
|
558
|
+
change: upsert,
|
|
559
|
+
insert,
|
|
560
|
+
disconnect,
|
|
561
|
+
close: disconnect,
|
|
562
|
+
pool: null,
|
|
563
|
+
};
|