db-model-router 1.0.5 → 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.
- package/README.md +150 -11
- package/TODO.md +0 -14
- 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 +23 -45
- package/demo/.env.example +1 -0
- package/demo/app.js +3 -1
- package/demo/commons/db.js +11 -0
- package/demo/commons/migrate.js +3 -0
- package/demo/commons/modules.js +18 -0
- package/demo/commons/password.js +36 -0
- package/demo/commons/webhook.js +81 -0
- package/demo/dbmr.schema.json +22 -46
- package/demo/middleware/authenticate.js +14 -0
- package/demo/middleware/hasPermission.js +30 -0
- package/demo/middleware/tenantIsolation.js +17 -0
- package/demo/migrations/20260509170349_create_saas_tables.sql +69 -0
- package/demo/migrations/{20260430155809_create_tables.sql → 20260509170349_create_tables.sql} +11 -25
- package/demo/models/addresses.js +5 -3
- package/demo/models/cart_items.js +5 -3
- package/demo/models/carts.js +5 -3
- package/demo/models/categories.js +5 -3
- package/demo/models/coupons.js +5 -3
- package/demo/models/index.js +43 -0
- package/demo/models/order_items.js +4 -2
- package/demo/models/orders.js +5 -3
- package/demo/models/payments.js +5 -3
- package/demo/models/product_images.js +4 -2
- package/demo/models/product_reviews.js +5 -3
- package/demo/models/product_variants.js +5 -3
- package/demo/models/products.js +5 -3
- package/demo/models/role_permissions.js +17 -0
- package/demo/models/roles.js +17 -0
- package/demo/models/shipments.js +5 -3
- package/demo/models/tenants.js +18 -0
- package/demo/models/users.js +12 -8
- package/demo/models/webhook_logs.js +22 -0
- package/demo/models/webhooks.js +19 -0
- package/demo/models/wishlists.js +4 -2
- package/demo/openapi.json +1744 -616
- package/demo/package-lock.json +24 -24
- package/demo/package.json +9 -0
- package/demo/routes/{addresses.js → addresses/index.js} +1 -1
- package/demo/routes/auth/index.js +55 -0
- package/demo/routes/carts/{cart_items.js → cart_items/index.js} +1 -1
- package/demo/routes/{carts.js → carts/index.js} +1 -1
- package/demo/routes/{categories.js → categories/index.js} +1 -1
- package/demo/routes/{coupons.js → coupons/index.js} +1 -1
- package/demo/routes/index.js +39 -24
- package/demo/routes/{orders.js → orders/index.js} +1 -1
- package/demo/routes/orders/{order_items.js → order_items/index.js} +1 -1
- package/demo/routes/orders/{payments.js → payments/index.js} +1 -1
- package/demo/routes/orders/{shipments.js → shipments/index.js} +1 -1
- package/demo/routes/{products.js → products/index.js} +1 -1
- package/demo/routes/products/{product_images.js → product_images/index.js} +1 -1
- package/demo/routes/products/{product_reviews.js → product_reviews/index.js} +1 -1
- package/demo/routes/products/{product_variants.js → product_variants/index.js} +1 -1
- 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.js → wishlists/index.js} +1 -1
- 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 +112 -43
- package/src/cli/commands/help.js +0 -1
- package/src/cli/diff-engine.js +2 -1
- package/src/cli/generate-model.js +9 -4
- package/src/cli/generate-openapi.js +40 -13
- package/src/cli/generate-route.js +61 -22
- package/src/cli/generate-saas-structure.js +122 -0
- package/src/cli/init/generators.js +42 -30
- package/src/cli/init.js +8 -0
- package/src/cli/main.js +8 -1
- package/src/cli/saas/generate-saas-middleware.js +108 -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-utils.js +176 -0
- package/src/commons/kafka.js +139 -0
- package/src/commons/model.js +29 -9
- 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/sqlite3/db.js +11 -0
- package/demo/docs/llm.md +0 -197
- package/demo/llms.txt +0 -70
- package/demo/routes/users.js +0 -6
- package/src/cli/commands/generate-llm-docs.js +0 -418
- /package/demo/migrations/{20260430155808_create_migrations_table.sql → 20260509170349_create_migrations_table.sql} +0 -0
|
@@ -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;
|