db-model-router 1.0.6 → 1.0.7

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 (137) hide show
  1. package/README.md +150 -11
  2. package/TODO.md +0 -15
  3. package/db-manager/.dbmanager.sqlite +0 -0
  4. package/db-manager/README.md +223 -0
  5. package/db-manager/adapter-proxy.js +361 -0
  6. package/db-manager/demo/cockroachdb.env +6 -0
  7. package/db-manager/demo/demo.sqlite +0 -0
  8. package/db-manager/demo/dynamodb.env +7 -0
  9. package/db-manager/demo/mongodb.env +4 -0
  10. package/db-manager/demo/mssql.env +6 -0
  11. package/db-manager/demo/mysql.env +6 -0
  12. package/db-manager/demo/oracle.env +6 -0
  13. package/db-manager/demo/postgres.env +6 -0
  14. package/db-manager/demo/redis.env +4 -0
  15. package/db-manager/demo/seeds/cockroachdb.sql +32 -0
  16. package/db-manager/demo/seeds/mssql.sql +32 -0
  17. package/db-manager/demo/seeds/mysql.sql +32 -0
  18. package/db-manager/demo/seeds/oracle.sql +43 -0
  19. package/db-manager/demo/seeds/postgres.sql +32 -0
  20. package/db-manager/demo/seeds/sqlite3.sql +32 -0
  21. package/db-manager/demo/sqlite3.env +2 -0
  22. package/db-manager/metadata-db.js +170 -0
  23. package/db-manager/public/.gitkeep +1 -0
  24. package/db-manager/public/css/style.css +1413 -0
  25. package/db-manager/public/js/app.js +1370 -0
  26. package/db-manager/routes/api.js +388 -0
  27. package/db-manager/routes/views.js +61 -0
  28. package/db-manager/server.js +39 -0
  29. package/db-manager/utils/build-filter-config.js +18 -0
  30. package/db-manager/utils/csv-export.js +59 -0
  31. package/db-manager/utils/export-filename.js +39 -0
  32. package/db-manager/utils/filter-tables.js +20 -0
  33. package/db-manager/utils/parse-filters.js +93 -0
  34. package/db-manager/utils/sort-state.js +35 -0
  35. package/db-manager/views/.gitkeep +1 -0
  36. package/db-manager/views/dashboard.ejs +53 -0
  37. package/db-manager/views/history.ejs +52 -0
  38. package/db-manager/views/index.ejs +35 -0
  39. package/db-manager/views/layout.ejs +31 -0
  40. package/db-manager/views/partials/data-panel.ejs +74 -0
  41. package/db-manager/views/partials/header.ejs +36 -0
  42. package/db-manager/views/partials/sidebar.ejs +30 -0
  43. package/db-manager/views/query.ejs +58 -0
  44. package/dbmr.schema.json +22 -44
  45. package/demo/.dockerignore +7 -0
  46. package/demo/.env.example +14 -0
  47. package/demo/Dockerfile +20 -0
  48. package/demo/app.js +39 -0
  49. package/demo/commons/add_migration.js +43 -0
  50. package/demo/commons/db.js +28 -0
  51. package/demo/commons/migrate.js +68 -0
  52. package/demo/commons/modules.js +18 -0
  53. package/demo/commons/password.js +36 -0
  54. package/demo/commons/security.js +30 -0
  55. package/demo/commons/session.js +13 -0
  56. package/demo/commons/webhook.js +81 -0
  57. package/demo/dbmr.schema.json +338 -0
  58. package/demo/middleware/authenticate.js +14 -0
  59. package/demo/middleware/hasPermission.js +30 -0
  60. package/demo/middleware/logger.js +67 -0
  61. package/demo/middleware/tenantIsolation.js +17 -0
  62. package/demo/migrations/20260509170349_create_migrations_table.sql +6 -0
  63. package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
  64. package/demo/migrations/20260509170349_create_tables.sql +193 -0
  65. package/demo/models/addresses.js +24 -0
  66. package/demo/models/cart_items.js +20 -0
  67. package/demo/models/carts.js +18 -0
  68. package/demo/models/categories.js +22 -0
  69. package/demo/models/coupons.js +25 -0
  70. package/demo/models/index.js +43 -0
  71. package/demo/models/order_items.js +23 -0
  72. package/demo/models/orders.js +27 -0
  73. package/demo/models/payments.js +23 -0
  74. package/demo/models/product_images.js +20 -0
  75. package/demo/models/product_reviews.js +22 -0
  76. package/demo/models/product_variants.js +22 -0
  77. package/demo/models/products.js +32 -0
  78. package/demo/models/role_permissions.js +17 -0
  79. package/demo/models/roles.js +17 -0
  80. package/demo/models/shipments.js +21 -0
  81. package/demo/models/tenants.js +18 -0
  82. package/demo/models/users.js +23 -0
  83. package/demo/models/webhook_logs.js +22 -0
  84. package/demo/models/webhooks.js +19 -0
  85. package/demo/models/wishlists.js +17 -0
  86. package/demo/openapi.json +7000 -0
  87. package/demo/package-lock.json +2810 -0
  88. package/demo/package.json +43 -0
  89. package/demo/routes/addresses/index.js +6 -0
  90. package/demo/routes/auth/index.js +55 -0
  91. package/demo/routes/carts/cart_items/index.js +7 -0
  92. package/demo/routes/carts/index.js +6 -0
  93. package/demo/routes/categories/index.js +6 -0
  94. package/demo/routes/coupons/index.js +6 -0
  95. package/demo/routes/docs.js +18 -0
  96. package/demo/routes/health.js +35 -0
  97. package/demo/routes/index.js +54 -0
  98. package/demo/routes/orders/index.js +6 -0
  99. package/demo/routes/orders/order_items/index.js +7 -0
  100. package/demo/routes/orders/payments/index.js +7 -0
  101. package/demo/routes/orders/shipments/index.js +7 -0
  102. package/demo/routes/products/index.js +6 -0
  103. package/demo/routes/products/product_images/index.js +7 -0
  104. package/demo/routes/products/product_reviews/index.js +7 -0
  105. package/demo/routes/products/product_variants/index.js +7 -0
  106. package/demo/routes/roles/index.js +75 -0
  107. package/demo/routes/roles/permissions/index.js +47 -0
  108. package/demo/routes/tenants/index.js +45 -0
  109. package/demo/routes/users/index.js +45 -0
  110. package/demo/routes/wishlists/index.js +6 -0
  111. package/demo/seeds/saas-seed.js +329 -0
  112. package/docker-compose.yml +61 -0
  113. package/package.json +120 -113
  114. package/scripts/demo-create.js +1 -1
  115. package/skill/SKILL.md +119 -3
  116. package/src/cli/commands/db-manager.js +134 -0
  117. package/src/cli/commands/generate.js +106 -60
  118. package/src/cli/commands/help.js +0 -1
  119. package/src/cli/generate-route.js +60 -21
  120. package/src/cli/generate-saas-structure.js +122 -0
  121. package/src/cli/init/generators.js +6 -0
  122. package/src/cli/init.js +8 -0
  123. package/src/cli/main.js +8 -1
  124. package/src/cli/saas/generate-saas-middleware.js +108 -0
  125. package/src/cli/saas/generate-saas-migrations.js +480 -0
  126. package/src/cli/saas/generate-saas-models.js +211 -0
  127. package/src/cli/saas/generate-saas-openapi.js +419 -0
  128. package/src/cli/saas/generate-saas-routes.js +435 -0
  129. package/src/cli/saas/generate-saas-seeds.js +243 -0
  130. package/src/cli/saas/generate-saas-utils.js +176 -0
  131. package/src/commons/kafka.js +139 -0
  132. package/src/commons/model.js +29 -9
  133. package/src/index.js +2 -0
  134. package/src/mssql/db.js +41 -3
  135. package/src/mysql/db.js +3 -0
  136. package/src/postgres/db.js +6 -0
  137. package/src/cli/generate-db-manager.js +0 -1573
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * SaaS utility generators.
5
+ *
6
+ * Each function returns the file content string for a commons/ utility module.
7
+ * Generated code uses ES6 module syntax (import/export).
8
+ */
9
+
10
+ /**
11
+ * Generate the content for `commons/password.js`.
12
+ *
13
+ * @returns {string} File content for commons/password.js
14
+ */
15
+ function generatePasswordUtil() {
16
+ return `import crypto from "crypto";
17
+
18
+ /**
19
+ * Hash a password using scrypt with a random salt.
20
+ * Returns a string in the format "salt:derivedKey" (both hex-encoded).
21
+ *
22
+ * @param {string} password - The plaintext password to hash
23
+ * @returns {Promise<string>} The hashed password string
24
+ */
25
+ export function hashPassword(password) {
26
+ const salt = crypto.randomBytes(16).toString("hex");
27
+ return new Promise((resolve, reject) => {
28
+ crypto.scrypt(password, salt, 64, (err, derivedKey) => {
29
+ if (err) reject(err);
30
+ resolve(salt + ":" + derivedKey.toString("hex"));
31
+ });
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Verify a password against a previously hashed value.
37
+ * Uses timing-safe comparison to prevent timing attacks.
38
+ *
39
+ * @param {string} password - The plaintext password to verify
40
+ * @param {string} hash - The stored hash in "salt:derivedKey" format
41
+ * @returns {Promise<boolean>} True if the password matches, false otherwise
42
+ */
43
+ export function verifyPassword(password, hash) {
44
+ const [salt, key] = hash.split(":");
45
+ return new Promise((resolve, reject) => {
46
+ crypto.scrypt(password, salt, 64, (err, derivedKey) => {
47
+ if (err) reject(err);
48
+ resolve(crypto.timingSafeEqual(Buffer.from(key, "hex"), derivedKey));
49
+ });
50
+ });
51
+ }
52
+ `;
53
+ }
54
+
55
+ /**
56
+ * Generate the content for `commons/modules.js`.
57
+ *
58
+ * @returns {string} File content for commons/modules.js
59
+ */
60
+ function generateModulesUtil() {
61
+ return `/**
62
+ * Registry of all SaaS module names.
63
+ * This is the single source of truth for valid module identifiers
64
+ * used by the permission system.
65
+ *
66
+ * @type {string[]}
67
+ */
68
+ export const modules = ["users", "tenants", "roles", "permissions", "webhooks"];
69
+
70
+ /**
71
+ * Check whether a given name is a registered module.
72
+ *
73
+ * @param {string} name - The module name to validate
74
+ * @returns {boolean} True if the name exists in the modules registry
75
+ */
76
+ export function isValidModule(name) {
77
+ return modules.includes(name);
78
+ }
79
+ `;
80
+ }
81
+
82
+ /**
83
+ * Generate the content for `commons/webhook.js`.
84
+ *
85
+ * @returns {string} File content for commons/webhook.js
86
+ */
87
+ function generateWebhookUtil() {
88
+ return `import crypto from "crypto";
89
+
90
+ /**
91
+ * Retry delay schedule in seconds.
92
+ * Attempt 0: immediate, 1: 1 min, 2: 5 min, 3: 1 hour, 4: 1 day.
93
+ *
94
+ * @type {number[]}
95
+ */
96
+ export const RETRY_DELAYS = [0, 60, 300, 3600, 86400];
97
+
98
+ /**
99
+ * Sign a webhook payload using HMAC-SHA256.
100
+ *
101
+ * @param {object} payload - The payload object to sign
102
+ * @param {string} secret - The tenant's webhook secret
103
+ * @returns {string} Hex-encoded HMAC-SHA256 signature
104
+ */
105
+ export function signPayload(payload, secret) {
106
+ const body = JSON.stringify(payload);
107
+ return crypto.createHmac("sha256", secret).update(body).digest("hex");
108
+ }
109
+
110
+ /**
111
+ * Look up the configured webhook for a tenant.
112
+ * TODO: Replace this stub with actual database lookup.
113
+ */
114
+ export async function lookupWebhook(tenantId) {
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * Log a webhook delivery event.
120
+ * TODO: Replace this stub with actual database insert into webhook_logs.
121
+ */
122
+ export async function logWebhookEvent(webhookId, tenantId, eventType, payload, status, responseBody, responseStatusCode) {
123
+ // Stub: replace with actual DB insert
124
+ }
125
+
126
+ /**
127
+ * Delay execution for the specified number of milliseconds.
128
+ */
129
+ export function delay(ms) {
130
+ return new Promise((resolve) => setTimeout(resolve, ms));
131
+ }
132
+
133
+ /**
134
+ * Send a webhook notification to the configured endpoint for a tenant.
135
+ * Retries delivery up to 5 times with exponential backoff.
136
+ */
137
+ export async function sendWebhook(tenantId, event, context) {
138
+ const webhook = await lookupWebhook(tenantId);
139
+ if (!webhook) return;
140
+
141
+ const payload = { context, event, timestamp: new Date().toISOString() };
142
+ payload.signature = signPayload(payload, webhook.secret);
143
+
144
+ for (let attempt = 0; attempt < RETRY_DELAYS.length; attempt++) {
145
+ if (attempt > 0) {
146
+ await delay(RETRY_DELAYS[attempt] * 1000);
147
+ console.log(\`Webhook retry attempt \${attempt}, delay: \${RETRY_DELAYS[attempt]}s\`);
148
+ }
149
+ try {
150
+ const response = await fetch(webhook.url, {
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/json", "X-Webhook-Key": webhook.key },
153
+ body: JSON.stringify(payload),
154
+ });
155
+ await logWebhookEvent(
156
+ webhook.id, tenantId, event.type, payload,
157
+ response.ok ? "success" : "failed",
158
+ await response.text(), response.status
159
+ );
160
+ if (response.ok) return;
161
+ } catch (err) {
162
+ await logWebhookEvent(
163
+ webhook.id, tenantId, event.type, payload,
164
+ "error", err.message, null
165
+ );
166
+ }
167
+ }
168
+ }
169
+ `;
170
+ }
171
+
172
+ module.exports = {
173
+ generatePasswordUtil,
174
+ generateModulesUtil,
175
+ generateWebhookUtil,
176
+ };
@@ -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
+ };
@@ -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
- return getResult.count > 0 ? getResult["data"][0] : null;
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
- return getResult.count > 0 ? getResult["data"][0] : null;
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) return result["data"][0];
258
- else return null;
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
- return getResult.count > 0 ? getResult["data"][0] : null;
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) return result["data"][0];
297
- else return null;
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
- return await db.remove(table, filter, option.safeDelete);
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) return result["data"][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/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 result.recordset || { affectedRows: result.rowsAffected?.[0] || 0 };
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
  }
@@ -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
  });