db-model-router 1.0.6 → 1.0.8
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/README.md +150 -11
- package/TODO.md +0 -15
- package/db-manager/.dbmanager.sqlite +0 -0
- package/db-manager/README.md +223 -0
- package/db-manager/adapter-proxy.js +361 -0
- package/db-manager/demo/cockroachdb.env +6 -0
- package/db-manager/demo/demo.sqlite +0 -0
- package/db-manager/demo/dynamodb.env +7 -0
- package/db-manager/demo/mongodb.env +4 -0
- package/db-manager/demo/mssql.env +6 -0
- package/db-manager/demo/mysql.env +6 -0
- package/db-manager/demo/oracle.env +6 -0
- package/db-manager/demo/postgres.env +6 -0
- package/db-manager/demo/redis.env +4 -0
- package/db-manager/demo/seeds/cockroachdb.sql +32 -0
- package/db-manager/demo/seeds/mssql.sql +32 -0
- package/db-manager/demo/seeds/mysql.sql +32 -0
- package/db-manager/demo/seeds/oracle.sql +43 -0
- package/db-manager/demo/seeds/postgres.sql +32 -0
- package/db-manager/demo/seeds/sqlite3.sql +32 -0
- package/db-manager/demo/sqlite3.env +2 -0
- package/db-manager/metadata-db.js +170 -0
- package/db-manager/public/.gitkeep +1 -0
- package/db-manager/public/css/style.css +1413 -0
- package/db-manager/public/js/app.js +1370 -0
- package/db-manager/routes/api.js +388 -0
- package/db-manager/routes/views.js +61 -0
- package/db-manager/server.js +39 -0
- package/db-manager/utils/build-filter-config.js +18 -0
- package/db-manager/utils/csv-export.js +59 -0
- package/db-manager/utils/export-filename.js +39 -0
- package/db-manager/utils/filter-tables.js +20 -0
- package/db-manager/utils/parse-filters.js +93 -0
- package/db-manager/utils/sort-state.js +35 -0
- package/db-manager/views/.gitkeep +1 -0
- package/db-manager/views/dashboard.ejs +53 -0
- package/db-manager/views/history.ejs +52 -0
- package/db-manager/views/index.ejs +35 -0
- package/db-manager/views/layout.ejs +31 -0
- package/db-manager/views/partials/data-panel.ejs +74 -0
- package/db-manager/views/partials/header.ejs +36 -0
- package/db-manager/views/partials/sidebar.ejs +30 -0
- package/db-manager/views/query.ejs +58 -0
- package/dbmr.schema.json +22 -44
- package/demo/.dockerignore +7 -0
- package/demo/.env.example +15 -0
- package/demo/Dockerfile +20 -0
- package/demo/app.js +39 -0
- package/demo/commons/add_migration.js +43 -0
- package/demo/commons/db.js +17 -0
- package/demo/commons/migrate.js +68 -0
- package/demo/commons/modules.js +18 -0
- package/demo/commons/password.js +36 -0
- package/demo/commons/security.js +30 -0
- package/demo/commons/session.js +13 -0
- package/demo/commons/webhook.js +81 -0
- package/demo/dbmr.schema.json +338 -0
- package/demo/middleware/authenticate.js +14 -0
- package/demo/middleware/hasPermission.js +30 -0
- package/demo/middleware/logger.js +67 -0
- package/demo/middleware/tenantIsolation.js +19 -0
- package/demo/migrations/20260510092158_create_migrations_table.sql +6 -0
- package/demo/migrations/20260510092159_create_saas_tables.sql +69 -0
- package/demo/migrations/20260510092159_create_tables.sql +193 -0
- package/demo/models/addresses.js +24 -0
- package/demo/models/cart_items.js +20 -0
- package/demo/models/carts.js +18 -0
- package/demo/models/categories.js +22 -0
- package/demo/models/coupons.js +25 -0
- package/demo/models/index.js +43 -0
- package/demo/models/order_items.js +23 -0
- package/demo/models/orders.js +27 -0
- package/demo/models/payments.js +23 -0
- package/demo/models/product_images.js +20 -0
- package/demo/models/product_reviews.js +22 -0
- package/demo/models/product_variants.js +22 -0
- package/demo/models/products.js +32 -0
- package/demo/models/role_permissions.js +17 -0
- package/demo/models/roles.js +17 -0
- package/demo/models/shipments.js +21 -0
- package/demo/models/tenants.js +18 -0
- package/demo/models/users.js +23 -0
- package/demo/models/webhook_logs.js +22 -0
- package/demo/models/webhooks.js +19 -0
- package/demo/models/wishlists.js +17 -0
- package/demo/openapi.json +7000 -0
- package/demo/package-lock.json +2827 -0
- package/demo/package.json +42 -0
- package/demo/routes/addresses/index.js +10 -0
- package/demo/routes/auth/index.js +55 -0
- package/demo/routes/carts/cart_items/index.js +11 -0
- package/demo/routes/carts/index.js +14 -0
- package/demo/routes/categories/index.js +10 -0
- package/demo/routes/coupons/index.js +10 -0
- package/demo/routes/docs.js +18 -0
- package/demo/routes/health.js +35 -0
- package/demo/routes/index.js +54 -0
- package/demo/routes/orders/index.js +18 -0
- package/demo/routes/orders/order_items/index.js +11 -0
- package/demo/routes/orders/payments/index.js +11 -0
- package/demo/routes/orders/shipments/index.js +11 -0
- package/demo/routes/products/index.js +18 -0
- package/demo/routes/products/product_images/index.js +11 -0
- package/demo/routes/products/product_reviews/index.js +11 -0
- package/demo/routes/products/product_variants/index.js +11 -0
- package/demo/routes/roles/index.js +75 -0
- package/demo/routes/roles/permissions/index.js +47 -0
- package/demo/routes/tenants/index.js +45 -0
- package/demo/routes/users/index.js +45 -0
- package/demo/routes/wishlists/index.js +10 -0
- package/demo/seeds/saas-seed.js +329 -0
- package/docker-compose.yml +61 -0
- package/package.json +120 -113
- package/scripts/demo-create.js +1 -1
- package/skill/SKILL.md +119 -3
- package/src/cli/commands/db-manager.js +134 -0
- package/src/cli/commands/generate.js +106 -60
- package/src/cli/commands/help.js +0 -1
- package/src/cli/generate-route.js +66 -27
- package/src/cli/generate-saas-structure.js +129 -0
- package/src/cli/init/dependencies.js +1 -1
- package/src/cli/init/generators.js +6 -77
- package/src/cli/init.js +9 -2
- package/src/cli/main.js +8 -1
- package/src/cli/saas/generate-saas-middleware.js +110 -0
- package/src/cli/saas/generate-saas-migrations.js +480 -0
- package/src/cli/saas/generate-saas-models.js +211 -0
- package/src/cli/saas/generate-saas-openapi.js +419 -0
- package/src/cli/saas/generate-saas-routes.js +435 -0
- package/src/cli/saas/generate-saas-seeds.js +243 -0
- package/src/cli/saas/generate-saas-tests.js +473 -0
- package/src/cli/saas/generate-saas-utils.js +176 -0
- package/src/commons/kafka.js +139 -0
- package/src/commons/model.js +29 -9
- package/src/commons/route.js +6 -6
- package/src/index.js +2 -0
- package/src/mssql/db.js +41 -3
- package/src/mysql/db.js +3 -0
- package/src/postgres/db.js +6 -0
- package/src/cli/generate-db-manager.js +0 -1573
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kafka Producer Service
|
|
3
|
+
*
|
|
4
|
+
* Produces events to Kafka when database write operations (insert, update, upsert, delete) are performed.
|
|
5
|
+
* Enabled only when KAFKA_BROKER is set in environment variables.
|
|
6
|
+
*
|
|
7
|
+
* Event format:
|
|
8
|
+
* {
|
|
9
|
+
* table_name: string,
|
|
10
|
+
* operation_type: "insert" | "update" | "upsert" | "delete",
|
|
11
|
+
* data: object,
|
|
12
|
+
* timestamp: string (ISO 8601)
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* Each row produces its own event. Bulk operations produce one event per entry.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
let producer = null;
|
|
19
|
+
let kafka = null;
|
|
20
|
+
let isConnected = false;
|
|
21
|
+
let isEnabled = false;
|
|
22
|
+
let topicPrefix = "";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize Kafka producer if KAFKA_BROKER env variable is set.
|
|
26
|
+
* @param {object} [options] - Optional configuration overrides
|
|
27
|
+
* @param {string} [options.broker] - Kafka broker URL (default: process.env.KAFKA_BROKER)
|
|
28
|
+
* @param {string} [options.clientId] - Kafka client ID (default: process.env.KAFKA_CLIENT_ID || "db-model-router")
|
|
29
|
+
* @param {string} [options.topicPrefix] - Topic prefix (default: process.env.KAFKA_TOPIC_PREFIX || "dbmr")
|
|
30
|
+
* @returns {Promise<boolean>} Whether Kafka was successfully initialized
|
|
31
|
+
*/
|
|
32
|
+
async function init(options = {}) {
|
|
33
|
+
const broker = options.broker || process.env.KAFKA_BROKER;
|
|
34
|
+
if (!broker) {
|
|
35
|
+
isEnabled = false;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const clientId =
|
|
40
|
+
options.clientId || process.env.KAFKA_CLIENT_ID || "db-model-router";
|
|
41
|
+
topicPrefix = options.topicPrefix || process.env.KAFKA_TOPIC_PREFIX || "dbmr";
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const { Kafka } = require("kafkajs");
|
|
45
|
+
kafka = new Kafka({
|
|
46
|
+
clientId,
|
|
47
|
+
brokers: broker.split(",").map((b) => b.trim()),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
producer = kafka.producer();
|
|
51
|
+
await producer.connect();
|
|
52
|
+
isConnected = true;
|
|
53
|
+
isEnabled = true;
|
|
54
|
+
return true;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(
|
|
57
|
+
"[db-model-router] Kafka initialization failed:",
|
|
58
|
+
err.message,
|
|
59
|
+
);
|
|
60
|
+
isEnabled = false;
|
|
61
|
+
isConnected = false;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Produce Kafka event(s) for a database operation.
|
|
68
|
+
* If data is an array, one event is produced per entry (batched to avoid exceeding message size limits).
|
|
69
|
+
* If data is a single object, one event is produced.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} tableName - The database table name
|
|
72
|
+
* @param {string} operationType - One of: "insert", "update", "upsert", "delete"
|
|
73
|
+
* @param {object|object[]} data - The data involved in the operation
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
async function produce(tableName, operationType, data) {
|
|
77
|
+
if (!isEnabled || !isConnected || !producer) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const topic = `${topicPrefix}.${tableName}`;
|
|
82
|
+
const entries = Array.isArray(data) ? data : [data];
|
|
83
|
+
const timestamp = new Date().toISOString();
|
|
84
|
+
|
|
85
|
+
const messages = entries.map((entry) => ({
|
|
86
|
+
key: tableName,
|
|
87
|
+
value: JSON.stringify({
|
|
88
|
+
table_name: tableName,
|
|
89
|
+
operation_type: operationType,
|
|
90
|
+
data: entry,
|
|
91
|
+
timestamp,
|
|
92
|
+
}),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
// Batch messages to avoid exceeding Kafka's max request size
|
|
96
|
+
const BATCH_SIZE = 500;
|
|
97
|
+
try {
|
|
98
|
+
for (let i = 0; i < messages.length; i += BATCH_SIZE) {
|
|
99
|
+
const batch = messages.slice(i, i + BATCH_SIZE);
|
|
100
|
+
await producer.send({ topic, messages: batch });
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(
|
|
104
|
+
`[db-model-router] Kafka produce failed for ${topic}:`,
|
|
105
|
+
err.message,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Disconnect the Kafka producer gracefully.
|
|
112
|
+
* @returns {Promise<void>}
|
|
113
|
+
*/
|
|
114
|
+
async function disconnect() {
|
|
115
|
+
if (producer && isConnected) {
|
|
116
|
+
try {
|
|
117
|
+
await producer.disconnect();
|
|
118
|
+
} catch (_) {
|
|
119
|
+
// ignore disconnect errors
|
|
120
|
+
}
|
|
121
|
+
isConnected = false;
|
|
122
|
+
isEnabled = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if Kafka is currently enabled and connected.
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
function status() {
|
|
131
|
+
return isEnabled && isConnected;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
init,
|
|
136
|
+
produce,
|
|
137
|
+
disconnect,
|
|
138
|
+
status,
|
|
139
|
+
};
|
package/src/commons/model.js
CHANGED
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
RemoveUnknownData,
|
|
7
7
|
} = require("./validator");
|
|
8
8
|
const { getType, jsonStringify, jsonSafeParse } = require("./function");
|
|
9
|
+
const { produce } = require("./kafka");
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Extract and remove reserved params from a data/payload object.
|
|
@@ -215,9 +216,12 @@ module.exports = function model(
|
|
|
215
216
|
const getResult = await db.get(table, [
|
|
216
217
|
[[primary_key, "=", insertResult.id]],
|
|
217
218
|
]);
|
|
218
|
-
|
|
219
|
+
const record = getResult.count > 0 ? getResult["data"][0] : null;
|
|
220
|
+
if (record) produce(table, "insert", record);
|
|
221
|
+
return record;
|
|
219
222
|
}
|
|
220
223
|
//TODO: Bulk Insert -> Return inserted objects
|
|
224
|
+
produce(table, "insert", data);
|
|
221
225
|
return insertResult;
|
|
222
226
|
},
|
|
223
227
|
update: async (data) => {
|
|
@@ -233,6 +237,7 @@ module.exports = function model(
|
|
|
233
237
|
data = jsonStringify(data);
|
|
234
238
|
updateResult = await db.upsert(table, data, unique);
|
|
235
239
|
//TODO: Bulk Update -> Return updated objects
|
|
240
|
+
produce(table, "update", data);
|
|
236
241
|
} else {
|
|
237
242
|
stripTimestampFields(data, allTimestampKeys);
|
|
238
243
|
await validateInput(
|
|
@@ -246,7 +251,9 @@ module.exports = function model(
|
|
|
246
251
|
const getResult = await db.get(table, [
|
|
247
252
|
[[primary_key, "=", updateResult.id]],
|
|
248
253
|
]);
|
|
249
|
-
|
|
254
|
+
const record = getResult.count > 0 ? getResult["data"][0] : null;
|
|
255
|
+
if (record) produce(table, "update", record);
|
|
256
|
+
return record;
|
|
250
257
|
} else if (data[0].hasOwnProperty(primary_key)) {
|
|
251
258
|
const result = await db.get(
|
|
252
259
|
table,
|
|
@@ -254,8 +261,10 @@ module.exports = function model(
|
|
|
254
261
|
[],
|
|
255
262
|
option.safeDelete,
|
|
256
263
|
);
|
|
257
|
-
if (result.count > 0)
|
|
258
|
-
|
|
264
|
+
if (result.count > 0) {
|
|
265
|
+
produce(table, "update", result["data"][0]);
|
|
266
|
+
return result["data"][0];
|
|
267
|
+
} else return null;
|
|
259
268
|
}
|
|
260
269
|
}
|
|
261
270
|
return updateResult;
|
|
@@ -274,6 +283,7 @@ module.exports = function model(
|
|
|
274
283
|
data = jsonStringify(data);
|
|
275
284
|
updateResult = await db.upsert(table, data, unique);
|
|
276
285
|
//TODO: Bulk Upsert -> Return Inserted/Updated objects
|
|
286
|
+
produce(table, "upsert", data);
|
|
277
287
|
} else {
|
|
278
288
|
stripTimestampFields(data, allTimestampKeys);
|
|
279
289
|
await validateInput(
|
|
@@ -288,20 +298,27 @@ module.exports = function model(
|
|
|
288
298
|
const getResult = await db.get(table, [
|
|
289
299
|
[[primary_key, "=", updateResult.id]],
|
|
290
300
|
]);
|
|
291
|
-
|
|
301
|
+
const record = getResult.count > 0 ? getResult["data"][0] : null;
|
|
302
|
+
if (record) produce(table, "upsert", record);
|
|
303
|
+
return record;
|
|
292
304
|
} else if (originalData.hasOwnProperty(primary_key)) {
|
|
293
305
|
const result = await db.get(table, [
|
|
294
306
|
[[primary_key, "=", originalData[primary_key]]],
|
|
295
307
|
]);
|
|
296
|
-
if (result.count > 0)
|
|
297
|
-
|
|
308
|
+
if (result.count > 0) {
|
|
309
|
+
produce(table, "upsert", result["data"][0]);
|
|
310
|
+
return result["data"][0];
|
|
311
|
+
} else return null;
|
|
298
312
|
}
|
|
299
313
|
}
|
|
300
314
|
return updateResult;
|
|
301
315
|
},
|
|
302
316
|
remove: async (data) => {
|
|
317
|
+
const originalData = Array.isArray(data) ? [...data] : { ...data };
|
|
303
318
|
let filter = dataToFilter(jsonSafeParse(data), primary_key);
|
|
304
|
-
|
|
319
|
+
const removeResult = await db.remove(table, filter, option.safeDelete);
|
|
320
|
+
produce(table, "delete", originalData);
|
|
321
|
+
return removeResult;
|
|
305
322
|
},
|
|
306
323
|
byId: async (id, options = {}) => {
|
|
307
324
|
let type = getType(id);
|
|
@@ -427,7 +444,10 @@ module.exports = function model(
|
|
|
427
444
|
[],
|
|
428
445
|
option.safeDelete,
|
|
429
446
|
);
|
|
430
|
-
if (result.count > 0)
|
|
447
|
+
if (result.count > 0) {
|
|
448
|
+
produce(table, "update", result["data"][0]);
|
|
449
|
+
return result["data"][0];
|
|
450
|
+
}
|
|
431
451
|
return null;
|
|
432
452
|
},
|
|
433
453
|
pk: primary_key,
|
package/src/commons/route.js
CHANGED
|
@@ -31,7 +31,7 @@ module.exports = function route(model, override = {}) {
|
|
|
31
31
|
.Router({ mergeParams: true })
|
|
32
32
|
.get("/:" + model.pk, (req, res) => {
|
|
33
33
|
let payload = payloadOverride(
|
|
34
|
-
{ ...req.query, ...req.params },
|
|
34
|
+
{ ...(req.query || {}), ...(req.params || {}) },
|
|
35
35
|
req,
|
|
36
36
|
override,
|
|
37
37
|
);
|
|
@@ -52,7 +52,7 @@ module.exports = function route(model, override = {}) {
|
|
|
52
52
|
});
|
|
53
53
|
})
|
|
54
54
|
.post("/:id", (req, res) => {
|
|
55
|
-
let payload = payloadOverride(req.body, req, override);
|
|
55
|
+
let payload = payloadOverride(req.body || {}, req, override);
|
|
56
56
|
delete payload[model.pk];
|
|
57
57
|
model
|
|
58
58
|
.insert(payload)
|
|
@@ -64,7 +64,7 @@ module.exports = function route(model, override = {}) {
|
|
|
64
64
|
});
|
|
65
65
|
})
|
|
66
66
|
.put("/:id", (req, res) => {
|
|
67
|
-
let payload = payloadOverride(req.body, req, override);
|
|
67
|
+
let payload = payloadOverride(req.body || {}, req, override);
|
|
68
68
|
payload[model.pk] = req.params.id;
|
|
69
69
|
let validateAccessPayload = payloadOverride({}, req, override);
|
|
70
70
|
validateAccessPayload[model.pk] = req.params.id;
|
|
@@ -89,7 +89,7 @@ module.exports = function route(model, override = {}) {
|
|
|
89
89
|
});
|
|
90
90
|
})
|
|
91
91
|
.patch("/:id", (req, res) => {
|
|
92
|
-
let payload = payloadOverride(req.body, req, override);
|
|
92
|
+
let payload = payloadOverride(req.body || {}, req, override);
|
|
93
93
|
payload[model.pk] = req.params.id;
|
|
94
94
|
let validateAccessPayload = payloadOverride({}, req, override);
|
|
95
95
|
validateAccessPayload[model.pk] = req.params.id;
|
|
@@ -114,7 +114,7 @@ module.exports = function route(model, override = {}) {
|
|
|
114
114
|
});
|
|
115
115
|
})
|
|
116
116
|
.delete("/:id", (req, res) => {
|
|
117
|
-
let payload = payloadOverride(req.body, req, override);
|
|
117
|
+
let payload = payloadOverride(req.body || {}, req, override);
|
|
118
118
|
payload[model.pk] = req.params.id;
|
|
119
119
|
let validateAccessPayload = payloadOverride({}, req, override);
|
|
120
120
|
validateAccessPayload[model.pk] = req.params.id;
|
|
@@ -140,7 +140,7 @@ module.exports = function route(model, override = {}) {
|
|
|
140
140
|
})
|
|
141
141
|
.get("/", (req, res) => {
|
|
142
142
|
let payload = payloadOverride(
|
|
143
|
-
{ ...req.query, ...req.params },
|
|
143
|
+
{ ...(req.query || {}), ...(req.params || {}) },
|
|
144
144
|
req,
|
|
145
145
|
override,
|
|
146
146
|
);
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const model = require("./commons/model.js");
|
|
2
2
|
const route = require("./commons/route.js");
|
|
3
|
+
const kafka = require("./commons/kafka.js");
|
|
3
4
|
const routers = {
|
|
4
5
|
mysql: "./mysql/db.js",
|
|
5
6
|
mariadb: "./mysql/db.js",
|
|
@@ -56,4 +57,5 @@ module.exports = {
|
|
|
56
57
|
},
|
|
57
58
|
model,
|
|
58
59
|
route,
|
|
60
|
+
kafka,
|
|
59
61
|
};
|
package/src/mssql/db.js
CHANGED
|
@@ -2,8 +2,39 @@ const sql = require("mssql");
|
|
|
2
2
|
const { jsonSafeParse } = require("../commons/function");
|
|
3
3
|
|
|
4
4
|
let pool = null;
|
|
5
|
+
let dateStringsMode = false;
|
|
5
6
|
const WHERE_INVALID = "Invalid filter object";
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Formats a Date object to YYYY-MM-DD HH:mm:ss string in UTC.
|
|
10
|
+
*/
|
|
11
|
+
function formatDate(d) {
|
|
12
|
+
var y = d.getUTCFullYear();
|
|
13
|
+
var m = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
14
|
+
var day = String(d.getUTCDate()).padStart(2, "0");
|
|
15
|
+
var h = String(d.getUTCHours()).padStart(2, "0");
|
|
16
|
+
var min = String(d.getUTCMinutes()).padStart(2, "0");
|
|
17
|
+
var s = String(d.getUTCSeconds()).padStart(2, "0");
|
|
18
|
+
return y + "-" + m + "-" + day + " " + h + ":" + min + ":" + s;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Maps recordset rows, converting Date objects to formatted strings if dateStringsMode is enabled.
|
|
23
|
+
*/
|
|
24
|
+
function mapRecordset(rows) {
|
|
25
|
+
if (!dateStringsMode || !Array.isArray(rows)) return rows;
|
|
26
|
+
return rows.map(function (row) {
|
|
27
|
+
var mapped = {};
|
|
28
|
+
for (var key in row) {
|
|
29
|
+
if (Object.prototype.hasOwnProperty.call(row, key)) {
|
|
30
|
+
mapped[key] =
|
|
31
|
+
row[key] instanceof Date ? formatDate(row[key]) : row[key];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return mapped;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
7
38
|
const RETRYABLE_ERRORS = [
|
|
8
39
|
"ECONNREFUSED",
|
|
9
40
|
"ECONNRESET",
|
|
@@ -34,6 +65,9 @@ function isRetryable(err) {
|
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
async function connect(config) {
|
|
68
|
+
dateStringsMode =
|
|
69
|
+
config.dateStrings === true || process.env.DB_DATE_STRINGS === "true";
|
|
70
|
+
|
|
37
71
|
const mssqlConfig = {
|
|
38
72
|
server: config.server || config.host || process.env.DB_HOST || "localhost",
|
|
39
73
|
port: parseInt(config.port || process.env.DB_PORT || 1433),
|
|
@@ -75,7 +109,11 @@ async function query(sqlStr, parameter = []) {
|
|
|
75
109
|
}
|
|
76
110
|
// Replace positional @paramN placeholders if not already present
|
|
77
111
|
const result = await request.query(sqlStr);
|
|
78
|
-
return
|
|
112
|
+
return (
|
|
113
|
+
mapRecordset(result.recordset) || {
|
|
114
|
+
affectedRows: result.rowsAffected?.[0] || 0,
|
|
115
|
+
}
|
|
116
|
+
);
|
|
79
117
|
});
|
|
80
118
|
}
|
|
81
119
|
|
|
@@ -189,7 +227,7 @@ async function get(table, filter = [], sort = [], safeDelete = null) {
|
|
|
189
227
|
request.input("param" + i, whereData.value[i]);
|
|
190
228
|
}
|
|
191
229
|
const result = await request.query(sqlStr);
|
|
192
|
-
const rows = jsonSafeParse(result.recordset || []);
|
|
230
|
+
const rows = jsonSafeParse(mapRecordset(result.recordset || []));
|
|
193
231
|
const count = await qcount(table, filter, safeDelete);
|
|
194
232
|
return { data: rows, count };
|
|
195
233
|
}
|
|
@@ -216,7 +254,7 @@ async function list(
|
|
|
216
254
|
request.input("param" + i, whereData.value[i]);
|
|
217
255
|
}
|
|
218
256
|
const result = await request.query(sqlStr);
|
|
219
|
-
const rows = jsonSafeParse(result.recordset || []);
|
|
257
|
+
const rows = jsonSafeParse(mapRecordset(result.recordset || []));
|
|
220
258
|
const count = await qcount(table, filter, safeDelete);
|
|
221
259
|
return { data: rows, count };
|
|
222
260
|
}
|
package/src/mysql/db.js
CHANGED
|
@@ -4,6 +4,9 @@ let pool = null;
|
|
|
4
4
|
const WHERE_INVALID = "Invalid filter object";
|
|
5
5
|
|
|
6
6
|
function connect(credentails) {
|
|
7
|
+
// Force UTC timezone so timestamps are consistent
|
|
8
|
+
// dateStrings: true returns raw strings without JS Date conversion
|
|
9
|
+
credentails.timezone = "+00:00";
|
|
7
10
|
pool = mysql.createPool(credentails);
|
|
8
11
|
return pool;
|
|
9
12
|
}
|
package/src/postgres/db.js
CHANGED
|
@@ -13,6 +13,7 @@ function sanitizeValue(v) {
|
|
|
13
13
|
return v
|
|
14
14
|
.replace("T", " ")
|
|
15
15
|
.replace(/[+-]\d{2}:\d{2}$/, "")
|
|
16
|
+
.replace(/Z$/, "")
|
|
16
17
|
.slice(0, 19);
|
|
17
18
|
}
|
|
18
19
|
return v;
|
|
@@ -75,6 +76,11 @@ function connect(config) {
|
|
|
75
76
|
|
|
76
77
|
pool = new Pool(poolConfig);
|
|
77
78
|
|
|
79
|
+
// Set session timezone to UTC for consistent timestamp handling
|
|
80
|
+
pool.on("connect", (client) => {
|
|
81
|
+
client.query("SET timezone = 'UTC'");
|
|
82
|
+
});
|
|
83
|
+
|
|
78
84
|
pool.on("error", (err) => {
|
|
79
85
|
console.error("Unexpected PG pool error:", err.message);
|
|
80
86
|
});
|