db-model-router 1.0.4 → 1.0.6

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 (46) hide show
  1. package/README.md +110 -16
  2. package/TODO.md +15 -0
  3. package/dbmr.schema.json +333 -0
  4. package/docker-compose.yml +1 -1
  5. package/package.json +8 -7
  6. package/scripts/demo-create.js +47 -0
  7. package/skill/SKILL.md +464 -0
  8. package/skill/references/cockroachdb.md +49 -0
  9. package/skill/references/dynamodb.md +53 -0
  10. package/skill/references/mongodb.md +56 -0
  11. package/skill/references/mssql.md +55 -0
  12. package/skill/references/oracle.md +52 -0
  13. package/skill/references/postgres.md +50 -0
  14. package/skill/references/redis.md +53 -0
  15. package/skill/references/sqlite3.md +43 -0
  16. package/src/cli/commands/generate.js +95 -31
  17. package/src/cli/commands/help.js +12 -7
  18. package/src/cli/commands/init.js +2 -2
  19. package/src/cli/commands/inspect.js +1 -0
  20. package/src/cli/diff-engine.js +54 -23
  21. package/src/cli/generate-db-manager.js +1573 -0
  22. package/src/cli/generate-docs-route.js +31 -0
  23. package/src/cli/generate-migration.js +356 -0
  24. package/src/cli/generate-model.js +9 -4
  25. package/src/cli/generate-openapi.js +40 -13
  26. package/src/cli/generate-route.js +55 -27
  27. package/src/cli/init/dependencies.js +3 -0
  28. package/src/cli/init/generators.js +37 -31
  29. package/src/cli/init.js +8 -8
  30. package/src/cli/main.js +2 -2
  31. package/src/cockroachdb/db.js +90 -59
  32. package/src/commons/route.js +20 -20
  33. package/src/commons/validator.js +58 -1
  34. package/src/dynamodb/db.js +50 -27
  35. package/src/mongodb/db.js +1 -0
  36. package/src/mssql/db.js +89 -61
  37. package/src/mysql/db.js +1 -0
  38. package/src/oracle/db.js +1 -0
  39. package/src/postgres/db.js +61 -41
  40. package/src/redis/db.js +1 -0
  41. package/src/schema/schema-parser.js +43 -1
  42. package/src/schema/schema-printer.js +7 -0
  43. package/src/schema/schema-validator.js +17 -0
  44. package/src/sqlite3/db.js +12 -0
  45. package/docs/SKILL.md +0 -419
  46. package/src/cli/commands/generate-llm-docs.js +0 -418
