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,388 @@
1
+ "use strict";
2
+
3
+ const express = require("express");
4
+ const createAdapterProxy = require("../adapter-proxy");
5
+ const {
6
+ parseFilterValue,
7
+ objectToFilter,
8
+ } = require("../../src/commons/validator");
9
+ const { generateCSV } = require("../utils/csv-export");
10
+ const {
11
+ generateExportFilename,
12
+ generateQueryExportFilename,
13
+ } = require("../utils/export-filename");
14
+
15
+ // Reserved query params that are NOT filter columns
16
+ const RESERVED_PARAMS = ["page", "limit", "sort", "dir"];
17
+
18
+ /**
19
+ * Extracts filter params from query string (excludes reserved params).
20
+ * Uses the library's parseFilterValue to parse operator prefixes.
21
+ * Returns filter in the library's format: [[col, op, val], ...]
22
+ */
23
+ function buildFilter(query) {
24
+ const filterObj = {};
25
+ for (const key of Object.keys(query)) {
26
+ if (RESERVED_PARAMS.includes(key)) continue;
27
+ // Skip Express-parsed nested objects (like filter[0][col])
28
+ if (typeof query[key] !== "string") continue;
29
+ filterObj[key] = query[key];
30
+ }
31
+ if (Object.keys(filterObj).length === 0) return [];
32
+ return objectToFilter(filterObj);
33
+ }
34
+
35
+ /**
36
+ * Creates API routes for the DB Manager App.
37
+ *
38
+ * @param {object} db - The library adapter instance (has list, insert, upsert, remove, query methods)
39
+ * @param {object} metaDb - The metadata database instance
40
+ * @param {string} [dbType] - The database type (defaults to process.env.DB_TYPE)
41
+ * @returns {express.Router} Express Router with all API endpoints mounted
42
+ */
43
+ function apiRoutes(db, metaDb, dbType) {
44
+ const router = express.Router();
45
+ const type = dbType || process.env.DB_TYPE || "sqlite3";
46
+ const proxy = createAdapterProxy(db, type);
47
+
48
+ // GET /api/tables — list all tables
49
+ router.get("/api/tables", async (req, res) => {
50
+ try {
51
+ const tables = await proxy.getTables();
52
+ res.json({ tables });
53
+ } catch (err) {
54
+ res
55
+ .status(500)
56
+ .json({ error: true, message: err.message || "Failed to list tables" });
57
+ }
58
+ });
59
+
60
+ // GET /api/tables/:name/schema — get column metadata
61
+ router.get("/api/tables/:name/schema", async (req, res) => {
62
+ try {
63
+ const schema = await proxy.getSchema(req.params.name);
64
+ res.json(schema);
65
+ } catch (err) {
66
+ res
67
+ .status(500)
68
+ .json({ error: true, message: err.message || "Failed to get schema" });
69
+ }
70
+ });
71
+
72
+ // GET /api/tables/:name/rows — list rows with pagination, filtering, sorting
73
+ // Filter syntax matches the library: ?name=john&age=>25&status=!inactive
74
+ router.get("/api/tables/:name/rows", async (req, res) => {
75
+ try {
76
+ const page = parseInt(req.query.page, 10) || 0;
77
+ const limit = parseInt(req.query.limit, 10) || 30;
78
+ const sort = [];
79
+ if (req.query.sort) {
80
+ const dir = (req.query.dir || "asc").toLowerCase();
81
+ sort.push(dir === "desc" ? `-${req.query.sort}` : req.query.sort);
82
+ }
83
+
84
+ const filter = buildFilter(req.query);
85
+
86
+ const result = await db.list(
87
+ req.params.name,
88
+ filter,
89
+ sort,
90
+ null,
91
+ page,
92
+ limit,
93
+ );
94
+ res.json({
95
+ data: result.data || [],
96
+ count: result.count || 0,
97
+ page,
98
+ limit,
99
+ });
100
+ } catch (err) {
101
+ res
102
+ .status(500)
103
+ .json({ error: true, message: err.message || "Failed to list rows" });
104
+ }
105
+ });
106
+
107
+ // POST /api/tables/:name/rows — insert row(s)
108
+ router.post("/api/tables/:name/rows", async (req, res) => {
109
+ try {
110
+ const { data } = req.body;
111
+ if (!data) {
112
+ return res
113
+ .status(400)
114
+ .json({ error: true, message: "Missing 'data' in request body" });
115
+ }
116
+
117
+ const table = req.params.name;
118
+ const result = await db.insert(table, data);
119
+
120
+ // Record in query history (non-fatal)
121
+ try {
122
+ const connectionId = metaDb._connectionId || 1;
123
+ metaDb.recordQuery(
124
+ connectionId,
125
+ `INSERT INTO ${table}`,
126
+ result.rows || 1,
127
+ );
128
+ } catch (_) {}
129
+
130
+ res.json(result);
131
+ } catch (err) {
132
+ res
133
+ .status(500)
134
+ .json({ error: true, message: err.message || "Failed to insert row" });
135
+ }
136
+ });
137
+
138
+ // PUT /api/tables/:name/rows — upsert row
139
+ router.put("/api/tables/:name/rows", async (req, res) => {
140
+ try {
141
+ const { data, uniqueKeys } = req.body;
142
+ if (!data) {
143
+ return res
144
+ .status(400)
145
+ .json({ error: true, message: "Missing 'data' in request body" });
146
+ }
147
+
148
+ const table = req.params.name;
149
+ const result = await db.upsert(table, data, uniqueKeys || []);
150
+
151
+ // Record in query history (non-fatal)
152
+ try {
153
+ const connectionId = metaDb._connectionId || 1;
154
+ metaDb.recordQuery(
155
+ connectionId,
156
+ `UPSERT INTO ${table}`,
157
+ result.rows || 1,
158
+ );
159
+ } catch (_) {}
160
+
161
+ res.json(result);
162
+ } catch (err) {
163
+ res
164
+ .status(500)
165
+ .json({ error: true, message: err.message || "Failed to upsert row" });
166
+ }
167
+ });
168
+
169
+ // DELETE /api/tables/:name/rows — delete rows by PK filter
170
+ router.delete("/api/tables/:name/rows", async (req, res) => {
171
+ try {
172
+ const { keys, pkColumn } = req.body;
173
+ if (!keys || !Array.isArray(keys) || keys.length === 0) {
174
+ return res.status(400).json({
175
+ error: true,
176
+ message: "Missing or empty 'keys' array in request body",
177
+ });
178
+ }
179
+ if (!pkColumn) {
180
+ return res
181
+ .status(400)
182
+ .json({ error: true, message: "Missing 'pkColumn' in request body" });
183
+ }
184
+
185
+ const table = req.params.name;
186
+ const filter = keys.map((k) => [[pkColumn, "=", k]]);
187
+ const result = await db.remove(table, filter);
188
+
189
+ // Record in query history (non-fatal)
190
+ try {
191
+ const connectionId = metaDb._connectionId || 1;
192
+ metaDb.recordQuery(
193
+ connectionId,
194
+ `DELETE FROM ${table} WHERE ${pkColumn} IN (${keys.join(", ")})`,
195
+ keys.length,
196
+ );
197
+ } catch (_) {}
198
+
199
+ res.json(result || { message: `${keys.length} row(s) removed` });
200
+ } catch (err) {
201
+ res
202
+ .status(500)
203
+ .json({ error: true, message: err.message || "Failed to delete rows" });
204
+ }
205
+ });
206
+
207
+ // POST /api/tables/:name/export — export selected rows as CSV
208
+ router.post("/api/tables/:name/export", async (req, res) => {
209
+ try {
210
+ const { keys, pkColumn } = req.body;
211
+ if (!keys || !Array.isArray(keys) || keys.length === 0) {
212
+ return res.status(400).json({
213
+ error: true,
214
+ message: "Missing or empty 'keys' array in request body",
215
+ });
216
+ }
217
+ if (!pkColumn) {
218
+ return res
219
+ .status(400)
220
+ .json({ error: true, message: "Missing 'pkColumn' in request body" });
221
+ }
222
+
223
+ const table = req.params.name;
224
+ const filter = keys.map((k) => [[pkColumn, "=", k]]);
225
+ const result = await db.get(table, filter);
226
+ const rows = result.data || [];
227
+
228
+ // Get schema for column names
229
+ const schema = await proxy.getSchema(table);
230
+ const columns = schema.columns.map((col) => col.name);
231
+
232
+ const csv = generateCSV(columns, rows);
233
+ const filename = generateExportFilename(table);
234
+
235
+ res.setHeader("Content-Type", "text/csv");
236
+ res.setHeader(
237
+ "Content-Disposition",
238
+ `attachment; filename="${filename}"`,
239
+ );
240
+ res.send(csv);
241
+ } catch (err) {
242
+ res
243
+ .status(500)
244
+ .json({ error: true, message: err.message || "Failed to export rows" });
245
+ }
246
+ });
247
+
248
+ // GET /api/history/connections
249
+ router.get("/api/history/connections", (req, res) => {
250
+ try {
251
+ const connections = metaDb.getConnections();
252
+ res.json({ connections });
253
+ } catch (err) {
254
+ res.status(500).json({
255
+ error: true,
256
+ message: err.message || "Failed to get connection history",
257
+ });
258
+ }
259
+ });
260
+
261
+ // GET /api/history/queries
262
+ router.get("/api/history/queries", (req, res) => {
263
+ try {
264
+ const connectionId = req.query.connectionId
265
+ ? parseInt(req.query.connectionId, 10)
266
+ : metaDb._connectionId || 1;
267
+ const queries = metaDb.getQueries(connectionId);
268
+ res.json({ queries });
269
+ } catch (err) {
270
+ res.status(500).json({
271
+ error: true,
272
+ message: err.message || "Failed to get query history",
273
+ });
274
+ }
275
+ });
276
+
277
+ // POST /api/query — execute custom SQL
278
+ router.post("/api/query", async (req, res) => {
279
+ try {
280
+ const { query: queryText } = req.body;
281
+ if (!queryText) {
282
+ return res
283
+ .status(400)
284
+ .json({ error: true, message: "Missing 'query' in request body" });
285
+ }
286
+
287
+ const result = await db.query(queryText);
288
+ const data = Array.isArray(result) ? result : [];
289
+ const columns = data.length > 0 ? Object.keys(data[0]) : [];
290
+ const rowCount = data.length;
291
+
292
+ // Record in metadata DB (non-fatal)
293
+ try {
294
+ const connectionId = metaDb._connectionId || 1;
295
+ metaDb.recordQuery(connectionId, queryText, rowCount);
296
+ } catch (_) {}
297
+
298
+ res.json({ columns, data, rowCount });
299
+ } catch (err) {
300
+ res.status(500).json({
301
+ error: true,
302
+ message: err.message || "Query execution failed",
303
+ });
304
+ }
305
+ });
306
+
307
+ // POST /api/query/export — export query results as CSV
308
+ router.post("/api/query/export", async (req, res) => {
309
+ try {
310
+ const { query: queryText } = req.body;
311
+ if (!queryText) {
312
+ return res
313
+ .status(400)
314
+ .json({ error: true, message: "Missing 'query' in request body" });
315
+ }
316
+
317
+ const result = await db.query(queryText);
318
+ const data = Array.isArray(result) ? result : [];
319
+ const columns = data.length > 0 ? Object.keys(data[0]) : [];
320
+
321
+ const csv = generateCSV(columns, data);
322
+ const filename = generateQueryExportFilename();
323
+
324
+ res.setHeader("Content-Type", "text/csv");
325
+ res.setHeader(
326
+ "Content-Disposition",
327
+ `attachment; filename="${filename}"`,
328
+ );
329
+ res.send(csv);
330
+ } catch (err) {
331
+ res.status(500).json({
332
+ error: true,
333
+ message: err.message || "Query execution failed",
334
+ });
335
+ }
336
+ });
337
+
338
+ // GET /api/dashboard — table metadata
339
+ router.get("/api/dashboard", async (req, res) => {
340
+ try {
341
+ const tableNames = await proxy.getTables();
342
+ const tables = await Promise.all(
343
+ tableNames.map(async (name) => {
344
+ const schema = await proxy.getSchema(name);
345
+ const columnCount = schema.columns.length;
346
+ const result = await db.list(name, [], [], null, 0, 1);
347
+ const rowCount = result.count || 0;
348
+
349
+ let indexCount = 0;
350
+ try {
351
+ const indexes = await db.query(
352
+ `SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='index' AND tbl_name='${name}'`,
353
+ );
354
+ if (indexes && indexes[0]) indexCount = indexes[0].cnt || 0;
355
+ } catch (_) {}
356
+
357
+ let sizeMB = 0;
358
+ try {
359
+ const pageInfo = await db.query(
360
+ `SELECT SUM(pgsize) as total_size FROM dbstat WHERE name='${name}'`,
361
+ );
362
+ if (pageInfo && pageInfo[0] && pageInfo[0].total_size) {
363
+ sizeMB = parseFloat(
364
+ (pageInfo[0].total_size / (1024 * 1024)).toFixed(3),
365
+ );
366
+ }
367
+ } catch (_) {
368
+ sizeMB = parseFloat(
369
+ ((rowCount * columnCount * 50) / (1024 * 1024)).toFixed(3),
370
+ );
371
+ }
372
+
373
+ return { name, columnCount, indexCount, rowCount, sizeMB };
374
+ }),
375
+ );
376
+ res.json({ tables });
377
+ } catch (err) {
378
+ res.status(500).json({
379
+ error: true,
380
+ message: "Failed to retrieve table metadata",
381
+ });
382
+ }
383
+ });
384
+
385
+ return router;
386
+ }
387
+
388
+ module.exports = apiRoutes;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ const express = require("express");
4
+
5
+ /**
6
+ * Creates view routes for the DB Manager App.
7
+ *
8
+ * @param {object} db - The library adapter instance
9
+ * @param {object} metaDb - The metadata database instance
10
+ * @param {string} [dbType] - The database type (defaults to process.env.DB_TYPE)
11
+ * @returns {express.Router} Express Router with view endpoints mounted
12
+ */
13
+ function viewRoutes(db, metaDb, dbType) {
14
+ const router = express.Router();
15
+ const type = dbType || process.env.DB_TYPE || "sqlite3";
16
+
17
+ // GET / — redirect to dashboard
18
+ router.get("/", (req, res) => {
19
+ res.redirect("/dashboard");
20
+ });
21
+
22
+ // GET /dashboard — render dashboard page (default landing page)
23
+ router.get("/dashboard", (req, res) => {
24
+ res.render("dashboard", {
25
+ dbType: type,
26
+ dbName: process.env.DB_NAME || "unknown",
27
+ dbHost: process.env.DB_HOST || "localhost",
28
+ });
29
+ });
30
+
31
+ // GET /tables — render table browser page
32
+ router.get("/tables", (req, res) => {
33
+ res.render("index", {
34
+ dbType: type,
35
+ dbName: process.env.DB_NAME || "unknown",
36
+ dbHost: process.env.DB_HOST || "localhost",
37
+ });
38
+ });
39
+
40
+ // GET /query — render query page
41
+ router.get("/query", (req, res) => {
42
+ res.render("query", {
43
+ dbType: type,
44
+ dbName: process.env.DB_NAME || "unknown",
45
+ dbHost: process.env.DB_HOST || "localhost",
46
+ });
47
+ });
48
+
49
+ // GET /history — render history page
50
+ router.get("/history", (req, res) => {
51
+ res.render("history", {
52
+ dbType: type,
53
+ dbName: process.env.DB_NAME || "unknown",
54
+ dbHost: process.env.DB_HOST || "localhost",
55
+ });
56
+ });
57
+
58
+ return router;
59
+ }
60
+
61
+ module.exports = viewRoutes;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const express = require("express");
5
+ const apiRoutes = require("./routes/api");
6
+ const viewRoutes = require("./routes/views");
7
+
8
+ /**
9
+ * Creates and configures the Express app for the DB Manager.
10
+ *
11
+ * @param {object} db - The library adapter instance
12
+ * @param {object} metaDb - The metadata database instance
13
+ * @param {string} [dbType] - The database type (defaults to process.env.DB_TYPE)
14
+ * @returns {express.Application} Configured Express app instance
15
+ */
16
+ function createApp(db, metaDb, dbType) {
17
+ const app = express();
18
+
19
+ // View engine setup
20
+ app.set("view engine", "ejs");
21
+ app.set("views", path.join(__dirname, "views"));
22
+
23
+ // Static files
24
+ app.use(express.static(path.join(__dirname, "public")));
25
+
26
+ // Body parsing
27
+ app.use(express.json());
28
+ app.use(express.urlencoded({ extended: true }));
29
+
30
+ // Mount API routes (they already include /api prefix)
31
+ app.use("/", apiRoutes(db, metaDb, dbType));
32
+
33
+ // Mount view routes
34
+ app.use("/", viewRoutes(db, metaDb, dbType));
35
+
36
+ return app;
37
+ }
38
+
39
+ module.exports = createApp;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Builds filter bar configuration from a table schema.
5
+ * This is a pure function extracted from the client-side filter bar rendering
6
+ * logic so it can be tested without DOM dependencies.
7
+ *
8
+ * @param {{ columns: Array<{ name: string }> }} schema - Table schema
9
+ * @returns {Array<{ column: string, placeholder: string }>} Filter input configs
10
+ */
11
+ function buildFilterConfig(schema) {
12
+ if (!schema || !schema.columns) return [];
13
+ return schema.columns.map(function (col) {
14
+ return { column: col.name, placeholder: col.name };
15
+ });
16
+ }
17
+
18
+ module.exports = { buildFilterConfig };
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Escapes a single cell value for CSV (RFC 4180).
5
+ * Wraps in double quotes if value contains comma, double quote, or newline.
6
+ * Internal double quotes are doubled.
7
+ * @param {*} value - Cell value (will be stringified)
8
+ * @returns {string} Escaped CSV cell value
9
+ */
10
+ function escapeCSVCell(value) {
11
+ if (value === null || value === undefined) {
12
+ return "";
13
+ }
14
+
15
+ var str = String(value);
16
+
17
+ if (
18
+ str.indexOf('"') !== -1 ||
19
+ str.indexOf(",") !== -1 ||
20
+ str.indexOf("\n") !== -1 ||
21
+ str.indexOf("\r") !== -1
22
+ ) {
23
+ return '"' + str.replace(/"/g, '""') + '"';
24
+ }
25
+
26
+ return str;
27
+ }
28
+
29
+ /**
30
+ * Converts rows to a CSV string with proper escaping.
31
+ * @param {string[]} columns - Column names for the header row
32
+ * @param {object[]} rows - Array of row objects
33
+ * @returns {string} RFC 4180 compliant CSV string
34
+ */
35
+ function generateCSV(columns, rows) {
36
+ var lines = [];
37
+
38
+ // Header row - column names are escaped using the same rules as cell values
39
+ var header = columns
40
+ .map(function (col) {
41
+ return escapeCSVCell(col);
42
+ })
43
+ .join(",");
44
+ lines.push(header);
45
+
46
+ // Data rows
47
+ for (var i = 0; i < rows.length; i++) {
48
+ var row = rows[i];
49
+ var cells = columns.map(function (col) {
50
+ return escapeCSVCell(row[col]);
51
+ });
52
+ lines.push(cells.join(","));
53
+ }
54
+
55
+ // RFC 4180: each record ends with CRLF
56
+ return lines.join("\r\n") + "\r\n";
57
+ }
58
+
59
+ module.exports = { generateCSV, escapeCSVCell };
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Generates a timestamp string in ISO 8601 compact format (YYYYMMDDTHHmmss).
5
+ * @returns {string} Timestamp string, e.g. "20250615T143022"
6
+ */
7
+ function generateTimestamp() {
8
+ return new Date()
9
+ .toISOString()
10
+ .replace(/[-:]/g, "")
11
+ .replace(/\.\d+Z$/, "");
12
+ }
13
+
14
+ /**
15
+ * Generates an export filename for a table export.
16
+ * Format: {tableName}_{YYYYMMDDTHHmmss}.csv
17
+ * @param {string} tableName - The name of the table being exported
18
+ * @returns {string} The generated filename
19
+ */
20
+ function generateExportFilename(tableName) {
21
+ const timestamp = generateTimestamp();
22
+ return `${tableName}_${timestamp}.csv`;
23
+ }
24
+
25
+ /**
26
+ * Generates an export filename for a query export.
27
+ * Format: export_{YYYYMMDDTHHmmss}.csv
28
+ * @returns {string} The generated filename
29
+ */
30
+ function generateQueryExportFilename() {
31
+ const timestamp = generateTimestamp();
32
+ return `export_${timestamp}.csv`;
33
+ }
34
+
35
+ module.exports = {
36
+ generateExportFilename,
37
+ generateQueryExportFilename,
38
+ generateTimestamp,
39
+ };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Filters a list of table names by a search string using case-insensitive substring matching.
5
+ *
6
+ * @param {string[]} tables - Array of table name strings
7
+ * @param {string} search - The search string to filter by
8
+ * @returns {string[]} Filtered array containing only tables whose names include the search string (case-insensitive)
9
+ */
10
+ function filterTables(tables, search) {
11
+ if (!search || search.trim() === "") {
12
+ return tables.slice();
13
+ }
14
+ const needle = search.toLowerCase();
15
+ return tables.filter(function (table) {
16
+ return table.toLowerCase().indexOf(needle) !== -1;
17
+ });
18
+ }
19
+
20
+ module.exports = filterTables;