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,552 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
|
|
3
|
+
const {
|
|
4
|
+
DynamoDBDocumentClient,
|
|
5
|
+
ScanCommand,
|
|
6
|
+
PutCommand,
|
|
7
|
+
UpdateCommand,
|
|
8
|
+
DeleteCommand,
|
|
9
|
+
BatchWriteCommand,
|
|
10
|
+
} = require("@aws-sdk/lib-dynamodb");
|
|
11
|
+
const { jsonSafeParse } = require("../commons/function");
|
|
12
|
+
|
|
13
|
+
let docClient = null;
|
|
14
|
+
let tablePrefix = "";
|
|
15
|
+
let primaryKey = "id";
|
|
16
|
+
const WHERE_INVALID = "Invalid filter object";
|
|
17
|
+
|
|
18
|
+
function connect(config) {
|
|
19
|
+
const clientConfig = {};
|
|
20
|
+
if (config.region) clientConfig.region = config.region;
|
|
21
|
+
if (config.endpoint) clientConfig.endpoint = config.endpoint;
|
|
22
|
+
if (config.accessKeyId && config.secretAccessKey) {
|
|
23
|
+
clientConfig.credentials = {
|
|
24
|
+
accessKeyId: config.accessKeyId,
|
|
25
|
+
secretAccessKey: config.secretAccessKey,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const client = new DynamoDBClient(clientConfig);
|
|
30
|
+
docClient = DynamoDBDocumentClient.from(client, {
|
|
31
|
+
marshallOptions: { removeUndefinedValues: true },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
tablePrefix = config.tablePrefix || config.prefix || "";
|
|
35
|
+
if (config.primaryKey) primaryKey = config.primaryKey;
|
|
36
|
+
|
|
37
|
+
return docClient;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tableName(table) {
|
|
41
|
+
return tablePrefix ? `${tablePrefix}${table}` : table;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function where(filter, safeDelete = null) {
|
|
45
|
+
if (filter !== null && filter !== "" && !Array.isArray(filter)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
if (
|
|
50
|
+
filter === null ||
|
|
51
|
+
filter === "" ||
|
|
52
|
+
filter.length === 0 ||
|
|
53
|
+
(Array.isArray(filter[0]) && filter[0].length === 0) ||
|
|
54
|
+
(Array.isArray(filter[0]) &&
|
|
55
|
+
Array.isArray(filter[0][0]) &&
|
|
56
|
+
filter[0][0].length === 0)
|
|
57
|
+
) {
|
|
58
|
+
if (safeDelete === null) {
|
|
59
|
+
return { query: "", value: {}, names: {} };
|
|
60
|
+
} else {
|
|
61
|
+
filter = [[]];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (safeDelete !== null) {
|
|
69
|
+
for (const filterItem of filter) {
|
|
70
|
+
filterItem.push([safeDelete, "=", 0]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const valid_conditionals = [
|
|
75
|
+
"=",
|
|
76
|
+
"like",
|
|
77
|
+
"not like",
|
|
78
|
+
"in",
|
|
79
|
+
"not in",
|
|
80
|
+
"<",
|
|
81
|
+
">",
|
|
82
|
+
"<=",
|
|
83
|
+
">=",
|
|
84
|
+
"!=",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
let orGroups = [];
|
|
88
|
+
let expressionValues = {};
|
|
89
|
+
let expressionNames = {};
|
|
90
|
+
let paramIndex = 0;
|
|
91
|
+
|
|
92
|
+
for (const group of filter) {
|
|
93
|
+
let andConditions = [];
|
|
94
|
+
for (const condition of group) {
|
|
95
|
+
if (!valid_conditionals.includes(condition[1])) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
(condition[1] === "in" || condition[1] === "not in") &&
|
|
100
|
+
!Array.isArray(condition[2])
|
|
101
|
+
) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const field = condition[0];
|
|
106
|
+
const op = condition[1];
|
|
107
|
+
const val = condition[2];
|
|
108
|
+
const nameKey = `#col${paramIndex}`;
|
|
109
|
+
expressionNames[nameKey] = field;
|
|
110
|
+
|
|
111
|
+
switch (op) {
|
|
112
|
+
case "=":
|
|
113
|
+
expressionValues[`:val${paramIndex}`] = val;
|
|
114
|
+
andConditions.push(`${nameKey} = :val${paramIndex}`);
|
|
115
|
+
break;
|
|
116
|
+
case "!=":
|
|
117
|
+
expressionValues[`:val${paramIndex}`] = val;
|
|
118
|
+
andConditions.push(`${nameKey} <> :val${paramIndex}`);
|
|
119
|
+
break;
|
|
120
|
+
case "<":
|
|
121
|
+
case ">":
|
|
122
|
+
case "<=":
|
|
123
|
+
case ">=":
|
|
124
|
+
expressionValues[`:val${paramIndex}`] = val;
|
|
125
|
+
andConditions.push(`${nameKey} ${op} :val${paramIndex}`);
|
|
126
|
+
break;
|
|
127
|
+
case "like":
|
|
128
|
+
expressionValues[`:val${paramIndex}`] = val;
|
|
129
|
+
andConditions.push(`contains(${nameKey}, :val${paramIndex})`);
|
|
130
|
+
break;
|
|
131
|
+
case "not like":
|
|
132
|
+
expressionValues[`:val${paramIndex}`] = val;
|
|
133
|
+
andConditions.push(`NOT contains(${nameKey}, :val${paramIndex})`);
|
|
134
|
+
break;
|
|
135
|
+
case "in": {
|
|
136
|
+
const inParts = [];
|
|
137
|
+
for (let k = 0; k < val.length; k++) {
|
|
138
|
+
const inKey = `:val${paramIndex}_${k}`;
|
|
139
|
+
expressionValues[inKey] = val[k];
|
|
140
|
+
inParts.push(inKey);
|
|
141
|
+
}
|
|
142
|
+
andConditions.push(`${nameKey} IN (${inParts.join(", ")})`);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "not in": {
|
|
146
|
+
const ninParts = [];
|
|
147
|
+
for (let k = 0; k < val.length; k++) {
|
|
148
|
+
const ninKey = `:val${paramIndex}_${k}`;
|
|
149
|
+
expressionValues[ninKey] = val[k];
|
|
150
|
+
ninParts.push(ninKey);
|
|
151
|
+
}
|
|
152
|
+
andConditions.push(`NOT ${nameKey} IN (${ninParts.join(", ")})`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
paramIndex++;
|
|
157
|
+
}
|
|
158
|
+
if (andConditions.length > 0) {
|
|
159
|
+
orGroups.push(andConditions.join(" AND "));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let filterExpression = "";
|
|
164
|
+
if (orGroups.length === 1) {
|
|
165
|
+
filterExpression = orGroups[0];
|
|
166
|
+
} else if (orGroups.length > 1) {
|
|
167
|
+
filterExpression = orGroups.map((g) => `(${g})`).join(" OR ");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
query: filterExpression,
|
|
172
|
+
value: expressionValues,
|
|
173
|
+
names: expressionNames,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function get(table, filter = [], sort = [], safeDelete = null) {
|
|
178
|
+
const whereData = where(filter, safeDelete);
|
|
179
|
+
if (whereData == null) {
|
|
180
|
+
throw new Error(WHERE_INVALID);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const params = { TableName: tableName(table) };
|
|
184
|
+
if (whereData.query) {
|
|
185
|
+
params.FilterExpression = whereData.query;
|
|
186
|
+
params.ExpressionAttributeValues = whereData.value;
|
|
187
|
+
params.ExpressionAttributeNames = whereData.names;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Scan all items (handle pagination internally to get all results)
|
|
191
|
+
let items = [];
|
|
192
|
+
let lastKey = undefined;
|
|
193
|
+
do {
|
|
194
|
+
if (lastKey) params.ExclusiveStartKey = lastKey;
|
|
195
|
+
const result = await docClient.send(new ScanCommand(params));
|
|
196
|
+
items = items.concat(result.Items || []);
|
|
197
|
+
lastKey = result.LastEvaluatedKey;
|
|
198
|
+
} while (lastKey);
|
|
199
|
+
|
|
200
|
+
// Apply sort in-memory
|
|
201
|
+
if (sort && sort.length > 0) {
|
|
202
|
+
items = sortItems(items, sort);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { data: jsonSafeParse(items), count: items.length };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function list(
|
|
209
|
+
table,
|
|
210
|
+
filter = [],
|
|
211
|
+
sort = [],
|
|
212
|
+
safeDelete = null,
|
|
213
|
+
page = 0,
|
|
214
|
+
limit = 30,
|
|
215
|
+
) {
|
|
216
|
+
const whereData = where(filter, safeDelete);
|
|
217
|
+
if (whereData == null) {
|
|
218
|
+
throw new Error(WHERE_INVALID);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Get total count first
|
|
222
|
+
const count = await qcount(table, filter, safeDelete);
|
|
223
|
+
|
|
224
|
+
// Scan all matching items (DynamoDB Limit applies before FilterExpression)
|
|
225
|
+
const params = { TableName: tableName(table) };
|
|
226
|
+
if (whereData.query) {
|
|
227
|
+
params.FilterExpression = whereData.query;
|
|
228
|
+
params.ExpressionAttributeValues = whereData.value;
|
|
229
|
+
params.ExpressionAttributeNames = whereData.names;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let items = [];
|
|
233
|
+
let lastKey = undefined;
|
|
234
|
+
do {
|
|
235
|
+
if (lastKey) params.ExclusiveStartKey = lastKey;
|
|
236
|
+
const result = await docClient.send(new ScanCommand(params));
|
|
237
|
+
items = items.concat(result.Items || []);
|
|
238
|
+
lastKey = result.LastEvaluatedKey;
|
|
239
|
+
} while (lastKey);
|
|
240
|
+
|
|
241
|
+
// Apply sort in-memory
|
|
242
|
+
if (sort && sort.length > 0) {
|
|
243
|
+
items = sortItems(items, sort);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Apply pagination in-memory
|
|
247
|
+
const offset = page * limit;
|
|
248
|
+
const paged = items.slice(offset, offset + limit);
|
|
249
|
+
|
|
250
|
+
return { data: jsonSafeParse(paged), count };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function qcount(table, filter, safeDelete = null) {
|
|
254
|
+
const whereData = where(filter, safeDelete);
|
|
255
|
+
if (whereData == null) {
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const params = {
|
|
261
|
+
TableName: tableName(table),
|
|
262
|
+
Select: "COUNT",
|
|
263
|
+
};
|
|
264
|
+
if (whereData.query) {
|
|
265
|
+
params.FilterExpression = whereData.query;
|
|
266
|
+
params.ExpressionAttributeValues = whereData.value;
|
|
267
|
+
params.ExpressionAttributeNames = whereData.names;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let total = 0;
|
|
271
|
+
let lastKey = undefined;
|
|
272
|
+
do {
|
|
273
|
+
if (lastKey) params.ExclusiveStartKey = lastKey;
|
|
274
|
+
const result = await docClient.send(new ScanCommand(params));
|
|
275
|
+
total += result.Count || 0;
|
|
276
|
+
lastKey = result.LastEvaluatedKey;
|
|
277
|
+
} while (lastKey);
|
|
278
|
+
|
|
279
|
+
return total;
|
|
280
|
+
} catch (err) {
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function remove(table, filter, safeDelete = null) {
|
|
286
|
+
const whereData = where(filter);
|
|
287
|
+
if (whereData == null) {
|
|
288
|
+
throw new Error(WHERE_INVALID);
|
|
289
|
+
}
|
|
290
|
+
if (!whereData.query && Object.keys(whereData.value).length < 1) {
|
|
291
|
+
throw new Error("unable to remove as there are no filter attributes");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// First get matching items to find their keys
|
|
295
|
+
const items = await getAllMatchingItems(table, whereData);
|
|
296
|
+
|
|
297
|
+
if (safeDelete != null) {
|
|
298
|
+
// Soft delete: update each item
|
|
299
|
+
for (const item of items) {
|
|
300
|
+
const key = extractKey(item);
|
|
301
|
+
await docClient.send(
|
|
302
|
+
new UpdateCommand({
|
|
303
|
+
TableName: tableName(table),
|
|
304
|
+
Key: key,
|
|
305
|
+
UpdateExpression: `SET #sd = :sdVal`,
|
|
306
|
+
ExpressionAttributeNames: { "#sd": safeDelete },
|
|
307
|
+
ExpressionAttributeValues: { ":sdVal": 1 },
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Hard delete
|
|
313
|
+
for (const item of items) {
|
|
314
|
+
const key = extractKey(item);
|
|
315
|
+
await docClient.send(
|
|
316
|
+
new DeleteCommand({
|
|
317
|
+
TableName: tableName(table),
|
|
318
|
+
Key: key,
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const rows = items.length;
|
|
325
|
+
return {
|
|
326
|
+
message: rows + " " + table + (rows > 1 ? "s" : "") + " removed",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function upsert(table, data, uniqueKeys = []) {
|
|
331
|
+
let array = Array.isArray(data) ? [...data] : [data];
|
|
332
|
+
const total = array.length;
|
|
333
|
+
|
|
334
|
+
if (!uniqueKeys || uniqueKeys.length === 0) {
|
|
335
|
+
return insert(table, data, uniqueKeys);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let lastId = null;
|
|
339
|
+
for (const row of array) {
|
|
340
|
+
const key = {};
|
|
341
|
+
for (const k of uniqueKeys) {
|
|
342
|
+
key[k] = row[k];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const nonKeyFields = Object.keys(row).filter(
|
|
346
|
+
(k) => !uniqueKeys.includes(k),
|
|
347
|
+
);
|
|
348
|
+
if (nonKeyFields.length === 0) {
|
|
349
|
+
// Only key fields, just put the item
|
|
350
|
+
await docClient.send(
|
|
351
|
+
new PutCommand({
|
|
352
|
+
TableName: tableName(table),
|
|
353
|
+
Item: row,
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
lastId = key[uniqueKeys[0]];
|
|
357
|
+
} else {
|
|
358
|
+
const updateParts = [];
|
|
359
|
+
const exprValues = {};
|
|
360
|
+
const exprNames = {};
|
|
361
|
+
nonKeyFields.forEach((field, idx) => {
|
|
362
|
+
exprNames[`#f${idx}`] = field;
|
|
363
|
+
exprValues[`:v${idx}`] = row[field];
|
|
364
|
+
updateParts.push(`#f${idx} = :v${idx}`);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
await docClient.send(
|
|
369
|
+
new UpdateCommand({
|
|
370
|
+
TableName: tableName(table),
|
|
371
|
+
Key: key,
|
|
372
|
+
UpdateExpression: `SET ${updateParts.join(", ")}`,
|
|
373
|
+
ExpressionAttributeNames: exprNames,
|
|
374
|
+
ExpressionAttributeValues: exprValues,
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
if (err.name === "ConditionalCheckFailedException") {
|
|
379
|
+
const dupError = new Error(err.message);
|
|
380
|
+
dupError.code = "ER_DUP_ENTRY";
|
|
381
|
+
dupError.sqlMessage = err.message;
|
|
382
|
+
throw dupError;
|
|
383
|
+
}
|
|
384
|
+
if (err.name === "ResourceNotFoundException") {
|
|
385
|
+
const notFoundError = new Error(err.message);
|
|
386
|
+
notFoundError.sqlMessage = err.message;
|
|
387
|
+
throw notFoundError;
|
|
388
|
+
}
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
lastId = key[uniqueKeys[0]];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const response = {
|
|
396
|
+
rows: total,
|
|
397
|
+
message:
|
|
398
|
+
(total === 1
|
|
399
|
+
? `1 ${namify(table)} is `
|
|
400
|
+
: `${total} ${namify(table)}s are `) + "saved",
|
|
401
|
+
type: "success",
|
|
402
|
+
};
|
|
403
|
+
if (total === 1 && lastId != null) {
|
|
404
|
+
response.id = lastId;
|
|
405
|
+
}
|
|
406
|
+
return response;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function insert(table, data, uniqueKeys = []) {
|
|
410
|
+
let array = Array.isArray(data) ? [...data] : [data];
|
|
411
|
+
const total = array.length;
|
|
412
|
+
|
|
413
|
+
// Auto-generate primary key (UUID) for items missing it
|
|
414
|
+
const pkField = (uniqueKeys && uniqueKeys[0]) || primaryKey;
|
|
415
|
+
for (const item of array) {
|
|
416
|
+
if (item[pkField] == null || item[pkField] === "") {
|
|
417
|
+
item[pkField] = crypto.randomUUID();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
if (total === 1) {
|
|
423
|
+
await docClient.send(
|
|
424
|
+
new PutCommand({
|
|
425
|
+
TableName: tableName(table),
|
|
426
|
+
Item: array[0],
|
|
427
|
+
}),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const response = {
|
|
431
|
+
rows: 1,
|
|
432
|
+
message: `1 ${namify(table)} is saved`,
|
|
433
|
+
type: "success",
|
|
434
|
+
};
|
|
435
|
+
if (array[0][pkField] != null) {
|
|
436
|
+
response.id = array[0][pkField];
|
|
437
|
+
}
|
|
438
|
+
return response;
|
|
439
|
+
} else {
|
|
440
|
+
// Batch write (max 25 items per batch)
|
|
441
|
+
const batches = [];
|
|
442
|
+
for (let i = 0; i < array.length; i += 25) {
|
|
443
|
+
batches.push(array.slice(i, i + 25));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (const batch of batches) {
|
|
447
|
+
const requestItems = {
|
|
448
|
+
[tableName(table)]: batch.map((item) => ({
|
|
449
|
+
PutRequest: { Item: item },
|
|
450
|
+
})),
|
|
451
|
+
};
|
|
452
|
+
await docClient.send(
|
|
453
|
+
new BatchWriteCommand({ RequestItems: requestItems }),
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
rows: total,
|
|
459
|
+
message: `${total} ${namify(table)}s are saved`,
|
|
460
|
+
type: "success",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
} catch (err) {
|
|
464
|
+
if (err.name === "ConditionalCheckFailedException") {
|
|
465
|
+
const dupError = new Error(err.message);
|
|
466
|
+
dupError.code = "ER_DUP_ENTRY";
|
|
467
|
+
dupError.sqlMessage = err.message;
|
|
468
|
+
throw dupError;
|
|
469
|
+
}
|
|
470
|
+
if (err.name === "ResourceNotFoundException") {
|
|
471
|
+
const notFoundError = new Error(err.message);
|
|
472
|
+
notFoundError.sqlMessage = err.message;
|
|
473
|
+
throw notFoundError;
|
|
474
|
+
}
|
|
475
|
+
throw err;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function disconnect() {
|
|
480
|
+
docClient = null;
|
|
481
|
+
tablePrefix = "";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// --- Utility helpers ---
|
|
485
|
+
|
|
486
|
+
async function getAllMatchingItems(table, whereData) {
|
|
487
|
+
const params = { TableName: tableName(table) };
|
|
488
|
+
if (whereData.query) {
|
|
489
|
+
params.FilterExpression = whereData.query;
|
|
490
|
+
params.ExpressionAttributeValues = whereData.value;
|
|
491
|
+
params.ExpressionAttributeNames = whereData.names;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let items = [];
|
|
495
|
+
let lastKey = undefined;
|
|
496
|
+
do {
|
|
497
|
+
if (lastKey) params.ExclusiveStartKey = lastKey;
|
|
498
|
+
const result = await docClient.send(new ScanCommand(params));
|
|
499
|
+
items = items.concat(result.Items || []);
|
|
500
|
+
lastKey = result.LastEvaluatedKey;
|
|
501
|
+
} while (lastKey);
|
|
502
|
+
|
|
503
|
+
return items;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function extractKey(item) {
|
|
507
|
+
// For DynamoDB, we need to know the key schema.
|
|
508
|
+
// We use the primaryKey configured at connect time.
|
|
509
|
+
const key = {};
|
|
510
|
+
if (item[primaryKey] != null) {
|
|
511
|
+
key[primaryKey] = item[primaryKey];
|
|
512
|
+
}
|
|
513
|
+
return key;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function sortItems(items, sort) {
|
|
517
|
+
return [...items].sort((a, b) => {
|
|
518
|
+
for (const s of sort) {
|
|
519
|
+
let field, dir;
|
|
520
|
+
if (s[0] === "-") {
|
|
521
|
+
field = s.substring(1);
|
|
522
|
+
dir = -1;
|
|
523
|
+
} else {
|
|
524
|
+
field = s;
|
|
525
|
+
dir = 1;
|
|
526
|
+
}
|
|
527
|
+
if (a[field] < b[field]) return -1 * dir;
|
|
528
|
+
if (a[field] > b[field]) return 1 * dir;
|
|
529
|
+
}
|
|
530
|
+
return 0;
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function namify(text) {
|
|
535
|
+
return text
|
|
536
|
+
.replace("_", " ")
|
|
537
|
+
.replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase());
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
module.exports = {
|
|
541
|
+
connect,
|
|
542
|
+
get,
|
|
543
|
+
list,
|
|
544
|
+
where,
|
|
545
|
+
qcount,
|
|
546
|
+
remove,
|
|
547
|
+
upsert,
|
|
548
|
+
change: upsert,
|
|
549
|
+
insert,
|
|
550
|
+
disconnect,
|
|
551
|
+
close: disconnect,
|
|
552
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const model = require("./commons/model.js");
|
|
2
|
+
const route = require("./commons/route.js");
|
|
3
|
+
const routers = {
|
|
4
|
+
mysql: "./mysql/db.js",
|
|
5
|
+
postgresql: "./postgres/db.js",
|
|
6
|
+
postgres: "./postgres/db.js",
|
|
7
|
+
oracle: "./oracle/db.js",
|
|
8
|
+
sqlite3: "./sqlite3/db.js",
|
|
9
|
+
mssql: "./mssql/db.js",
|
|
10
|
+
cockroachdb: "./cockroachdb/db.js",
|
|
11
|
+
mongodb: "./mongodb/db.js",
|
|
12
|
+
redis: "./redis/db.js",
|
|
13
|
+
dynamodb: "./dynamodb/db.js",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let db = null;
|
|
17
|
+
|
|
18
|
+
function init(DB_TYPE) {
|
|
19
|
+
const dbType = (DB_TYPE || "mysql").toLowerCase();
|
|
20
|
+
const routerPath = routers[dbType];
|
|
21
|
+
if (!routerPath) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Unsupported DB_TYPE: "${dbType}". Supported: ${Object.keys(routers).join(", ")}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
db = require(routerPath);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code === "MODULE_NOT_FOUND") {
|
|
30
|
+
const driverMap = {
|
|
31
|
+
mysql: "mysql2",
|
|
32
|
+
postgresql: "pg",
|
|
33
|
+
postgres: "pg",
|
|
34
|
+
oracle: "oracledb",
|
|
35
|
+
sqlite3: "better-sqlite3",
|
|
36
|
+
mssql: "mssql",
|
|
37
|
+
cockroachdb: "pg",
|
|
38
|
+
mongodb: "mongodb",
|
|
39
|
+
redis: "ioredis",
|
|
40
|
+
dynamodb: "@aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb",
|
|
41
|
+
};
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Missing driver for "${dbType}". Install it with: npm install ${driverMap[dbType] || dbType}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
init,
|
|
52
|
+
get db() {
|
|
53
|
+
return db;
|
|
54
|
+
},
|
|
55
|
+
model,
|
|
56
|
+
route,
|
|
57
|
+
};
|