@@ -0,0 +1,1573 @@
1
+ "use strict";
2
+
3
+ const SQL_ADAPTERS = [
4
+ "mysql",
5
+ "mariadb",
6
+ "postgres",
7
+ "sqlite3",
8
+ "mssql",
9
+ "cockroachdb",
10
+ "oracle",
11
+ ];
12
+
13
+ /**
14
+ * Generate the auth middleware content string.
15
+ * Returns an ES module that exports a default middleware function
16
+ * checking req.session["db-manager"] === true.
17
+ *
18
+ * @returns {string}
19
+ */
20
+ function generateDbManagerAuthMiddleware() {
21
+ return [
22
+ "export default function dbManagerAuth(req, res, next) {",
23
+ ' if (req.session["db-manager"] === true) {',
24
+ " return next();",
25
+ " }",
26
+ ' res.redirect("/database/login");',
27
+ "}",
28
+ "",
29
+ ].join("\n");
30
+ }
31
+
32
+ /**
33
+ * Generate the login page EJS template content string.
34
+ * Returns a complete HTML page with dark theme, password form,
35
+ * error display, and 503 not-configured message.
36
+ *
37
+ * @returns {string}
38
+ */
39
+ function generateLoginTemplate() {
40
+ return [
41
+ "<!DOCTYPE html>",
42
+ '<html lang="en">',
43
+ "<head>",
44
+ ' <meta charset="UTF-8">',
45
+ ' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
46
+ " <title>DB Manager - Login</title>",
47
+ " <style>",
48
+ " * { margin: 0; padding: 0; box-sizing: border-box; }",
49
+ " body {",
50
+ ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
51
+ " background: #1a1a2e;",
52
+ " color: #e0e0e0;",
53
+ " display: flex;",
54
+ " align-items: center;",
55
+ " justify-content: center;",
56
+ " min-height: 100vh;",
57
+ " }",
58
+ " .login-container {",
59
+ " background: #16213e;",
60
+ " padding: 2rem;",
61
+ " border-radius: 8px;",
62
+ " width: 100%;",
63
+ " max-width: 400px;",
64
+ " box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);",
65
+ " }",
66
+ " h1 {",
67
+ " text-align: center;",
68
+ " margin-bottom: 1.5rem;",
69
+ " color: #e0e0e0;",
70
+ " font-size: 1.5rem;",
71
+ " }",
72
+ " .error {",
73
+ " background: #e74c3c;",
74
+ " color: #fff;",
75
+ " padding: 0.75rem;",
76
+ " border-radius: 4px;",
77
+ " margin-bottom: 1rem;",
78
+ " text-align: center;",
79
+ " }",
80
+ " .not-configured {",
81
+ " background: #e67e22;",
82
+ " color: #fff;",
83
+ " padding: 0.75rem;",
84
+ " border-radius: 4px;",
85
+ " margin-bottom: 1rem;",
86
+ " text-align: center;",
87
+ " }",
88
+ " label {",
89
+ " display: block;",
90
+ " margin-bottom: 0.5rem;",
91
+ " color: #b0b0b0;",
92
+ " }",
93
+ ' input[type="password"] {',
94
+ " width: 100%;",
95
+ " padding: 0.75rem;",
96
+ " border: 1px solid #2a2a4a;",
97
+ " border-radius: 4px;",
98
+ " background: #0f3460;",
99
+ " color: #e0e0e0;",
100
+ " font-size: 1rem;",
101
+ " margin-bottom: 1rem;",
102
+ " }",
103
+ ' input[type="password"]:focus {',
104
+ " outline: none;",
105
+ " border-color: #533483;",
106
+ " }",
107
+ " button {",
108
+ " width: 100%;",
109
+ " padding: 0.75rem;",
110
+ " background: #533483;",
111
+ " color: #fff;",
112
+ " border: none;",
113
+ " border-radius: 4px;",
114
+ " font-size: 1rem;",
115
+ " cursor: pointer;",
116
+ " }",
117
+ " button:hover {",
118
+ " background: #6a42a0;",
119
+ " }",
120
+ " </style>",
121
+ "</head>",
122
+ "<body>",
123
+ ' <div class="login-container">',
124
+ " <h1>DB Manager</h1>",
125
+ " <% if (locals.notConfigured) { %>",
126
+ ' <div class="not-configured">503 \\u2014 Database manager password is not configured.</div>',
127
+ " <% } %>",
128
+ " <% if (locals.error) { %>",
129
+ ' <div class="error"><%= error %></div>',
130
+ " <% } %>",
131
+ ' <form method="POST" action="/database/login">',
132
+ ' <label for="password">Password</label>',
133
+ ' <input type="password" id="password" name="password" required>',
134
+ ' <button type="submit">Login</button>',
135
+ " </form>",
136
+ " </div>",
137
+ "</body>",
138
+ "</html>",
139
+ "",
140
+ ].join("\n");
141
+ }
142
+
143
+ /**
144
+ * Generate the manager page EJS template content string.
145
+ * Returns a complete HTML page with dark theme, left sidebar with table list,
146
+ * three tabs (Structure, Data, Query), and inline vanilla JavaScript for
147
+ * all client-side interactivity.
148
+ *
149
+ * @returns {string}
150
+ */
151
+ function generateManagerTemplate() {
152
+ var lines = [];
153
+ function p(s) {
154
+ lines.push(s);
155
+ }
156
+
157
+ // HTML head
158
+ p("<!DOCTYPE html>");
159
+ p('<html lang="en">');
160
+ p("<head>");
161
+ p(' <meta charset="UTF-8">');
162
+ p(' <meta name="viewport" content="width=device-width, initial-scale=1.0">');
163
+ p(" <title>DB Manager</title>");
164
+ p(" <style>");
165
+ p(" * { margin: 0; padding: 0; box-sizing: border-box; }");
166
+ p(" body {");
167
+ p(
168
+ ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
169
+ );
170
+ p(
171
+ " background: #1a1a2e; color: #e0e0e0; display: flex; min-height: 100vh;",
172
+ );
173
+ p(" }");
174
+ p(
175
+ " .sidebar { width: 240px; min-width: 240px; background: #16213e; border-right: 1px solid #2a2a4a; display: flex; flex-direction: column; overflow: hidden; }",
176
+ );
177
+ p(" .sidebar-header { padding: 1rem; border-bottom: 1px solid #2a2a4a; }");
178
+ p(
179
+ " .sidebar-header h2 { font-size: 1.1rem; margin-bottom: 0.75rem; color: #e0e0e0; }",
180
+ );
181
+ p(
182
+ " .sidebar-header input { width: 100%; padding: 0.5rem; border: 1px solid #2a2a4a; border-radius: 4px; background: #0f3460; color: #e0e0e0; font-size: 0.875rem; }",
183
+ );
184
+ p(
185
+ " .sidebar-header input:focus { outline: none; border-color: #533483; }",
186
+ );
187
+ p(" .table-list { flex: 1; overflow-y: auto; list-style: none; }");
188
+ p(
189
+ " .table-list li { padding: 0.6rem 1rem; cursor: pointer; border-bottom: 1px solid #2a2a4a; font-size: 0.875rem; transition: background 0.15s; }",
190
+ );
191
+ p(" .table-list li:hover { background: #0f3460; }");
192
+ p(" .table-list li.active { background: #533483; color: #fff; }");
193
+ p(
194
+ " .main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }",
195
+ );
196
+ p(
197
+ " .main-header { padding: 1rem; background: #16213e; border-bottom: 1px solid #2a2a4a; }",
198
+ );
199
+ p(" .main-header h1 { font-size: 1.25rem; color: #e0e0e0; }");
200
+ p(
201
+ " .tabs { display: flex; background: #16213e; border-bottom: 1px solid #2a2a4a; }",
202
+ );
203
+ p(
204
+ " .tab-btn { padding: 0.75rem 1.5rem; background: none; border: none; color: #b0b0b0; cursor: pointer; font-size: 0.9rem; border-bottom: 2px solid transparent; width: auto; }",
205
+ );
206
+ p(" .tab-btn:hover { color: #e0e0e0; }");
207
+ p(" .tab-btn.active { color: #e0e0e0; border-bottom-color: #533483; }");
208
+ p(
209
+ " .tab-content { display: none; flex: 1; overflow: auto; padding: 1rem; }",
210
+ );
211
+ p(" .tab-content.active { display: block; }");
212
+ p(
213
+ " table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }",
214
+ );
215
+ p(
216
+ " th, td { padding: 0.5rem 0.75rem; text-align: left; border-bottom: 1px solid #2a2a4a; }",
217
+ );
218
+ p(
219
+ " th { background: #0f3460; color: #e0e0e0; font-weight: 600; cursor: pointer; user-select: none; white-space: nowrap; }",
220
+ );
221
+ p(" th:hover { background: #12407a; }");
222
+ p(" td { background: #16213e; color: #d0d0d0; }");
223
+ p(" tr:hover td { background: #1a2a4e; }");
224
+ p(
225
+ " .btn { padding: 0.4rem 0.8rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.8rem; color: #fff; width: auto; }",
226
+ );
227
+ p(" .btn-primary { background: #533483; }");
228
+ p(" .btn-primary:hover { background: #6a42a0; }");
229
+ p(" .btn-danger { background: #e74c3c; }");
230
+ p(" .btn-danger:hover { background: #c0392b; }");
231
+ p(" .btn-success { background: #27ae60; }");
232
+ p(" .btn-success:hover { background: #219a52; }");
233
+ p(" .btn-secondary { background: #555; }");
234
+ p(" .btn-secondary:hover { background: #666; }");
235
+ p(
236
+ " .data-toolbar { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; flex-wrap: wrap; align-items: center; }",
237
+ );
238
+ p(
239
+ " .pagination { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.75rem; }",
240
+ );
241
+ p(
242
+ " .pagination select, .pagination span { font-size: 0.8rem; color: #e0e0e0; }",
243
+ );
244
+ p(
245
+ " .pagination select { background: #0f3460; border: 1px solid #2a2a4a; color: #e0e0e0; padding: 0.25rem; border-radius: 4px; }",
246
+ );
247
+ p(
248
+ " .filter-row input { width: 100%; padding: 0.3rem 0.5rem; background: #0f3460; border: 1px solid #2a2a4a; border-radius: 4px; color: #e0e0e0; font-size: 0.8rem; }",
249
+ );
250
+ p(" .filter-row input:focus { outline: none; border-color: #533483; }");
251
+ p(
252
+ " .query-area { width: 100%; min-height: 120px; padding: 0.75rem; background: #0f3460; border: 1px solid #2a2a4a; border-radius: 4px; color: #e0e0e0; font-family: monospace; font-size: 0.875rem; resize: vertical; margin-bottom: 0.75rem; }",
253
+ );
254
+ p(" .query-area:focus { outline: none; border-color: #533483; }");
255
+ p(
256
+ " .query-error { background: #e74c3c; color: #fff; padding: 0.75rem; border-radius: 4px; margin-top: 0.75rem; }",
257
+ );
258
+ p(
259
+ ' td input[type="text"], td input[type="number"] { width: 100%; padding: 0.25rem 0.4rem; background: #0f3460; border: 1px solid #2a2a4a; border-radius: 3px; color: #e0e0e0; font-size: 0.8rem; }',
260
+ );
261
+ p(
262
+ " .add-row-form { background: #0f3460; padding: 1rem; border-radius: 4px; margin-bottom: 0.75rem; display: none; }",
263
+ );
264
+ p(
265
+ " .add-row-form .form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.5rem; margin-bottom: 0.75rem; }",
266
+ );
267
+ p(
268
+ " .add-row-form label { display: block; font-size: 0.75rem; color: #b0b0b0; margin-bottom: 0.25rem; }",
269
+ );
270
+ p(
271
+ " .add-row-form input, .add-row-form select { width: 100%; padding: 0.4rem; background: #16213e; border: 1px solid #2a2a4a; border-radius: 3px; color: #e0e0e0; font-size: 0.8rem; }",
272
+ );
273
+ p(" .add-row-form .required-mark { color: #e74c3c; }");
274
+ p(
275
+ " .message { padding: 0.5rem 0.75rem; border-radius: 4px; margin-bottom: 0.75rem; font-size: 0.85rem; }",
276
+ );
277
+ p(" .message-success { background: #27ae60; color: #fff; }");
278
+ p(" .message-error { background: #e74c3c; color: #fff; }");
279
+ p(' input[type="checkbox"] { cursor: pointer; width: auto; }');
280
+ p(
281
+ " .placeholder { display: flex; align-items: center; justify-content: center; flex: 1; color: #666; font-size: 1.1rem; }",
282
+ );
283
+ p(" </style>");
284
+ p("</head>");
285
+ p("<body>");
286
+
287
+ // Sidebar
288
+ p(' <div class="sidebar">');
289
+ p(' <div class="sidebar-header">');
290
+ p(" <h2>Tables</h2>");
291
+ p(
292
+ ' <input type="text" id="tableSearch" placeholder="Search tables..." aria-label="Search tables">',
293
+ );
294
+ p(" </div>");
295
+ p(' <ul class="table-list" id="tableList"></ul>');
296
+ p(" </div>");
297
+
298
+ // Main content area
299
+ p(' <div class="main">');
300
+ p(' <div class="main-header">');
301
+ p(' <h1 id="tableTitle">Select a table</h1>');
302
+ p(" </div>");
303
+ p(' <div class="tabs" id="tabBar" style="display:none;">');
304
+ p(
305
+ ' <button class="tab-btn active" data-tab="structure">Structure</button>',
306
+ );
307
+ p(' <button class="tab-btn" data-tab="data">Data</button>');
308
+ p(' <button class="tab-btn" data-tab="query">Query</button>');
309
+ p(" </div>");
310
+ p(
311
+ ' <div id="placeholder" class="placeholder">Choose a table from the sidebar to get started.</div>',
312
+ );
313
+
314
+ // Structure tab
315
+ p(' <div class="tab-content active" id="tab-structure">');
316
+ p(' <div id="structureContent"></div>');
317
+ p(" </div>");
318
+
319
+ // Data tab
320
+ p(' <div class="tab-content" id="tab-data">');
321
+ p(' <div id="dataMessage"></div>');
322
+ p(' <div class="data-toolbar">');
323
+ p(' <button class="btn btn-primary" id="addRowBtn">Add Row</button>');
324
+ p(
325
+ ' <button class="btn btn-danger" id="deleteSelectedBtn" style="display:none;">Delete Selected</button>',
326
+ );
327
+ p(
328
+ ' <button class="btn btn-secondary" id="downloadCsvBtn">Download CSV</button>',
329
+ );
330
+ p(" </div>");
331
+ p(' <div class="add-row-form" id="addRowForm">');
332
+ p(
333
+ ' <h3 style="margin-bottom:0.75rem;font-size:0.95rem;">Add New Row</h3>',
334
+ );
335
+ p(' <div class="form-grid" id="addRowFields"></div>');
336
+ p(
337
+ ' <div id="addRowError" class="message message-error" style="display:none;"></div>',
338
+ );
339
+ p(' <div style="display:flex;gap:0.5rem;">');
340
+ p(
341
+ ' <button class="btn btn-success" id="addRowSubmit">Insert</button>',
342
+ );
343
+ p(
344
+ ' <button class="btn btn-secondary" id="addRowCancel">Cancel</button>',
345
+ );
346
+ p(" </div>");
347
+ p(" </div>");
348
+ p(' <div id="dataTableContainer"></div>');
349
+ p(
350
+ ' <div class="pagination" id="paginationControls" style="display:none;">',
351
+ );
352
+ p(' <button class="btn btn-secondary" id="prevPageBtn">Prev</button>');
353
+ p(' <span id="pageInfo"></span>');
354
+ p(' <button class="btn btn-secondary" id="nextPageBtn">Next</button>');
355
+ p(
356
+ ' <label style="margin-left:0.5rem;font-size:0.8rem;color:#b0b0b0;">',
357
+ );
358
+ p(" Page size:");
359
+ p(' <select id="pageSizeSelect">');
360
+ p(' <option value="10">10</option>');
361
+ p(' <option value="30" selected>30</option>');
362
+ p(' <option value="50">50</option>');
363
+ p(' <option value="100">100</option>');
364
+ p(" </select>");
365
+ p(" </label>");
366
+ p(" </div>");
367
+ p(" </div>");
368
+
369
+ // Query tab
370
+ p(' <div class="tab-content" id="tab-query">');
371
+ p(
372
+ ' <textarea class="query-area" id="queryInput" placeholder="Enter SQL query..." aria-label="SQL query input"></textarea>',
373
+ );
374
+ p(
375
+ ' <button class="btn btn-primary" id="executeQueryBtn">Execute</button>',
376
+ );
377
+ p(
378
+ ' <div id="queryError" style="display:none;" class="query-error"></div>',
379
+ );
380
+ p(' <div id="queryResults" style="margin-top:0.75rem;"></div>');
381
+ p(" </div>");
382
+ p(" </div>");
383
+
384
+ // --- Inline JavaScript ---
385
+ p(" <script>");
386
+ _appendManagerScript(p);
387
+ p(" </script>");
388
+ p("</body>");
389
+ p("</html>");
390
+
391
+ return lines.join("\n");
392
+ }
393
+
394
+ /**
395
+ * Internal helper: pushes inline JavaScript lines for the manager template.
396
+ * @param {function} p - push function that appends a line
397
+ */
398
+ function _appendManagerScript(p) {
399
+ // State variables
400
+ p(" var currentTable = null;");
401
+ p(" var columns = [];");
402
+ p(" var pkColumn = null;");
403
+ p(" var currentPage = 1;");
404
+ p(" var pageSize = 30;");
405
+ p(" var sortColumn = null;");
406
+ p(' var sortDir = "asc";');
407
+ p(" var selectedRows = new Set();");
408
+ p(" var allTables = [];");
409
+ p("");
410
+
411
+ // DOM references
412
+ p(' var tableListEl = document.getElementById("tableList");');
413
+ p(' var tableSearchEl = document.getElementById("tableSearch");');
414
+ p(' var tableTitleEl = document.getElementById("tableTitle");');
415
+ p(' var tabBar = document.getElementById("tabBar");');
416
+ p(' var placeholderEl = document.getElementById("placeholder");');
417
+ p(' var structureContent = document.getElementById("structureContent");');
418
+ p(
419
+ ' var dataTableContainer = document.getElementById("dataTableContainer");',
420
+ );
421
+ p(' var dataMessage = document.getElementById("dataMessage");');
422
+ p(
423
+ ' var paginationControls = document.getElementById("paginationControls");',
424
+ );
425
+ p(' var pageInfo = document.getElementById("pageInfo");');
426
+ p(' var pageSizeSelect = document.getElementById("pageSizeSelect");');
427
+ p(
428
+ ' var deleteSelectedBtn = document.getElementById("deleteSelectedBtn");',
429
+ );
430
+ p(' var addRowFormEl = document.getElementById("addRowForm");');
431
+ p(' var addRowFields = document.getElementById("addRowFields");');
432
+ p(' var addRowError = document.getElementById("addRowError");');
433
+ p(' var queryInput = document.getElementById("queryInput");');
434
+ p(' var queryError = document.getElementById("queryError");');
435
+ p(' var queryResults = document.getElementById("queryResults");');
436
+ p("");
437
+
438
+ // escapeHtml helper
439
+ p(" function escapeHtml(str) {");
440
+ p(' var d = document.createElement("div");');
441
+ p(" d.appendChild(document.createTextNode(str));");
442
+ p(" return d.innerHTML;");
443
+ p(" }");
444
+ p("");
445
+
446
+ // showDataMessage helper
447
+ p(" function showDataMessage(msg, type) {");
448
+ p(
449
+ " dataMessage.innerHTML = '<div class=\"message message-' + type + '\">' + escapeHtml(msg) + '</div>';",
450
+ );
451
+ p(" setTimeout(function() { dataMessage.innerHTML = ''; }, 4000);");
452
+ p(" }");
453
+ p("");
454
+
455
+ // --- Sidebar: load tables ---
456
+ p(" function loadTables() {");
457
+ p(' fetch("/database/tables")');
458
+ p(" .then(function(r) { return r.json(); })");
459
+ p(" .then(function(data) {");
460
+ p(" allTables = data.tables || [];");
461
+ p(" renderTableList(allTables);");
462
+ p(" });");
463
+ p(" }");
464
+ p("");
465
+
466
+ p(" function renderTableList(tables) {");
467
+ p(' tableListEl.innerHTML = "";');
468
+ p(" tables.forEach(function(t) {");
469
+ p(' var li = document.createElement("li");');
470
+ p(" li.textContent = t;");
471
+ p(' if (t === currentTable) li.className = "active";');
472
+ p(' li.addEventListener("click", function() { selectTable(t); });');
473
+ p(" tableListEl.appendChild(li);");
474
+ p(" });");
475
+ p(" }");
476
+ p("");
477
+
478
+ // Sidebar search filter
479
+ p(' tableSearchEl.addEventListener("input", function() {');
480
+ p(" var q = tableSearchEl.value.toLowerCase();");
481
+ p(" var filtered = allTables.filter(function(t) {");
482
+ p(" return t.toLowerCase().indexOf(q) !== -1;");
483
+ p(" });");
484
+ p(" renderTableList(filtered);");
485
+ p(" });");
486
+ p("");
487
+
488
+ // --- Tab switching ---
489
+ p(' document.querySelectorAll(".tab-btn").forEach(function(btn) {');
490
+ p(' btn.addEventListener("click", function() {');
491
+ p(
492
+ ' document.querySelectorAll(".tab-btn").forEach(function(b) { b.classList.remove("active"); });',
493
+ );
494
+ p(
495
+ ' document.querySelectorAll(".tab-content").forEach(function(c) { c.classList.remove("active"); });',
496
+ );
497
+ p(' btn.classList.add("active");');
498
+ p(' var tab = btn.getAttribute("data-tab");');
499
+ p(' document.getElementById("tab-" + tab).classList.add("active");');
500
+ p(" });");
501
+ p(" });");
502
+ p("");
503
+
504
+ // --- Select table ---
505
+ p(" function selectTable(name) {");
506
+ p(" currentTable = name;");
507
+ p(" currentPage = 1;");
508
+ p(" sortColumn = null;");
509
+ p(' sortDir = "asc";');
510
+ p(" selectedRows = new Set();");
511
+ p(" tableTitleEl.textContent = name;");
512
+ p(' tabBar.style.display = "flex";');
513
+ p(' placeholderEl.style.display = "none";');
514
+ p(" renderTableList(allTables.filter(function(t) {");
515
+ p(" var q = tableSearchEl.value.toLowerCase();");
516
+ p(" return t.toLowerCase().indexOf(q) !== -1;");
517
+ p(" }));");
518
+ p(" loadStructure();");
519
+ p(" loadData();");
520
+ p(" }");
521
+ p("");
522
+
523
+ // --- Structure tab ---
524
+ p(" function loadStructure() {");
525
+ p(
526
+ ' fetch("/database/tables/" + encodeURIComponent(currentTable) + "?schema=true")',
527
+ );
528
+ p(" .then(function(r) { return r.json(); })");
529
+ p(" .then(function(data) {");
530
+ p(" columns = data.columns || [];");
531
+ p(" pkColumn = data.pk || null;");
532
+ p(" renderStructure();");
533
+ p(" renderAddRowForm();");
534
+ p(" });");
535
+ p(" }");
536
+ p("");
537
+
538
+ p(" function renderStructure() {");
539
+ p(' var html = "<table><thead><tr>";');
540
+ p(
541
+ ' html += "<th>Name</th><th>Type</th><th>Nullable</th><th>Default</th><th>PK</th>";',
542
+ );
543
+ p(' html += "</tr></thead><tbody>";');
544
+ p(" columns.forEach(function(col) {");
545
+ p(' html += "<tr>";');
546
+ p(' html += "<td>" + escapeHtml(col.name) + "</td>";');
547
+ p(' html += "<td>" + escapeHtml(col.type) + "</td>";');
548
+ p(' html += "<td>" + (col.nullable ? "Yes" : "No") + "</td>";');
549
+ p(
550
+ ' html += "<td>" + (col.default !== null && col.default !== undefined ? escapeHtml(String(col.default)) : "NULL") + "</td>";',
551
+ );
552
+ p(' html += "<td>" + (col.pk ? "Yes" : "No") + "</td>";');
553
+ p(' html += "</tr>";');
554
+ p(" });");
555
+ p(' html += "</tbody></table>";');
556
+ p(" structureContent.innerHTML = html;");
557
+ p(" }");
558
+ p("");
559
+
560
+ // --- Data tab ---
561
+ p(" function buildDataParams() {");
562
+ p(' var params = "?page=" + currentPage + "&size=" + pageSize;');
563
+ p(" if (sortColumn) {");
564
+ p(
565
+ ' params += "&sort=" + encodeURIComponent(sortColumn) + "&dir=" + sortDir;',
566
+ );
567
+ p(" }");
568
+ p(
569
+ ' document.querySelectorAll(".filter-row input").forEach(function(input) {',
570
+ );
571
+ p(" var val = input.value.trim();");
572
+ p(" if (val) {");
573
+ p(
574
+ ' params += "&" + encodeURIComponent(input.dataset.column) + "=" + encodeURIComponent("%" + val + "%");',
575
+ );
576
+ p(" }");
577
+ p(" });");
578
+ p(" return params;");
579
+ p(" }");
580
+ p("");
581
+
582
+ p(" function loadData() {");
583
+ p(" var params = buildDataParams();");
584
+ p(
585
+ ' fetch("/database/tables/" + encodeURIComponent(currentTable) + params)',
586
+ );
587
+ p(" .then(function(r) { return r.json(); })");
588
+ p(" .then(function(data) {");
589
+ p(" renderDataTable(data);");
590
+ p(" renderPagination(data);");
591
+ p(" selectedRows = new Set();");
592
+ p(" updateDeleteBtn();");
593
+ p(" });");
594
+ p(" }");
595
+ p("");
596
+
597
+ p(" function renderDataTable(data) {");
598
+ p(" if (!data.data || data.data.length === 0) {");
599
+ p(
600
+ " dataTableContainer.innerHTML = '<p style=\"color:#666;padding:1rem;\">No data found.</p>';",
601
+ );
602
+ p(" return;");
603
+ p(" }");
604
+ p(
605
+ " var cols = columns.length > 0 ? columns.map(function(c) { return c.name; }) : Object.keys(data.data[0]);",
606
+ );
607
+ p(' var html = "<table><thead><tr>";');
608
+ p(' html += \'<th><input type="checkbox" id="selectAll"></th>\';');
609
+ p(" cols.forEach(function(col) {");
610
+ p(' var arrow = "";');
611
+ p(
612
+ " if (sortColumn === col) arrow = sortDir === 'asc' ? ' \\u2191' : ' \\u2193';",
613
+ );
614
+ p(
615
+ " html += '<th data-col=\"' + escapeHtml(col) + '\">' + escapeHtml(col) + arrow + '</th>';",
616
+ );
617
+ p(" });");
618
+ p(" html += '<th>Actions</th></tr>';");
619
+ p(" // Filter row");
620
+ p(" html += '<tr class=\"filter-row\"><td></td>';");
621
+ p(" cols.forEach(function(col) {");
622
+ p(
623
+ ' html += \'<td><input type="text" data-column="\' + escapeHtml(col) + \'" placeholder="Filter..."></td>\';',
624
+ );
625
+ p(" });");
626
+ p(" html += '<td></td></tr></thead><tbody>';");
627
+ p(" data.data.forEach(function(row) {");
628
+ p(' var pkVal = pkColumn ? row[pkColumn] : "";');
629
+ p(" html += '<tr data-pk=\"' + escapeHtml(String(pkVal)) + '\">';");
630
+ p(
631
+ ' html += \'<td><input type="checkbox" class="row-check" value="\' + escapeHtml(String(pkVal)) + \'"></td>\';',
632
+ );
633
+ p(" cols.forEach(function(col) {");
634
+ p(
635
+ ' var val = row[col] !== null && row[col] !== undefined ? String(row[col]) : "";',
636
+ );
637
+ p(
638
+ " html += '<td class=\"data-cell\" data-col=\"' + escapeHtml(col) + '\">' + escapeHtml(val) + '</td>';",
639
+ );
640
+ p(" });");
641
+ p(
642
+ " html += '<td><button class=\"btn btn-primary edit-btn\">Edit</button></td>';",
643
+ );
644
+ p(" html += '</tr>';");
645
+ p(" });");
646
+ p(" html += '</tbody></table>';");
647
+ p(" dataTableContainer.innerHTML = html;");
648
+ p("");
649
+ p(" // Select all checkbox");
650
+ p(' var selectAllCb = document.getElementById("selectAll");');
651
+ p(" if (selectAllCb) {");
652
+ p(' selectAllCb.addEventListener("change", function() {');
653
+ p(' document.querySelectorAll(".row-check").forEach(function(cb) {');
654
+ p(" cb.checked = selectAllCb.checked;");
655
+ p(" if (cb.checked) selectedRows.add(cb.value);");
656
+ p(" else selectedRows.delete(cb.value);");
657
+ p(" });");
658
+ p(" updateDeleteBtn();");
659
+ p(" });");
660
+ p(" }");
661
+ p(" // Row checkboxes");
662
+ p(' document.querySelectorAll(".row-check").forEach(function(cb) {');
663
+ p(' cb.addEventListener("change", function() {');
664
+ p(" if (cb.checked) selectedRows.add(cb.value);");
665
+ p(" else selectedRows.delete(cb.value);");
666
+ p(" updateDeleteBtn();");
667
+ p(" });");
668
+ p(" });");
669
+ p(" // Sort headers");
670
+ p(
671
+ ' document.querySelectorAll("#tab-data th[data-col]").forEach(function(th) {',
672
+ );
673
+ p(' th.addEventListener("click", function() {');
674
+ p(' var col = th.getAttribute("data-col");');
675
+ p(" if (sortColumn === col) {");
676
+ p(' sortDir = sortDir === "asc" ? "desc" : "asc";');
677
+ p(" } else {");
678
+ p(" sortColumn = col;");
679
+ p(' sortDir = "asc";');
680
+ p(" }");
681
+ p(" loadData();");
682
+ p(" });");
683
+ p(" });");
684
+ p(" // Filter inputs debounce");
685
+ p(
686
+ ' document.querySelectorAll(".filter-row input").forEach(function(input) {',
687
+ );
688
+ p(" var timer;");
689
+ p(' input.addEventListener("input", function() {');
690
+ p(" clearTimeout(timer);");
691
+ p(
692
+ " timer = setTimeout(function() { currentPage = 1; loadData(); }, 400);",
693
+ );
694
+ p(" });");
695
+ p(" });");
696
+ p(" // Edit buttons");
697
+ p(' document.querySelectorAll(".edit-btn").forEach(function(btn) {');
698
+ p(' btn.addEventListener("click", function() {');
699
+ p(' startEdit(btn.closest("tr"));');
700
+ p(" });");
701
+ p(" });");
702
+ p(" }");
703
+ p("");
704
+
705
+ // Pagination
706
+ p(" function renderPagination(data) {");
707
+ p(" var total = data.total || 0;");
708
+ p(" var totalPages = Math.max(1, Math.ceil(total / pageSize));");
709
+ p(
710
+ ' pageInfo.textContent = "Page " + currentPage + " of " + totalPages + " (" + total + " rows)";',
711
+ );
712
+ p(' paginationControls.style.display = "flex";');
713
+ p(" }");
714
+ p("");
715
+ p(
716
+ ' document.getElementById("prevPageBtn").addEventListener("click", function() {',
717
+ );
718
+ p(" if (currentPage > 1) { currentPage--; loadData(); }");
719
+ p(" });");
720
+ p(
721
+ ' document.getElementById("nextPageBtn").addEventListener("click", function() {',
722
+ );
723
+ p(" currentPage++; loadData();");
724
+ p(" });");
725
+ p(' pageSizeSelect.addEventListener("change", function() {');
726
+ p(" pageSize = parseInt(pageSizeSelect.value, 10);");
727
+ p(" currentPage = 1;");
728
+ p(" loadData();");
729
+ p(" });");
730
+ p("");
731
+
732
+ // Delete selected button visibility
733
+ p(" function updateDeleteBtn() {");
734
+ p(
735
+ ' deleteSelectedBtn.style.display = selectedRows.size > 0 ? "inline-block" : "none";',
736
+ );
737
+ p(" }");
738
+ p("");
739
+
740
+ // --- Delete selected ---
741
+ p(' deleteSelectedBtn.addEventListener("click", function() {');
742
+ p(" var count = selectedRows.size;");
743
+ p(
744
+ " if (!confirm('Are you sure you want to delete ' + count + ' row(s)?')) return;",
745
+ );
746
+ p(" var keys = Array.from(selectedRows);");
747
+ p(' fetch("/database/tables/" + encodeURIComponent(currentTable), {');
748
+ p(' method: "DELETE",');
749
+ p(' headers: { "Content-Type": "application/json" },');
750
+ p(" body: JSON.stringify({ keys: keys })");
751
+ p(" })");
752
+ p(
753
+ " .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })",
754
+ );
755
+ p(" .then(function(res) {");
756
+ p(" if (res.ok) {");
757
+ p(
758
+ " showDataMessage('Deleted ' + (res.data.deleted || count) + ' row(s).', 'success');",
759
+ );
760
+ p(" selectedRows = new Set();");
761
+ p(" loadData();");
762
+ p(" } else {");
763
+ p(
764
+ " showDataMessage(res.data.error || 'Delete failed.', 'error');",
765
+ );
766
+ p(" }");
767
+ p(" });");
768
+ p(" });");
769
+ p("");
770
+
771
+ // --- Inline edit ---
772
+ p(" function startEdit(tr) {");
773
+ p(' var cells = tr.querySelectorAll(".data-cell");');
774
+ p(" var originalValues = {};");
775
+ p(" cells.forEach(function(cell) {");
776
+ p(' var col = cell.getAttribute("data-col");');
777
+ p(" originalValues[col] = cell.textContent;");
778
+ p(' var input = document.createElement("input");');
779
+ p(' input.type = "text";');
780
+ p(" input.value = cell.textContent;");
781
+ p(" input.dataset.col = col;");
782
+ p(' cell.textContent = "";');
783
+ p(" cell.appendChild(input);");
784
+ p(" });");
785
+ p(' var actionsCell = tr.querySelector("td:last-child");');
786
+ p(' actionsCell.innerHTML = "";');
787
+ p(' var saveBtn = document.createElement("button");');
788
+ p(' saveBtn.className = "btn btn-success";');
789
+ p(' saveBtn.textContent = "Save";');
790
+ p(' var cancelBtn = document.createElement("button");');
791
+ p(' cancelBtn.className = "btn btn-secondary";');
792
+ p(' cancelBtn.textContent = "Cancel";');
793
+ p(' cancelBtn.style.marginLeft = "0.25rem";');
794
+ p(" actionsCell.appendChild(saveBtn);");
795
+ p(" actionsCell.appendChild(cancelBtn);");
796
+ p("");
797
+ p(' cancelBtn.addEventListener("click", function() {');
798
+ p(" cells.forEach(function(cell) {");
799
+ p(' var col = cell.getAttribute("data-col");');
800
+ p(" cell.textContent = originalValues[col];");
801
+ p(" });");
802
+ p(
803
+ " actionsCell.innerHTML = '<button class=\"btn btn-primary edit-btn\">Edit</button>';",
804
+ );
805
+ p(
806
+ ' actionsCell.querySelector(".edit-btn").addEventListener("click", function() { startEdit(tr); });',
807
+ );
808
+ p(" });");
809
+ p("");
810
+ p(' saveBtn.addEventListener("click", function() {');
811
+ p(' var pkVal = tr.getAttribute("data-pk");');
812
+ p(" var body = {};");
813
+ p(" cells.forEach(function(cell) {");
814
+ p(' var input = cell.querySelector("input");');
815
+ p(" if (input) body[input.dataset.col] = input.value;");
816
+ p(" });");
817
+ p(
818
+ ' fetch("/database/tables/" + encodeURIComponent(currentTable) + "/" + encodeURIComponent(pkVal), {',
819
+ );
820
+ p(' method: "PUT",');
821
+ p(' headers: { "Content-Type": "application/json" },');
822
+ p(" body: JSON.stringify(body)");
823
+ p(" })");
824
+ p(
825
+ " .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })",
826
+ );
827
+ p(" .then(function(res) {");
828
+ p(" if (res.ok) {");
829
+ p(" showDataMessage('Row updated successfully.', 'success');");
830
+ p(" loadData();");
831
+ p(" } else {");
832
+ p(
833
+ " showDataMessage(res.data.error || 'Update failed.', 'error');",
834
+ );
835
+ p(" }");
836
+ p(" });");
837
+ p(" });");
838
+ p(" }");
839
+ p("");
840
+
841
+ // --- Add row form ---
842
+ p(" function renderAddRowForm() {");
843
+ p(' addRowFields.innerHTML = "";');
844
+ p(" columns.forEach(function(col) {");
845
+ p(' var div = document.createElement("div");');
846
+ p(' var label = document.createElement("label");');
847
+ p(" label.textContent = col.name;");
848
+ p(" if (!col.nullable && !col.pk) {");
849
+ p(' var mark = document.createElement("span");');
850
+ p(' mark.className = "required-mark";');
851
+ p(' mark.textContent = " *";');
852
+ p(" label.appendChild(mark);");
853
+ p(" }");
854
+ p(" div.appendChild(label);");
855
+ p(" var input;");
856
+ p(' var colType = (col.type || "").toLowerCase();');
857
+ p(" if (colType.indexOf('bool') !== -1) {");
858
+ p(' input = document.createElement("select");');
859
+ p(
860
+ ' var optEmpty = document.createElement("option"); optEmpty.value = ""; optEmpty.textContent = "-- select --";',
861
+ );
862
+ p(
863
+ ' var optTrue = document.createElement("option"); optTrue.value = "true"; optTrue.textContent = "true";',
864
+ );
865
+ p(
866
+ ' var optFalse = document.createElement("option"); optFalse.value = "false"; optFalse.textContent = "false";',
867
+ );
868
+ p(
869
+ " input.appendChild(optEmpty); input.appendChild(optTrue); input.appendChild(optFalse);",
870
+ );
871
+ p(
872
+ " } else if (colType.indexOf('int') !== -1 || colType.indexOf('numeric') !== -1 || colType.indexOf('decimal') !== -1 || colType.indexOf('float') !== -1 || colType.indexOf('double') !== -1 || colType.indexOf('real') !== -1) {",
873
+ );
874
+ p(' input = document.createElement("input");');
875
+ p(' input.type = "number";');
876
+ p(' input.step = "any";');
877
+ p(" } else {");
878
+ p(' input = document.createElement("input");');
879
+ p(' input.type = "text";');
880
+ p(" }");
881
+ p(" input.dataset.column = col.name;");
882
+ p(" div.appendChild(input);");
883
+ p(" addRowFields.appendChild(div);");
884
+ p(" });");
885
+ p(" }");
886
+ p("");
887
+
888
+ p(
889
+ ' document.getElementById("addRowBtn").addEventListener("click", function() {',
890
+ );
891
+ p(' addRowFormEl.style.display = "block";');
892
+ p(' addRowError.style.display = "none";');
893
+ p(" });");
894
+ p(
895
+ ' document.getElementById("addRowCancel").addEventListener("click", function() {',
896
+ );
897
+ p(' addRowFormEl.style.display = "none";');
898
+ p(' addRowError.style.display = "none";');
899
+ p(" });");
900
+ p(
901
+ ' document.getElementById("addRowSubmit").addEventListener("click", function() {',
902
+ );
903
+ p(" var body = {};");
904
+ p(
905
+ ' addRowFields.querySelectorAll("input, select").forEach(function(el) {',
906
+ );
907
+ p(" var val = el.value;");
908
+ p(' if (val !== "") body[el.dataset.column] = val;');
909
+ p(" });");
910
+ p(' fetch("/database/tables/" + encodeURIComponent(currentTable), {');
911
+ p(' method: "POST",');
912
+ p(' headers: { "Content-Type": "application/json" },');
913
+ p(" body: JSON.stringify(body)");
914
+ p(" })");
915
+ p(
916
+ " .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })",
917
+ );
918
+ p(" .then(function(res) {");
919
+ p(" if (res.ok) {");
920
+ p(' addRowFormEl.style.display = "none";');
921
+ p(" showDataMessage('Row inserted successfully.', 'success');");
922
+ p(" loadData();");
923
+ p(" } else {");
924
+ p(
925
+ " addRowError.textContent = res.data.error || 'Insert failed.';",
926
+ );
927
+ p(' addRowError.style.display = "block";');
928
+ p(" }");
929
+ p(" });");
930
+ p(" });");
931
+ p("");
932
+
933
+ // --- CSV download ---
934
+ p(
935
+ ' document.getElementById("downloadCsvBtn").addEventListener("click", function() {',
936
+ );
937
+ p(
938
+ ' var url = "/database/tables/" + encodeURIComponent(currentTable) + "/csv";',
939
+ );
940
+ p(" if (selectedRows.size > 0) {");
941
+ p(
942
+ ' url += "?ids=" + encodeURIComponent(Array.from(selectedRows).join(","));',
943
+ );
944
+ p(" }");
945
+ p(" window.location.href = url;");
946
+ p(" });");
947
+ p("");
948
+
949
+ // --- Query tab ---
950
+ p(
951
+ ' document.getElementById("executeQueryBtn").addEventListener("click", function() {',
952
+ );
953
+ p(" var sql = queryInput.value.trim();");
954
+ p(" if (!sql) return;");
955
+ p(' queryError.style.display = "none";');
956
+ p(' queryResults.innerHTML = "";');
957
+ p(' fetch("/database/query", {');
958
+ p(' method: "POST",');
959
+ p(' headers: { "Content-Type": "application/json" },');
960
+ p(" body: JSON.stringify({ sql: sql })");
961
+ p(" })");
962
+ p(
963
+ " .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })",
964
+ );
965
+ p(" .then(function(res) {");
966
+ p(" if (!res.ok) {");
967
+ p(" queryError.textContent = res.data.error || 'Query failed.';");
968
+ p(' queryError.style.display = "block";');
969
+ p(" return;");
970
+ p(" }");
971
+ p(" var rows = res.data.data || [];");
972
+ p(" if (rows.length === 0) {");
973
+ p(
974
+ " queryResults.innerHTML = '<p style=\"color:#b0b0b0;\">Query executed. ' + (res.data.rowCount || 0) + ' row(s) affected.</p>';",
975
+ );
976
+ p(" return;");
977
+ p(" }");
978
+ p(" var keys = Object.keys(rows[0]);");
979
+ p(' var html = "<table><thead><tr>";');
980
+ p(
981
+ " keys.forEach(function(k) { html += '<th>' + escapeHtml(k) + '</th>'; });",
982
+ );
983
+ p(' html += "</tr></thead><tbody>";');
984
+ p(" rows.forEach(function(row) {");
985
+ p(' html += "<tr>";');
986
+ p(" keys.forEach(function(k) {");
987
+ p(
988
+ ' var v = row[k] !== null && row[k] !== undefined ? String(row[k]) : "";',
989
+ );
990
+ p(" html += '<td>' + escapeHtml(v) + '</td>';");
991
+ p(" });");
992
+ p(' html += "</tr>";');
993
+ p(" });");
994
+ p(' html += "</tbody></table>";');
995
+ p(" queryResults.innerHTML = html;");
996
+ p(" });");
997
+ p(" });");
998
+ p("");
999
+
1000
+ // Init
1001
+ p(" loadTables();");
1002
+ }
1003
+
1004
+ /**
1005
+ * Generate the route handler content string for routes/database.js.
1006
+ * Returns an ES module string implementing all DB Manager API endpoints.
1007
+ *
1008
+ * @returns {string}
1009
+ */
1010
+ function generateDbManagerRoute() {
1011
+ var lines = [];
1012
+ function p(s) {
1013
+ lines.push(s);
1014
+ }
1015
+
1016
+ // Imports
1017
+ p('import express from "express";');
1018
+ p('import dbManagerAuth from "../middleware/db-manager-auth.js";');
1019
+ p("");
1020
+ p("const router = express.Router();");
1021
+ p("");
1022
+
1023
+ // --- Login routes (no auth) ---
1024
+ p("// GET /login — render login page");
1025
+ p('router.get("/login", (req, res) => {');
1026
+ p(" if (!process.env.DATABASE_MANAGER_PASSWORD) {");
1027
+ p(
1028
+ ' return res.status(503).render("db-manager/login", { notConfigured: true });',
1029
+ );
1030
+ p(" }");
1031
+ p(' res.render("db-manager/login");');
1032
+ p("});");
1033
+ p("");
1034
+
1035
+ p("// POST /login — authenticate with password");
1036
+ p('router.post("/login", (req, res) => {');
1037
+ p(" const configuredPassword = process.env.DATABASE_MANAGER_PASSWORD;");
1038
+ p(" if (!configuredPassword) {");
1039
+ p(
1040
+ ' return res.status(503).render("db-manager/login", { notConfigured: true });',
1041
+ );
1042
+ p(" }");
1043
+ p(" const { password } = req.body;");
1044
+ p(" if (password === configuredPassword) {");
1045
+ p(' req.session["db-manager"] = true;');
1046
+ p(' return res.redirect("/database");');
1047
+ p(" }");
1048
+ p(' res.render("db-manager/login", { error: "Invalid password" });');
1049
+ p("});");
1050
+ p("");
1051
+
1052
+ // --- Apply auth middleware to all remaining routes ---
1053
+ p("// Auth middleware for all routes below");
1054
+ p("router.use(dbManagerAuth);");
1055
+ p("");
1056
+
1057
+ // --- GET / — render manager page ---
1058
+ p("// GET / — render manager page");
1059
+ p('router.get("/", (req, res) => {');
1060
+ p(' res.render("db-manager/manager");');
1061
+ p("});");
1062
+ p("");
1063
+
1064
+ // --- GET /tables — list all table names ---
1065
+ p("// GET /tables — list all table names");
1066
+ p('router.get("/tables", async (req, res) => {');
1067
+ p(" try {");
1068
+ p(
1069
+ " const result = await global.db.query(\"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' OR table_type = 'BASE TABLE' ORDER BY table_name\");",
1070
+ );
1071
+ p(" const rows = result.rows || result || [];");
1072
+ p(
1073
+ " const tables = rows.map((r) => r.table_name || r.TABLE_NAME || r.name || Object.values(r)[0]);",
1074
+ );
1075
+ p(" res.json({ tables });");
1076
+ p(" } catch (err) {");
1077
+ p(" res.status(400).json({ error: err.message });");
1078
+ p(" }");
1079
+ p("});");
1080
+ p("");
1081
+
1082
+ // --- Helper: get primary key column for a table ---
1083
+ p("// Helper: get primary key column for a table");
1084
+ p("async function getPrimaryKey(tableName) {");
1085
+ p(" try {");
1086
+ p(" const result = await global.db.query(");
1087
+ p(
1088
+ " \"SELECT column_name FROM information_schema.key_column_usage WHERE table_name = '\" + tableName + \"' AND constraint_name LIKE '%pkey%' OR (table_name = '\" + tableName + \"' AND constraint_name LIKE '%PRIMARY%') LIMIT 1\"",
1089
+ );
1090
+ p(" );");
1091
+ p(" const rows = result.rows || result || [];");
1092
+ p(" if (rows.length > 0) {");
1093
+ p(
1094
+ " return rows[0].column_name || rows[0].COLUMN_NAME || Object.values(rows[0])[0];",
1095
+ );
1096
+ p(" }");
1097
+ p(" } catch (e) {");
1098
+ p(" // fallback");
1099
+ p(" }");
1100
+ p(' return "id";');
1101
+ p("}");
1102
+ p("");
1103
+
1104
+ // --- Helper: get column schema for a table ---
1105
+ p("// Helper: get column schema for a table");
1106
+ p("async function getTableSchema(tableName) {");
1107
+ p(" const result = await global.db.query(");
1108
+ p(
1109
+ ' "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = \'" + tableName + "\' ORDER BY ordinal_position"',
1110
+ );
1111
+ p(" );");
1112
+ p(" const rows = result.rows || result || [];");
1113
+ p(" const pk = await getPrimaryKey(tableName);");
1114
+ p(" const columns = rows.map((r) => ({");
1115
+ p(" name: r.column_name || r.COLUMN_NAME || Object.values(r)[0],");
1116
+ p(' type: r.data_type || r.DATA_TYPE || "",');
1117
+ p(' nullable: (r.is_nullable || r.IS_NULLABLE || "YES") === "YES",');
1118
+ p(" default: r.column_default || r.COLUMN_DEFAULT || null,");
1119
+ p(" pk: (r.column_name || r.COLUMN_NAME || Object.values(r)[0]) === pk,");
1120
+ p(" }));");
1121
+ p(" return { columns, pk };");
1122
+ p("}");
1123
+ p("");
1124
+
1125
+ // --- GET /tables/:table_name — get table data or schema ---
1126
+ p("// GET /tables/:table_name — get table data or schema");
1127
+ p('router.get("/tables/:table_name", async (req, res) => {');
1128
+ p(" const tableName = req.params.table_name;");
1129
+ p(" try {");
1130
+ p(" // Schema mode");
1131
+ p(' if (req.query.schema === "true") {');
1132
+ p(" const schema = await getTableSchema(tableName);");
1133
+ p(" return res.json(schema);");
1134
+ p(" }");
1135
+ p("");
1136
+ p(" // Data mode");
1137
+ p(" const page = parseInt(req.query.page, 10) || 1;");
1138
+ p(" const size = parseInt(req.query.size, 10) || 30;");
1139
+ p(" const sort = req.query.sort;");
1140
+ p(' const dir = req.query.dir === "desc" ? "DESC" : "ASC";');
1141
+ p(" const offset = (page - 1) * size;");
1142
+ p(" const pk = await getPrimaryKey(tableName);");
1143
+ p("");
1144
+ p(" // Build WHERE clause from filter params");
1145
+ p(" const filterKeys = Object.keys(req.query).filter(");
1146
+ p(' (k) => !["page", "size", "sort", "dir", "schema"].includes(k)');
1147
+ p(" );");
1148
+ p(' let where = "";');
1149
+ p(" if (filterKeys.length > 0) {");
1150
+ p(" const conditions = filterKeys.map(");
1151
+ p(
1152
+ ' (k) => k + " LIKE \'" + String(req.query[k]).replace(/\'/g, "\'\'") + "\'"',
1153
+ );
1154
+ p(" );");
1155
+ p(' where = " WHERE " + conditions.join(" AND ");');
1156
+ p(" }");
1157
+ p("");
1158
+ p(' let orderBy = "";');
1159
+ p(" if (sort) {");
1160
+ p(' orderBy = " ORDER BY " + sort + " " + dir;');
1161
+ p(" }");
1162
+ p("");
1163
+ p(" // Count total");
1164
+ p(
1165
+ ' const countResult = await global.db.query("SELECT COUNT(*) as count FROM " + tableName + where);',
1166
+ );
1167
+ p(" const countRows = countResult.rows || countResult || [];");
1168
+ p(
1169
+ " const total = parseInt(countRows[0].count || countRows[0].COUNT || Object.values(countRows[0])[0], 10) || 0;",
1170
+ );
1171
+ p("");
1172
+ p(" // Fetch rows");
1173
+ p(
1174
+ ' const dataResult = await global.db.query("SELECT * FROM " + tableName + where + orderBy + " LIMIT " + size + " OFFSET " + offset);',
1175
+ );
1176
+ p(" const data = dataResult.rows || dataResult || [];");
1177
+ p("");
1178
+ p(" res.json({ data, total, page, size, pk });");
1179
+ p(" } catch (err) {");
1180
+ p(" res.status(400).json({ error: err.message });");
1181
+ p(" }");
1182
+ p("});");
1183
+ p("");
1184
+
1185
+ // --- GET /tables/:table_name/csv — download table data as CSV ---
1186
+ p("// GET /tables/:table_name/csv — download table data as CSV");
1187
+ p('router.get("/tables/:table_name/csv", async (req, res) => {');
1188
+ p(" const tableName = req.params.table_name;");
1189
+ p(" try {");
1190
+ p(" const pk = await getPrimaryKey(tableName);");
1191
+ p(' let query = "SELECT * FROM " + tableName;');
1192
+ p("");
1193
+ p(" // Filter by specific IDs if provided");
1194
+ p(" if (req.query.ids) {");
1195
+ p(
1196
+ ' const ids = req.query.ids.split(",").map((id) => "\'" + String(id).replace(/\'/g, "\'\'") + "\'").join(",");',
1197
+ );
1198
+ p(' query += " WHERE " + pk + " IN (" + ids + ")";');
1199
+ p(" }");
1200
+ p("");
1201
+ p(" const result = await global.db.query(query);");
1202
+ p(" const rows = result.rows || result || [];");
1203
+ p("");
1204
+ p(" if (rows.length === 0) {");
1205
+ p(' res.setHeader("Content-Type", "text/csv");');
1206
+ p(
1207
+ ' res.setHeader("Content-Disposition", "attachment; filename=\\"" + tableName + ".csv\\"");',
1208
+ );
1209
+ p(' return res.send("");');
1210
+ p(" }");
1211
+ p("");
1212
+ p(" const columns = Object.keys(rows[0]);");
1213
+ p(' const csvLines = [columns.join(",")];');
1214
+ p(" rows.forEach((row) => {");
1215
+ p(" const values = columns.map((col) => {");
1216
+ p(" const val = row[col];");
1217
+ p(' if (val === null || val === undefined) return "";');
1218
+ p(" const str = String(val);");
1219
+ p(
1220
+ ' if (str.includes(",") || str.includes(\'\"\') || str.includes("\\n")) {',
1221
+ );
1222
+ p(" return '\"' + str.replace(/\"/g, '\"\"') + '\"';");
1223
+ p(" }");
1224
+ p(" return str;");
1225
+ p(" });");
1226
+ p(' csvLines.push(values.join(","));');
1227
+ p(" });");
1228
+ p("");
1229
+ p(' res.setHeader("Content-Type", "text/csv");');
1230
+ p(
1231
+ ' res.setHeader("Content-Disposition", "attachment; filename=\\"" + tableName + ".csv\\"");',
1232
+ );
1233
+ p(' res.send(csvLines.join("\\n"));');
1234
+ p(" } catch (err) {");
1235
+ p(" res.status(400).json({ error: err.message });");
1236
+ p(" }");
1237
+ p("});");
1238
+ p("");
1239
+
1240
+ // --- DELETE /tables/:table_name — bulk delete rows ---
1241
+ p("// DELETE /tables/:table_name — bulk delete rows by primary key values");
1242
+ p('router.delete("/tables/:table_name", async (req, res) => {');
1243
+ p(" const tableName = req.params.table_name;");
1244
+ p(" try {");
1245
+ p(" const { keys } = req.body;");
1246
+ p(" if (!keys || !Array.isArray(keys) || keys.length === 0) {");
1247
+ p(
1248
+ ' return res.status(400).json({ error: "No keys provided for deletion" });',
1249
+ );
1250
+ p(" }");
1251
+ p(" const pk = await getPrimaryKey(tableName);");
1252
+ p(
1253
+ ' const placeholders = keys.map((k) => "\'" + String(k).replace(/\'/g, "\'\'") + "\'").join(",");',
1254
+ );
1255
+ p(
1256
+ ' const result = await global.db.query("DELETE FROM " + tableName + " WHERE " + pk + " IN (" + placeholders + ")");',
1257
+ );
1258
+ p(
1259
+ " const deleted = (result && result.rowCount) || (result && result.changes) || keys.length;",
1260
+ );
1261
+ p(" res.json({ deleted });");
1262
+ p(" } catch (err) {");
1263
+ p(" res.status(400).json({ error: err.message });");
1264
+ p(" }");
1265
+ p("});");
1266
+ p("");
1267
+
1268
+ // --- PUT /tables/:table_name/:id — update a single row ---
1269
+ p("// PUT /tables/:table_name/:id — update a single row");
1270
+ p('router.put("/tables/:table_name/:id", async (req, res) => {');
1271
+ p(" const tableName = req.params.table_name;");
1272
+ p(" const id = req.params.id;");
1273
+ p(" try {");
1274
+ p(" const body = req.body;");
1275
+ p(" const keys = Object.keys(body);");
1276
+ p(" if (keys.length === 0) {");
1277
+ p(
1278
+ ' return res.status(400).json({ error: "No fields provided for update" });',
1279
+ );
1280
+ p(" }");
1281
+ p(" const pk = await getPrimaryKey(tableName);");
1282
+ p(" const setClauses = keys.map(");
1283
+ p(' (k) => k + " = \'" + String(body[k]).replace(/\'/g, "\'\'") + "\'"');
1284
+ p(" );");
1285
+ p(
1286
+ ' await global.db.query("UPDATE " + tableName + " SET " + setClauses.join(", ") + " WHERE " + pk + " = \'" + String(id).replace(/\'/g, "\'\'") + "\'");',
1287
+ );
1288
+ p("");
1289
+ p(" // Fetch updated row");
1290
+ p(
1291
+ ' const result = await global.db.query("SELECT * FROM " + tableName + " WHERE " + pk + " = \'" + String(id).replace(/\'/g, "\'\'") + "\'");',
1292
+ );
1293
+ p(" const rows = result.rows || result || [];");
1294
+ p(" res.json({ data: rows[0] || {} });");
1295
+ p(" } catch (err) {");
1296
+ p(" res.status(400).json({ error: err.message });");
1297
+ p(" }");
1298
+ p("});");
1299
+ p("");
1300
+
1301
+ // --- POST /tables/:table_name — insert a new row ---
1302
+ p("// POST /tables/:table_name — insert a new row");
1303
+ p('router.post("/tables/:table_name", async (req, res) => {');
1304
+ p(" const tableName = req.params.table_name;");
1305
+ p(" try {");
1306
+ p(" const body = req.body;");
1307
+ p(" const keys = Object.keys(body);");
1308
+ p(" if (keys.length === 0) {");
1309
+ p(
1310
+ ' return res.status(400).json({ error: "No fields provided for insert" });',
1311
+ );
1312
+ p(" }");
1313
+ p(' const columns = keys.join(", ");');
1314
+ p(" const values = keys.map(");
1315
+ p(' (k) => "\'" + String(body[k]).replace(/\'/g, "\'\'") + "\'"');
1316
+ p(' ).join(", ");');
1317
+ p(
1318
+ ' await global.db.query("INSERT INTO " + tableName + " (" + columns + ") VALUES (" + values + ")");',
1319
+ );
1320
+ p("");
1321
+ p(" res.status(201).json({ data: body });");
1322
+ p(" } catch (err) {");
1323
+ p(" res.status(400).json({ error: err.message });");
1324
+ p(" }");
1325
+ p("});");
1326
+ p("");
1327
+
1328
+ // --- POST /query — execute raw SQL ---
1329
+ p("// POST /query — execute raw SQL query");
1330
+ p('router.post("/query", async (req, res) => {');
1331
+ p(" try {");
1332
+ p(" const { sql } = req.body;");
1333
+ p(" if (!sql) {");
1334
+ p(' return res.status(400).json({ error: "No SQL query provided" });');
1335
+ p(" }");
1336
+ p(" const result = await global.db.query(sql);");
1337
+ p(" const data = result.rows || result || [];");
1338
+ p(" const rowCount = Array.isArray(data) ? data.length : 0;");
1339
+ p(" res.json({ data, rowCount });");
1340
+ p(" } catch (err) {");
1341
+ p(" res.status(400).json({ error: err.message });");
1342
+ p(" }");
1343
+ p("});");
1344
+ p("");
1345
+
1346
+ p("export default router;");
1347
+ p("");
1348
+
1349
+ return lines.join("\n");
1350
+ }
1351
+
1352
+ /**
1353
+ * Append DB Manager password variable to existing .env content.
1354
+ * Adds a newline separator and a "# DB Manager" comment before the variable.
1355
+ *
1356
+ * @param {string} existingEnv - Existing .env file content
1357
+ * @returns {string} Modified .env content with DATABASE_MANAGER_PASSWORD appended
1358
+ */
1359
+ function appendDbManagerEnv(existingEnv) {
1360
+ return (
1361
+ existingEnv.trimEnd() +
1362
+ "\n\n# DB Manager\nDATABASE_MANAGER_PASSWORD=admin\n"
1363
+ );
1364
+ }
1365
+
1366
+ /**
1367
+ * Append DB Manager password placeholder to existing .env.example content.
1368
+ * Adds a newline separator and a "# DB Manager" comment before the variable.
1369
+ *
1370
+ * @param {string} existingEnvExample - Existing .env.example file content
1371
+ * @returns {string} Modified .env.example content with DATABASE_MANAGER_PASSWORD appended
1372
+ */
1373
+ function appendDbManagerEnvExample(existingEnvExample) {
1374
+ return (
1375
+ existingEnvExample.trimEnd() +
1376
+ "\n\n# DB Manager\nDATABASE_MANAGER_PASSWORD=your_db_manager_password\n"
1377
+ );
1378
+ }
1379
+
1380
+ /**
1381
+ * Add the "ejs" dependency to a package.json string.
1382
+ * Parses the JSON, adds "ejs" to dependencies, and returns the modified JSON string.
1383
+ *
1384
+ * @param {string} packageJsonStr - Existing package.json content as a string
1385
+ * @returns {string} Modified package.json content with ejs dependency added
1386
+ */
1387
+ function addEjsDependency(packageJsonStr) {
1388
+ var pkg = JSON.parse(packageJsonStr);
1389
+ if (!pkg.dependencies) {
1390
+ pkg.dependencies = {};
1391
+ }
1392
+ pkg.dependencies["ejs"] = "^3.1.10";
1393
+ return JSON.stringify(pkg, null, 2) + "\n";
1394
+ }
1395
+
1396
+ /**
1397
+ * Inject DB Manager configuration into existing app.js content.
1398
+ * Adds:
1399
+ * - `import path from "path";` at the top (if not already present)
1400
+ * - `import dbManagerRoute from "./routes/database.js";` after other imports
1401
+ * - `app.set("view engine", "ejs");` and `app.set("views", path.join(__dirname, "views"));` after middleware setup
1402
+ * - `app.use("/database", dbManagerRoute);` before the error handler
1403
+ *
1404
+ * @param {string} appJsContent - Existing app.js content as a string
1405
+ * @returns {string} Modified app.js content with DB Manager integration
1406
+ */
1407
+ function addDbManagerToAppJs(appJsContent) {
1408
+ var lines = appJsContent.split("\n");
1409
+ var result = [];
1410
+ var pathImportExists = false;
1411
+ var dbManagerRouteImportAdded = false;
1412
+ var ejsConfigAdded = false;
1413
+ var routeMountAdded = false;
1414
+
1415
+ // Check if path import already exists
1416
+ for (var i = 0; i < lines.length; i++) {
1417
+ if (/^\s*import\s+path\s+from\s+["']path["']/.test(lines[i])) {
1418
+ pathImportExists = true;
1419
+ break;
1420
+ }
1421
+ }
1422
+
1423
+ for (var i = 0; i < lines.length; i++) {
1424
+ var line = lines[i];
1425
+
1426
+ // Add path import at the very top if not present (after the first import)
1427
+ if (
1428
+ !pathImportExists &&
1429
+ !dbManagerRouteImportAdded &&
1430
+ /^\s*import\s+/.test(line)
1431
+ ) {
1432
+ result.push('import path from "path";');
1433
+ pathImportExists = true;
1434
+ }
1435
+
1436
+ // Find the last import line to add dbManagerRoute import after it
1437
+ if (/^\s*import\s+/.test(line) && !dbManagerRouteImportAdded) {
1438
+ // Look ahead to see if next non-empty line is not an import
1439
+ var nextNonEmpty = i + 1;
1440
+ while (nextNonEmpty < lines.length && lines[nextNonEmpty].trim() === "") {
1441
+ nextNonEmpty++;
1442
+ }
1443
+ if (
1444
+ nextNonEmpty >= lines.length ||
1445
+ !/^\s*import\s+/.test(lines[nextNonEmpty])
1446
+ ) {
1447
+ result.push(line);
1448
+ result.push('import dbManagerRoute from "./routes/database.js";');
1449
+ dbManagerRouteImportAdded = true;
1450
+ continue;
1451
+ }
1452
+ }
1453
+
1454
+ // Add EJS config after middleware setup (after app.use(express.urlencoded...))
1455
+ if (!ejsConfigAdded && /app\.use\(\s*express\.urlencoded/.test(line)) {
1456
+ result.push(line);
1457
+ result.push("");
1458
+ result.push("// EJS view engine");
1459
+ result.push('app.set("view engine", "ejs");');
1460
+ result.push('app.set("views", path.join(__dirname, "views"));');
1461
+ ejsConfigAdded = true;
1462
+ continue;
1463
+ }
1464
+
1465
+ // Add route mount before the error handler
1466
+ if (
1467
+ !routeMountAdded &&
1468
+ /app\.use\(\s*\(\s*err\s*,\s*req\s*,\s*res\s*,\s*next\s*\)/.test(line)
1469
+ ) {
1470
+ result.push("// DB Manager");
1471
+ result.push('app.use("/database", dbManagerRoute);');
1472
+ result.push("");
1473
+ routeMountAdded = true;
1474
+ }
1475
+
1476
+ result.push(line);
1477
+ }
1478
+
1479
+ return result.join("\n");
1480
+ }
1481
+
1482
+ /**
1483
+ * Generate all DB Manager planned files.
1484
+ *
1485
+ * @param {object} schema - Parsed schema from parseSchema()
1486
+ * @param {object} [options]
1487
+ * @param {string} [options.envContent] - Existing .env content to append to
1488
+ * @param {string} [options.envExampleContent] - Existing .env.example content to append to
1489
+ * @param {string} [options.appJsContent] - Existing app.js content to modify
1490
+ * @param {string} [options.packageJsonContent] - Existing package.json content to modify
1491
+ * @returns {{ files: Array<{ relPath: string, content: string }>, warnings: string[] }}
1492
+ */
1493
+ function generateDbManager(schema, options) {
1494
+ options = options || {};
1495
+
1496
+ // SQL adapter guard — skip for NoSQL adapters
1497
+ if (!SQL_ADAPTERS.includes(schema.adapter)) {
1498
+ return {
1499
+ files: [],
1500
+ warnings: [
1501
+ "DB Manager requires a SQL adapter. Skipping DB Manager generation for adapter: " +
1502
+ schema.adapter,
1503
+ ],
1504
+ };
1505
+ }
1506
+
1507
+ var files = [];
1508
+
1509
+ // Core template files (always generated for SQL adapters)
1510
+ files.push({
1511
+ relPath: "routes/database.js",
1512
+ content: generateDbManagerRoute(),
1513
+ });
1514
+
1515
+ files.push({
1516
+ relPath: "middleware/db-manager-auth.js",
1517
+ content: generateDbManagerAuthMiddleware(),
1518
+ });
1519
+
1520
+ files.push({
1521
+ relPath: "views/db-manager/login.ejs",
1522
+ content: generateLoginTemplate(),
1523
+ });
1524
+
1525
+ files.push({
1526
+ relPath: "views/db-manager/manager.ejs",
1527
+ content: generateManagerTemplate(),
1528
+ });
1529
+
1530
+ // Optional modifications — only included when the corresponding option is provided
1531
+ if (options.envContent) {
1532
+ files.push({
1533
+ relPath: ".env",
1534
+ content: appendDbManagerEnv(options.envContent),
1535
+ });
1536
+ }
1537
+
1538
+ if (options.envExampleContent) {
1539
+ files.push({
1540
+ relPath: ".env.example",
1541
+ content: appendDbManagerEnvExample(options.envExampleContent),
1542
+ });
1543
+ }
1544
+
1545
+ if (options.packageJsonContent) {
1546
+ files.push({
1547
+ relPath: "package.json",
1548
+ content: addEjsDependency(options.packageJsonContent),
1549
+ });
1550
+ }
1551
+
1552
+ if (options.appJsContent) {
1553
+ files.push({
1554
+ relPath: "app.js",
1555
+ content: addDbManagerToAppJs(options.appJsContent),
1556
+ });
1557
+ }
1558
+
1559
+ return { files: files, warnings: [] };
1560
+ }
1561
+
1562
+ module.exports = {
1563
+ SQL_ADAPTERS,
1564
+ generateDbManager,
1565
+ generateDbManagerAuthMiddleware,
1566
+ generateDbManagerRoute,
1567
+ generateLoginTemplate,
1568
+ generateManagerTemplate,
1569
+ appendDbManagerEnv,
1570
+ appendDbManagerEnvExample,
1571
+ addEjsDependency,
1572
+ addDbManagerToAppJs,
1573
+ };