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.
Files changed (140) 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 +15 -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 +17 -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 +19 -0
  62. package/demo/migrations/20260510092158_create_migrations_table.sql +6 -0
  63. package/demo/migrations/20260510092159_create_saas_tables.sql +69 -0
  64. package/demo/migrations/20260510092159_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 +2827 -0
  88. package/demo/package.json +42 -0
  89. package/demo/routes/addresses/index.js +10 -0
  90. package/demo/routes/auth/index.js +55 -0
  91. package/demo/routes/carts/cart_items/index.js +11 -0
  92. package/demo/routes/carts/index.js +14 -0
  93. package/demo/routes/categories/index.js +10 -0
  94. package/demo/routes/coupons/index.js +10 -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 +18 -0
  99. package/demo/routes/orders/order_items/index.js +11 -0
  100. package/demo/routes/orders/payments/index.js +11 -0
  101. package/demo/routes/orders/shipments/index.js +11 -0
  102. package/demo/routes/products/index.js +18 -0
  103. package/demo/routes/products/product_images/index.js +11 -0
  104. package/demo/routes/products/product_reviews/index.js +11 -0
  105. package/demo/routes/products/product_variants/index.js +11 -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 +10 -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 +66 -27
  120. package/src/cli/generate-saas-structure.js +129 -0
  121. package/src/cli/init/dependencies.js +1 -1
  122. package/src/cli/init/generators.js +6 -77
  123. package/src/cli/init.js +9 -2
  124. package/src/cli/main.js +8 -1
  125. package/src/cli/saas/generate-saas-middleware.js +110 -0
  126. package/src/cli/saas/generate-saas-migrations.js +480 -0
  127. package/src/cli/saas/generate-saas-models.js +211 -0
  128. package/src/cli/saas/generate-saas-openapi.js +419 -0
  129. package/src/cli/saas/generate-saas-routes.js +435 -0
  130. package/src/cli/saas/generate-saas-seeds.js +243 -0
  131. package/src/cli/saas/generate-saas-tests.js +473 -0
  132. package/src/cli/saas/generate-saas-utils.js +176 -0
  133. package/src/commons/kafka.js +139 -0
  134. package/src/commons/model.js +29 -9
  135. package/src/commons/route.js +6 -6
  136. package/src/index.js +2 -0
  137. package/src/mssql/db.js +41 -3
  138. package/src/mysql/db.js +3 -0
  139. package/src/postgres/db.js +6 -0
  140. 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
+ };
@@ -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,
@@ -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 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
  });