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.
- package/README.md +110 -16
- package/TODO.md +15 -0
- package/dbmr.schema.json +333 -0
- package/docker-compose.yml +1 -1
- package/package.json +8 -7
- package/scripts/demo-create.js +47 -0
- package/skill/SKILL.md +464 -0
- package/skill/references/cockroachdb.md +49 -0
- package/skill/references/dynamodb.md +53 -0
- package/skill/references/mongodb.md +56 -0
- package/skill/references/mssql.md +55 -0
- package/skill/references/oracle.md +52 -0
- package/skill/references/postgres.md +50 -0
- package/skill/references/redis.md +53 -0
- package/skill/references/sqlite3.md +43 -0
- package/src/cli/commands/generate.js +95 -31
- package/src/cli/commands/help.js +12 -7
- package/src/cli/commands/init.js +2 -2
- package/src/cli/commands/inspect.js +1 -0
- package/src/cli/diff-engine.js +54 -23
- package/src/cli/generate-db-manager.js +1573 -0
- package/src/cli/generate-docs-route.js +31 -0
- package/src/cli/generate-migration.js +356 -0
- package/src/cli/generate-model.js +9 -4
- package/src/cli/generate-openapi.js +40 -13
- package/src/cli/generate-route.js +55 -27
- package/src/cli/init/dependencies.js +3 -0
- package/src/cli/init/generators.js +37 -31
- package/src/cli/init.js +8 -8
- package/src/cli/main.js +2 -2
- package/src/cockroachdb/db.js +90 -59
- package/src/commons/route.js +20 -20
- package/src/commons/validator.js +58 -1
- package/src/dynamodb/db.js +50 -27
- package/src/mongodb/db.js +1 -0
- package/src/mssql/db.js +89 -61
- package/src/mysql/db.js +1 -0
- package/src/oracle/db.js +1 -0
- package/src/postgres/db.js +61 -41
- package/src/redis/db.js +1 -0
- package/src/schema/schema-parser.js +43 -1
- package/src/schema/schema-printer.js +7 -0
- package/src/schema/schema-validator.js +17 -0
- package/src/sqlite3/db.js +12 -0
- package/docs/SKILL.md +0 -419
- 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
|
+
};
|