db-model-router 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +150 -11
  2. package/TODO.md +0 -15
  3. package/db-manager/.dbmanager.sqlite +0 -0
  4. package/db-manager/README.md +223 -0
  5. package/db-manager/adapter-proxy.js +361 -0
  6. package/db-manager/demo/cockroachdb.env +6 -0
  7. package/db-manager/demo/demo.sqlite +0 -0
  8. package/db-manager/demo/dynamodb.env +7 -0
  9. package/db-manager/demo/mongodb.env +4 -0
  10. package/db-manager/demo/mssql.env +6 -0
  11. package/db-manager/demo/mysql.env +6 -0
  12. package/db-manager/demo/oracle.env +6 -0
  13. package/db-manager/demo/postgres.env +6 -0
  14. package/db-manager/demo/redis.env +4 -0
  15. package/db-manager/demo/seeds/cockroachdb.sql +32 -0
  16. package/db-manager/demo/seeds/mssql.sql +32 -0
  17. package/db-manager/demo/seeds/mysql.sql +32 -0
  18. package/db-manager/demo/seeds/oracle.sql +43 -0
  19. package/db-manager/demo/seeds/postgres.sql +32 -0
  20. package/db-manager/demo/seeds/sqlite3.sql +32 -0
  21. package/db-manager/demo/sqlite3.env +2 -0
  22. package/db-manager/metadata-db.js +170 -0
  23. package/db-manager/public/.gitkeep +1 -0
  24. package/db-manager/public/css/style.css +1413 -0
  25. package/db-manager/public/js/app.js +1370 -0
  26. package/db-manager/routes/api.js +388 -0
  27. package/db-manager/routes/views.js +61 -0
  28. package/db-manager/server.js +39 -0
  29. package/db-manager/utils/build-filter-config.js +18 -0
  30. package/db-manager/utils/csv-export.js +59 -0
  31. package/db-manager/utils/export-filename.js +39 -0
  32. package/db-manager/utils/filter-tables.js +20 -0
  33. package/db-manager/utils/parse-filters.js +93 -0
  34. package/db-manager/utils/sort-state.js +35 -0
  35. package/db-manager/views/.gitkeep +1 -0
  36. package/db-manager/views/dashboard.ejs +53 -0
  37. package/db-manager/views/history.ejs +52 -0
  38. package/db-manager/views/index.ejs +35 -0
  39. package/db-manager/views/layout.ejs +31 -0
  40. package/db-manager/views/partials/data-panel.ejs +74 -0
  41. package/db-manager/views/partials/header.ejs +36 -0
  42. package/db-manager/views/partials/sidebar.ejs +30 -0
  43. package/db-manager/views/query.ejs +58 -0
  44. package/dbmr.schema.json +22 -44
  45. package/demo/.dockerignore +7 -0
  46. package/demo/.env.example +15 -0
  47. package/demo/Dockerfile +20 -0
  48. package/demo/app.js +39 -0
  49. package/demo/commons/add_migration.js +43 -0
  50. package/demo/commons/db.js +17 -0
  51. package/demo/commons/migrate.js +68 -0
  52. package/demo/commons/modules.js +18 -0
  53. package/demo/commons/password.js +36 -0
  54. package/demo/commons/security.js +30 -0
  55. package/demo/commons/session.js +13 -0
  56. package/demo/commons/webhook.js +81 -0
  57. package/demo/dbmr.schema.json +338 -0
  58. package/demo/middleware/authenticate.js +14 -0
  59. package/demo/middleware/hasPermission.js +30 -0
  60. package/demo/middleware/logger.js +67 -0
  61. package/demo/middleware/tenantIsolation.js +19 -0
  62. package/demo/migrations/20260510092158_create_migrations_table.sql +6 -0
  63. package/demo/migrations/20260510092159_create_saas_tables.sql +69 -0
  64. package/demo/migrations/20260510092159_create_tables.sql +193 -0
  65. package/demo/models/addresses.js +24 -0
  66. package/demo/models/cart_items.js +20 -0
  67. package/demo/models/carts.js +18 -0
  68. package/demo/models/categories.js +22 -0
  69. package/demo/models/coupons.js +25 -0
  70. package/demo/models/index.js +43 -0
  71. package/demo/models/order_items.js +23 -0
  72. package/demo/models/orders.js +27 -0
  73. package/demo/models/payments.js +23 -0
  74. package/demo/models/product_images.js +20 -0
  75. package/demo/models/product_reviews.js +22 -0
  76. package/demo/models/product_variants.js +22 -0
  77. package/demo/models/products.js +32 -0
  78. package/demo/models/role_permissions.js +17 -0
  79. package/demo/models/roles.js +17 -0
  80. package/demo/models/shipments.js +21 -0
  81. package/demo/models/tenants.js +18 -0
  82. package/demo/models/users.js +23 -0
  83. package/demo/models/webhook_logs.js +22 -0
  84. package/demo/models/webhooks.js +19 -0
  85. package/demo/models/wishlists.js +17 -0
  86. package/demo/openapi.json +7000 -0
  87. package/demo/package-lock.json +2827 -0
  88. package/demo/package.json +42 -0
  89. package/demo/routes/addresses/index.js +10 -0
  90. package/demo/routes/auth/index.js +55 -0
  91. package/demo/routes/carts/cart_items/index.js +11 -0
  92. package/demo/routes/carts/index.js +14 -0
  93. package/demo/routes/categories/index.js +10 -0
  94. package/demo/routes/coupons/index.js +10 -0
  95. package/demo/routes/docs.js +18 -0
  96. package/demo/routes/health.js +35 -0
  97. package/demo/routes/index.js +54 -0
  98. package/demo/routes/orders/index.js +18 -0
  99. package/demo/routes/orders/order_items/index.js +11 -0
  100. package/demo/routes/orders/payments/index.js +11 -0
  101. package/demo/routes/orders/shipments/index.js +11 -0
  102. package/demo/routes/products/index.js +18 -0
  103. package/demo/routes/products/product_images/index.js +11 -0
  104. package/demo/routes/products/product_reviews/index.js +11 -0
  105. package/demo/routes/products/product_variants/index.js +11 -0
  106. package/demo/routes/roles/index.js +75 -0
  107. package/demo/routes/roles/permissions/index.js +47 -0
  108. package/demo/routes/tenants/index.js +45 -0
  109. package/demo/routes/users/index.js +45 -0
  110. package/demo/routes/wishlists/index.js +10 -0
  111. package/demo/seeds/saas-seed.js +329 -0
  112. package/docker-compose.yml +61 -0
  113. package/package.json +120 -113
  114. package/scripts/demo-create.js +1 -1
  115. package/skill/SKILL.md +119 -3
  116. package/src/cli/commands/db-manager.js +134 -0
  117. package/src/cli/commands/generate.js +106 -60
  118. package/src/cli/commands/help.js +0 -1
  119. package/src/cli/generate-route.js +66 -27
  120. package/src/cli/generate-saas-structure.js +129 -0
  121. package/src/cli/init/dependencies.js +1 -1
  122. package/src/cli/init/generators.js +6 -77
  123. package/src/cli/init.js +9 -2
  124. package/src/cli/main.js +8 -1
  125. package/src/cli/saas/generate-saas-middleware.js +110 -0
  126. package/src/cli/saas/generate-saas-migrations.js +480 -0
  127. package/src/cli/saas/generate-saas-models.js +211 -0
  128. package/src/cli/saas/generate-saas-openapi.js +419 -0
  129. package/src/cli/saas/generate-saas-routes.js +435 -0
  130. package/src/cli/saas/generate-saas-seeds.js +243 -0
  131. package/src/cli/saas/generate-saas-tests.js +473 -0
  132. package/src/cli/saas/generate-saas-utils.js +176 -0
  133. package/src/commons/kafka.js +139 -0
  134. package/src/commons/model.js +29 -9
  135. package/src/commons/route.js +6 -6
  136. package/src/index.js +2 -0
  137. package/src/mssql/db.js +41 -3
  138. package/src/mysql/db.js +3 -0
  139. package/src/postgres/db.js +6 -0
  140. package/src/cli/generate-db-manager.js +0 -1573
@@ -0,0 +1,1370 @@
1
+ /* DB Manager - Client-Side Application */
2
+ /* eslint-env browser */
3
+ (function () {
4
+ "use strict";
5
+
6
+ // === State ===
7
+ var state = {
8
+ tables: [],
9
+ activeTable: null,
10
+ schema: null,
11
+ rows: [],
12
+ totalCount: 0,
13
+ page: 0,
14
+ limit: 30,
15
+ selectedKeys: [],
16
+ pkColumn: null,
17
+ sortColumn: null,
18
+ sortDir: null,
19
+ filters: [],
20
+ };
21
+
22
+ // === DOM References ===
23
+ var tableSearchInput = document.querySelector(".table-search");
24
+ var tableList = document.querySelector(".table-list");
25
+ var historyList = document.querySelector(".history-list");
26
+ var columnHeaders = document.querySelector(".column-headers");
27
+ var dataRows = document.querySelector(".data-rows");
28
+ var btnAdd = document.querySelector(".btn-add");
29
+ var btnDelete = document.querySelector(".btn-delete");
30
+ var btnExport = document.querySelector(".btn-export");
31
+ var btnFilter = document.querySelector(".btn-filter");
32
+ var btnPrev = document.querySelector(".btn-prev");
33
+ var btnNext = document.querySelector(".btn-next");
34
+ var pageInfo = document.querySelector(".page-info");
35
+ var pageSize = document.querySelector(".page-size");
36
+ var filterTagsContainer = document.querySelector(".filter-tags");
37
+ var filterModalOverlay = document.querySelector(".filter-modal-overlay");
38
+ var filterColSelect = document.querySelector(".filter-col-select");
39
+ var filterOpSelect = document.querySelector(".filter-op-select");
40
+ var filterValInput = document.querySelector(".filter-val-input");
41
+ var btnFilterAdd = document.querySelector(".btn-filter-add");
42
+ var filterModalClose = document.querySelector(".filter-modal-close");
43
+
44
+ // === Utility: Filter tables (same logic as db-manager/utils/filter-tables.js) ===
45
+ function filterTables(tables, search) {
46
+ if (!search || search.trim() === "") {
47
+ return tables.slice();
48
+ }
49
+ var needle = search.toLowerCase();
50
+ return tables.filter(function (table) {
51
+ return table.toLowerCase().indexOf(needle) !== -1;
52
+ });
53
+ }
54
+
55
+ // === Utility: Format cell value ===
56
+ // Converts ISO timestamps (YYYY-MM-DDTHH:mm:ss.000Z) to YYYY-MM-DD HH:mm:ss (UTC)
57
+ function formatCellValue(val) {
58
+ if (val === null || val === undefined) return "";
59
+ var str = String(val);
60
+ // Match ISO 8601 timestamp pattern
61
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?$/.test(str)) {
62
+ var d = new Date(str);
63
+ if (!isNaN(d.getTime())) {
64
+ var y = d.getUTCFullYear();
65
+ var m = String(d.getUTCMonth() + 1).padStart(2, "0");
66
+ var day = String(d.getUTCDate()).padStart(2, "0");
67
+ var h = String(d.getUTCHours()).padStart(2, "0");
68
+ var min = String(d.getUTCMinutes()).padStart(2, "0");
69
+ var s = String(d.getUTCSeconds()).padStart(2, "0");
70
+ return y + "-" + m + "-" + day + " " + h + ":" + min + ":" + s;
71
+ }
72
+ }
73
+ return str;
74
+ }
75
+
76
+ // === Utility: Debounce ===
77
+ function debounce(fn, delay) {
78
+ var timer = null;
79
+ return function () {
80
+ var context = this;
81
+ var args = arguments;
82
+ if (timer) clearTimeout(timer);
83
+ timer = setTimeout(function () {
84
+ fn.apply(context, args);
85
+ }, delay);
86
+ };
87
+ }
88
+
89
+ // === Utility: Encode filter value with operator prefix for library syntax ===
90
+ function encodeFilterValue(operator, value) {
91
+ switch (operator) {
92
+ case "=":
93
+ return encodeURIComponent(value);
94
+ case "!=":
95
+ return encodeURIComponent("!" + value);
96
+ case ">":
97
+ return encodeURIComponent(">" + value);
98
+ case ">=":
99
+ return encodeURIComponent(">=" + value);
100
+ case "<":
101
+ return encodeURIComponent("<" + value);
102
+ case "<=":
103
+ return encodeURIComponent("<=" + value);
104
+ case "like":
105
+ return encodeURIComponent("%" + value + "%");
106
+ case "not like":
107
+ return encodeURIComponent("!%" + value + "%");
108
+ case "in":
109
+ return encodeURIComponent("in(" + value + ")");
110
+ case "not in":
111
+ return encodeURIComponent("!in(" + value + ")");
112
+ default:
113
+ return encodeURIComponent(value);
114
+ }
115
+ }
116
+
117
+ // === Utility: Inline nextSortState (matches db-manager/utils/sort-state.js) ===
118
+ function nextSortState(currentColumn, currentDir, clickedColumn) {
119
+ // Clicking a different column → start ascending on new column
120
+ if (clickedColumn !== currentColumn) {
121
+ return { column: clickedColumn, dir: "asc" };
122
+ }
123
+ // Same column clicked — cycle through states
124
+ if (currentDir === null || currentDir === undefined) {
125
+ return { column: clickedColumn, dir: "asc" };
126
+ }
127
+ if (currentDir === "asc") {
128
+ return { column: clickedColumn, dir: "desc" };
129
+ }
130
+ // currentDir === 'desc' → clear sort
131
+ return { column: null, dir: null };
132
+ }
133
+
134
+ // === Toast Messages ===
135
+ function showToast(message, type) {
136
+ var container = document.querySelector(".toast-container");
137
+ if (!container) {
138
+ container = document.createElement("div");
139
+ container.className = "toast-container";
140
+ document.body.appendChild(container);
141
+ }
142
+ var toast = document.createElement("div");
143
+ toast.className = "toast toast-" + (type || "success");
144
+ toast.textContent = message;
145
+ container.appendChild(toast);
146
+ setTimeout(function () {
147
+ if (toast.parentNode) {
148
+ toast.parentNode.removeChild(toast);
149
+ }
150
+ }, 3000);
151
+ }
152
+
153
+ // === API Helpers ===
154
+ function apiFetch(url, options) {
155
+ return fetch(url, options)
156
+ .then(function (res) {
157
+ if (!res.ok) {
158
+ return res.json().then(function (body) {
159
+ throw new Error(body.message || "Request failed");
160
+ });
161
+ }
162
+ return res;
163
+ })
164
+ .catch(function (err) {
165
+ showToast(err.message || "Network error", "error");
166
+ throw err;
167
+ });
168
+ }
169
+
170
+ function apiGet(url) {
171
+ return apiFetch(url).then(function (res) {
172
+ return res.json();
173
+ });
174
+ }
175
+
176
+ function apiPost(url, body) {
177
+ return apiFetch(url, {
178
+ method: "POST",
179
+ headers: { "Content-Type": "application/json" },
180
+ body: JSON.stringify(body),
181
+ }).then(function (res) {
182
+ return res.json();
183
+ });
184
+ }
185
+
186
+ function apiPut(url, body) {
187
+ return apiFetch(url, {
188
+ method: "PUT",
189
+ headers: { "Content-Type": "application/json" },
190
+ body: JSON.stringify(body),
191
+ }).then(function (res) {
192
+ return res.json();
193
+ });
194
+ }
195
+
196
+ function apiDelete(url, body) {
197
+ return apiFetch(url, {
198
+ method: "DELETE",
199
+ headers: { "Content-Type": "application/json" },
200
+ body: JSON.stringify(body),
201
+ }).then(function (res) {
202
+ return res.json();
203
+ });
204
+ }
205
+
206
+ // === Sidebar: Fetch and Render Tables ===
207
+ function fetchTables() {
208
+ apiGet("/api/tables").then(function (data) {
209
+ state.tables = data.tables || [];
210
+ renderTableList(state.tables);
211
+ });
212
+ }
213
+
214
+ function renderTableList(tables) {
215
+ if (!tableList) return;
216
+ tableList.innerHTML = "";
217
+ var isTablesPage = window.location.pathname === "/tables";
218
+ tables.forEach(function (name) {
219
+ var li = document.createElement("li");
220
+ li.textContent = name;
221
+ li.setAttribute("data-table", name);
222
+ if (name === state.activeTable) {
223
+ li.classList.add("active");
224
+ }
225
+ li.addEventListener("click", function () {
226
+ if (isTablesPage) {
227
+ selectTable(name);
228
+ } else {
229
+ window.location.href = "/tables?table=" + encodeURIComponent(name);
230
+ }
231
+ });
232
+ tableList.appendChild(li);
233
+ });
234
+ }
235
+
236
+ // === Table Search ===
237
+ function onTableSearch() {
238
+ var query = tableSearchInput.value;
239
+ var filtered = filterTables(state.tables, query);
240
+ renderTableList(filtered);
241
+ }
242
+
243
+ // === Accordion Toggle ===
244
+ function setupAccordion() {
245
+ // Setup sidebar tables toggle
246
+ var tablesToggle = document.querySelector(".sidebar-tables-toggle");
247
+ if (tablesToggle) {
248
+ tablesToggle.addEventListener("click", function () {
249
+ var expanded = tablesToggle.getAttribute("aria-expanded") === "true";
250
+ tablesToggle.setAttribute("aria-expanded", expanded ? "false" : "true");
251
+ });
252
+ }
253
+ }
254
+
255
+ // === History ===
256
+ function fetchHistory() {
257
+ apiGet("/api/history/connections").then(function (data) {
258
+ var connections = data.connections || [];
259
+ historyList.innerHTML = "";
260
+ connections.forEach(function (conn) {
261
+ var li = document.createElement("li");
262
+ li.textContent =
263
+ conn.db_type +
264
+ " — " +
265
+ conn.database_name +
266
+ " (" +
267
+ conn.connected_at +
268
+ ")";
269
+ historyList.appendChild(li);
270
+ });
271
+ });
272
+ }
273
+
274
+ // === Table Selection ===
275
+ function selectTable(name) {
276
+ state.activeTable = name;
277
+ state.page = 0;
278
+ state.selectedKeys = [];
279
+ state.sortColumn = null;
280
+ state.sortDir = null;
281
+ state.filters = [];
282
+ updateSelectionButtons();
283
+
284
+ // Highlight active in sidebar
285
+ var items = tableList.querySelectorAll("li");
286
+ for (var i = 0; i < items.length; i++) {
287
+ items[i].classList.toggle(
288
+ "active",
289
+ items[i].getAttribute("data-table") === name,
290
+ );
291
+ }
292
+
293
+ // Fetch schema then rows
294
+ apiGet("/api/tables/" + encodeURIComponent(name) + "/schema").then(
295
+ function (schema) {
296
+ state.schema = schema;
297
+ state.pkColumn = schema.pk || null;
298
+ renderColumnHeaders();
299
+ renderFilterTags();
300
+ fetchRows();
301
+ },
302
+ );
303
+ }
304
+
305
+ // === Render Column Headers (with sort support) ===
306
+ function renderColumnHeaders() {
307
+ columnHeaders.innerHTML = "";
308
+ // Checkbox header
309
+ var thCheck = document.createElement("th");
310
+ var selectAll = document.createElement("input");
311
+ selectAll.type = "checkbox";
312
+ selectAll.setAttribute("aria-label", "Select all rows");
313
+ selectAll.addEventListener("change", function () {
314
+ toggleSelectAll(selectAll.checked);
315
+ });
316
+ thCheck.appendChild(selectAll);
317
+ columnHeaders.appendChild(thCheck);
318
+
319
+ // Column headers from schema
320
+ if (state.schema && state.schema.columns) {
321
+ state.schema.columns.forEach(function (col) {
322
+ var th = document.createElement("th");
323
+ th.className = "sortable";
324
+ th.setAttribute("data-column", col.name);
325
+ th.textContent = col.name + " ";
326
+
327
+ // Add sort indicator
328
+ if (state.sortColumn === col.name && state.sortDir) {
329
+ var icon = document.createElement("span");
330
+ icon.className = "material-icons sort-icon";
331
+ icon.textContent =
332
+ state.sortDir === "asc" ? "arrow_upward" : "arrow_downward";
333
+ th.appendChild(icon);
334
+ }
335
+
336
+ // Sort click handler
337
+ th.addEventListener("click", function () {
338
+ var result = nextSortState(state.sortColumn, state.sortDir, col.name);
339
+ state.sortColumn = result.column;
340
+ state.sortDir = result.dir;
341
+ state.page = 0;
342
+ renderColumnHeaders();
343
+ fetchRows();
344
+ });
345
+
346
+ columnHeaders.appendChild(th);
347
+ });
348
+ }
349
+
350
+ // Actions header
351
+ var thActions = document.createElement("th");
352
+ thActions.textContent = "Actions";
353
+ columnHeaders.appendChild(thActions);
354
+ }
355
+
356
+ // === Filter Popup and Tags ===
357
+ function openFilterModal() {
358
+ if (!filterModalOverlay || !filterColSelect) return;
359
+ // Populate column select with current schema columns
360
+ filterColSelect.innerHTML = '<option value="">Column...</option>';
361
+ if (state.schema && state.schema.columns) {
362
+ state.schema.columns.forEach(function (col) {
363
+ var opt = document.createElement("option");
364
+ opt.value = col.name;
365
+ opt.textContent = col.name;
366
+ filterColSelect.appendChild(opt);
367
+ });
368
+ }
369
+ if (filterValInput) filterValInput.value = "";
370
+ filterModalOverlay.style.display = "flex";
371
+ }
372
+
373
+ function closeFilterModal() {
374
+ if (filterModalOverlay) filterModalOverlay.style.display = "none";
375
+ }
376
+
377
+ function addFilter() {
378
+ if (!filterColSelect || !filterOpSelect || !filterValInput) return;
379
+ var col = filterColSelect.value;
380
+ var op = filterOpSelect.value;
381
+ var val = filterValInput.value.trim();
382
+ if (!col) {
383
+ showToast("Select a column", "error");
384
+ return;
385
+ }
386
+ if (!val) {
387
+ showToast("Enter a value", "error");
388
+ return;
389
+ }
390
+
391
+ state.filters.push({ column: col, operator: op, value: val });
392
+ state.page = 0;
393
+ renderFilterTags();
394
+ fetchRows();
395
+ closeFilterModal();
396
+ }
397
+
398
+ function removeFilter(index) {
399
+ state.filters.splice(index, 1);
400
+ state.page = 0;
401
+ renderFilterTags();
402
+ fetchRows();
403
+ }
404
+
405
+ function renderFilterTags() {
406
+ if (!filterTagsContainer) return;
407
+ filterTagsContainer.innerHTML = "";
408
+ state.filters.forEach(function (f, idx) {
409
+ var tag = document.createElement("span");
410
+ tag.className = "filter-tag";
411
+
412
+ var text = document.createElement("span");
413
+ text.className = "filter-tag-text";
414
+ text.textContent = f.column + " " + f.operator + " " + f.value;
415
+ text.title = f.column + " " + f.operator + " " + f.value;
416
+ tag.appendChild(text);
417
+
418
+ var removeBtn = document.createElement("button");
419
+ removeBtn.className = "filter-tag-remove";
420
+ removeBtn.innerHTML = "&times;";
421
+ removeBtn.title = "Remove filter";
422
+ removeBtn.addEventListener("click", function () {
423
+ removeFilter(idx);
424
+ });
425
+ tag.appendChild(removeBtn);
426
+
427
+ filterTagsContainer.appendChild(tag);
428
+ });
429
+ }
430
+
431
+ // === Fetch Rows (with filter and sort params) ===
432
+ function fetchRows() {
433
+ var url =
434
+ "/api/tables/" +
435
+ encodeURIComponent(state.activeTable) +
436
+ "/rows?page=" +
437
+ state.page +
438
+ "&limit=" +
439
+ state.limit;
440
+
441
+ // Append sort params
442
+ if (state.sortColumn && state.sortDir) {
443
+ url +=
444
+ "&sort=" +
445
+ encodeURIComponent(state.sortColumn) +
446
+ "&dir=" +
447
+ encodeURIComponent(state.sortDir);
448
+ }
449
+
450
+ // Append filter params using library syntax: ?column=prefixValue
451
+ for (var i = 0; i < state.filters.length; i++) {
452
+ var f = state.filters[i];
453
+ var encodedValue = encodeFilterValue(f.operator, f.value);
454
+ url += "&" + encodeURIComponent(f.column) + "=" + encodedValue;
455
+ }
456
+
457
+ apiGet(url).then(function (result) {
458
+ state.rows = result.data || [];
459
+ state.totalCount = result.count || 0;
460
+ renderRows();
461
+ updatePagination();
462
+ });
463
+ }
464
+
465
+ // === Render Rows (with Material Icon action buttons) ===
466
+ function renderRows() {
467
+ dataRows.innerHTML = "";
468
+ if (!state.schema || !state.schema.columns) return;
469
+
470
+ state.rows.forEach(function (row) {
471
+ var tr = document.createElement("tr");
472
+ var pkValue = state.pkColumn ? row[state.pkColumn] : null;
473
+
474
+ // Checkbox cell
475
+ var tdCheck = document.createElement("td");
476
+ var checkbox = document.createElement("input");
477
+ checkbox.type = "checkbox";
478
+ checkbox.setAttribute("aria-label", "Select row");
479
+ checkbox.checked = state.selectedKeys.indexOf(pkValue) !== -1;
480
+ checkbox.addEventListener("change", function () {
481
+ toggleRowSelection(pkValue, checkbox.checked);
482
+ tr.classList.toggle("selected", checkbox.checked);
483
+ });
484
+ tdCheck.appendChild(checkbox);
485
+ tr.appendChild(tdCheck);
486
+
487
+ // Data cells
488
+ state.schema.columns.forEach(function (col) {
489
+ var td = document.createElement("td");
490
+ var val = row[col.name];
491
+ td.textContent = formatCellValue(val);
492
+ td.setAttribute("data-column", col.name);
493
+ tr.appendChild(td);
494
+ });
495
+
496
+ // Actions cell with Material Icons
497
+ var tdActions = document.createElement("td");
498
+ tdActions.className = "row-actions";
499
+
500
+ var btnEdit = document.createElement("button");
501
+ btnEdit.className = "btn-edit";
502
+ btnEdit.setAttribute("aria-label", "Edit row");
503
+ var editIcon = document.createElement("span");
504
+ editIcon.className = "material-icons";
505
+ editIcon.textContent = "edit";
506
+ btnEdit.appendChild(editIcon);
507
+ btnEdit.addEventListener("click", function () {
508
+ startInlineEdit(tr, row);
509
+ });
510
+ tdActions.appendChild(btnEdit);
511
+
512
+ var btnRowDelete = document.createElement("button");
513
+ btnRowDelete.className = "btn-row-delete";
514
+ btnRowDelete.setAttribute("aria-label", "Delete row");
515
+ var deleteIcon = document.createElement("span");
516
+ deleteIcon.className = "material-icons";
517
+ deleteIcon.textContent = "delete";
518
+ btnRowDelete.appendChild(deleteIcon);
519
+ btnRowDelete.addEventListener("click", function () {
520
+ deleteSingleRow(pkValue);
521
+ });
522
+ tdActions.appendChild(btnRowDelete);
523
+
524
+ tr.appendChild(tdActions);
525
+
526
+ if (state.selectedKeys.indexOf(pkValue) !== -1) {
527
+ tr.classList.add("selected");
528
+ }
529
+
530
+ dataRows.appendChild(tr);
531
+ });
532
+ }
533
+
534
+ // === Delete Single Row ===
535
+ function deleteSingleRow(pkValue) {
536
+ if (!state.pkColumn || pkValue === null || pkValue === undefined) return;
537
+ var confirmed = confirm("Are you sure you want to delete this row?");
538
+ if (!confirmed) return;
539
+
540
+ apiDelete(
541
+ "/api/tables/" + encodeURIComponent(state.activeTable) + "/rows",
542
+ {
543
+ keys: [pkValue],
544
+ pkColumn: state.pkColumn,
545
+ },
546
+ ).then(function (result) {
547
+ showToast(result.message || "Row deleted", "success");
548
+ // Remove from selection if selected
549
+ var idx = state.selectedKeys.indexOf(pkValue);
550
+ if (idx !== -1) {
551
+ state.selectedKeys.splice(idx, 1);
552
+ }
553
+ updateSelectionButtons();
554
+ fetchRows();
555
+ });
556
+ }
557
+
558
+ // === Row Selection ===
559
+ function toggleRowSelection(pkValue, selected) {
560
+ if (selected) {
561
+ if (state.selectedKeys.indexOf(pkValue) === -1) {
562
+ state.selectedKeys.push(pkValue);
563
+ }
564
+ } else {
565
+ var idx = state.selectedKeys.indexOf(pkValue);
566
+ if (idx !== -1) {
567
+ state.selectedKeys.splice(idx, 1);
568
+ }
569
+ }
570
+ updateSelectionButtons();
571
+ }
572
+
573
+ function toggleSelectAll(checked) {
574
+ state.selectedKeys = [];
575
+ if (checked) {
576
+ state.rows.forEach(function (row) {
577
+ if (state.pkColumn && row[state.pkColumn] !== undefined) {
578
+ state.selectedKeys.push(row[state.pkColumn]);
579
+ }
580
+ });
581
+ }
582
+ renderRows();
583
+ updateSelectionButtons();
584
+ }
585
+
586
+ function updateSelectionButtons() {
587
+ var hasSelection = state.selectedKeys.length > 0;
588
+ if (btnDelete) btnDelete.disabled = !hasSelection;
589
+ if (btnExport) btnExport.disabled = !hasSelection;
590
+ }
591
+
592
+ // === Pagination ===
593
+ function updatePagination() {
594
+ var totalPages =
595
+ state.limit > 0 ? Math.ceil(state.totalCount / state.limit) : 1;
596
+ var currentPage = state.page + 1;
597
+ pageInfo.textContent =
598
+ state.totalCount > 0
599
+ ? "Page " +
600
+ currentPage +
601
+ " of " +
602
+ totalPages +
603
+ " (" +
604
+ state.totalCount +
605
+ " rows)"
606
+ : "No data";
607
+ btnPrev.disabled = state.page <= 0;
608
+ btnNext.disabled = state.limit === 0 || currentPage >= totalPages;
609
+ }
610
+
611
+ function goToPrevPage() {
612
+ if (state.page > 0) {
613
+ state.page--;
614
+ fetchRows();
615
+ }
616
+ }
617
+
618
+ function goToNextPage() {
619
+ var totalPages =
620
+ state.limit > 0 ? Math.ceil(state.totalCount / state.limit) : 1;
621
+ if (state.page + 1 < totalPages) {
622
+ state.page++;
623
+ fetchRows();
624
+ }
625
+ }
626
+
627
+ function onPageSizeChange() {
628
+ state.limit = parseInt(pageSize.value, 10);
629
+ state.page = 0;
630
+ if (state.activeTable) {
631
+ fetchRows();
632
+ }
633
+ }
634
+
635
+ // === Add Row Form ===
636
+ function showAddForm() {
637
+ if (!state.schema || !state.activeTable) {
638
+ showToast("Select a table first", "error");
639
+ return;
640
+ }
641
+
642
+ // Remove existing form if present
643
+ removeAddForm();
644
+
645
+ var panel = document.querySelector(".data-panel-content");
646
+ var form = document.createElement("div");
647
+ form.className = "row-form add-row-form";
648
+
649
+ var title = document.createElement("h3");
650
+ title.textContent = "Add Row to " + state.activeTable;
651
+ form.appendChild(title);
652
+
653
+ var grid = document.createElement("div");
654
+ grid.className = "form-grid";
655
+
656
+ state.schema.columns.forEach(function (col) {
657
+ // Skip auto-increment PK fields
658
+ if (
659
+ col.pk &&
660
+ col.type &&
661
+ col.type.toUpperCase().indexOf("INTEGER") !== -1
662
+ ) {
663
+ return;
664
+ }
665
+ var field = document.createElement("div");
666
+ field.className = "form-field";
667
+
668
+ var label = document.createElement("label");
669
+ label.textContent = col.name + (col.nullable ? "" : " *");
670
+ label.setAttribute("for", "add-" + col.name);
671
+ field.appendChild(label);
672
+
673
+ var input = document.createElement("input");
674
+ input.type = "text";
675
+ input.id = "add-" + col.name;
676
+ input.name = col.name;
677
+ input.placeholder = col.type || "";
678
+ if (col.default !== null && col.default !== undefined) {
679
+ input.placeholder += " (default: " + col.default + ")";
680
+ }
681
+ field.appendChild(input);
682
+ grid.appendChild(field);
683
+ });
684
+
685
+ form.appendChild(grid);
686
+
687
+ var actions = document.createElement("div");
688
+ actions.className = "form-actions";
689
+
690
+ var btnSave = document.createElement("button");
691
+ btnSave.className = "btn btn-save";
692
+ btnSave.textContent = "Save";
693
+ btnSave.type = "button";
694
+ btnSave.addEventListener("click", function () {
695
+ submitAddForm(form);
696
+ });
697
+
698
+ var btnCancel = document.createElement("button");
699
+ btnCancel.className = "btn btn-cancel";
700
+ btnCancel.textContent = "Cancel";
701
+ btnCancel.type = "button";
702
+ btnCancel.addEventListener("click", function () {
703
+ removeAddForm();
704
+ });
705
+
706
+ actions.appendChild(btnSave);
707
+ actions.appendChild(btnCancel);
708
+ form.appendChild(actions);
709
+
710
+ panel.insertBefore(form, panel.firstChild);
711
+ }
712
+
713
+ function removeAddForm() {
714
+ var existing = document.querySelector(".add-row-form");
715
+ if (existing) {
716
+ existing.parentNode.removeChild(existing);
717
+ }
718
+ }
719
+
720
+ function submitAddForm(form) {
721
+ var inputs = form.querySelectorAll(".form-grid input");
722
+ var data = {};
723
+ for (var i = 0; i < inputs.length; i++) {
724
+ var name = inputs[i].name;
725
+ var value = inputs[i].value;
726
+ if (value !== "") {
727
+ data[name] = value;
728
+ }
729
+ }
730
+
731
+ if (Object.keys(data).length === 0) {
732
+ showToast("Please fill in at least one field", "error");
733
+ return;
734
+ }
735
+
736
+ apiPost("/api/tables/" + encodeURIComponent(state.activeTable) + "/rows", {
737
+ data: data,
738
+ }).then(function (result) {
739
+ showToast(result.message || "Row added successfully", "success");
740
+ removeAddForm();
741
+ fetchRows();
742
+ });
743
+ }
744
+
745
+ // === Inline Edit ===
746
+ function startInlineEdit(tr, row) {
747
+ if (!state.schema || !state.pkColumn) return;
748
+
749
+ // Get data cells (skip checkbox at index 0 and actions at end)
750
+ var cells = tr.querySelectorAll("td[data-column]");
751
+ var originalValues = {};
752
+
753
+ for (var i = 0; i < cells.length; i++) {
754
+ var col = cells[i].getAttribute("data-column");
755
+ originalValues[col] = cells[i].textContent;
756
+
757
+ // Don't make PK editable
758
+ if (col === state.pkColumn) continue;
759
+
760
+ var input = document.createElement("input");
761
+ input.type = "text";
762
+ input.value = cells[i].textContent;
763
+ input.setAttribute("data-column", col);
764
+ cells[i].textContent = "";
765
+ cells[i].appendChild(input);
766
+ }
767
+
768
+ // Replace actions
769
+ var actionsCell = tr.querySelector(".row-actions");
770
+ actionsCell.innerHTML = "";
771
+
772
+ var btnSave = document.createElement("button");
773
+ btnSave.className = "btn-row-save";
774
+ btnSave.textContent = "Save";
775
+ btnSave.addEventListener("click", function () {
776
+ saveInlineEdit(tr, row, originalValues);
777
+ });
778
+
779
+ var btnCancel = document.createElement("button");
780
+ btnCancel.className = "btn-row-cancel";
781
+ btnCancel.textContent = "Cancel";
782
+ btnCancel.addEventListener("click", function () {
783
+ cancelInlineEdit(tr, originalValues);
784
+ });
785
+
786
+ actionsCell.appendChild(btnSave);
787
+ actionsCell.appendChild(btnCancel);
788
+ }
789
+
790
+ function saveInlineEdit(tr, row, originalValues) {
791
+ var inputs = tr.querySelectorAll("td[data-column] input");
792
+ var data = {};
793
+
794
+ // Include PK value
795
+ if (state.pkColumn) {
796
+ data[state.pkColumn] = row[state.pkColumn];
797
+ }
798
+
799
+ for (var i = 0; i < inputs.length; i++) {
800
+ var col = inputs[i].getAttribute("data-column");
801
+ data[col] = inputs[i].value;
802
+ }
803
+
804
+ // Include unchanged columns from original
805
+ for (var key in originalValues) {
806
+ if (!(key in data)) {
807
+ data[key] = originalValues[key];
808
+ }
809
+ }
810
+
811
+ apiPut("/api/tables/" + encodeURIComponent(state.activeTable) + "/rows", {
812
+ data: data,
813
+ uniqueKeys: [state.pkColumn],
814
+ }).then(function (result) {
815
+ showToast(result.message || "Row updated successfully", "success");
816
+ fetchRows();
817
+ });
818
+ }
819
+
820
+ function cancelInlineEdit(tr, originalValues) {
821
+ var cells = tr.querySelectorAll("td[data-column]");
822
+ for (var i = 0; i < cells.length; i++) {
823
+ var col = cells[i].getAttribute("data-column");
824
+ cells[i].innerHTML = "";
825
+ cells[i].textContent = originalValues[col] || "";
826
+ }
827
+
828
+ // Restore action buttons with Material Icons
829
+ var actionsCell = tr.querySelector(".row-actions");
830
+ actionsCell.innerHTML = "";
831
+
832
+ var btnEdit = document.createElement("button");
833
+ btnEdit.className = "btn-edit";
834
+ btnEdit.setAttribute("aria-label", "Edit row");
835
+ var editIcon = document.createElement("span");
836
+ editIcon.className = "material-icons";
837
+ editIcon.textContent = "edit";
838
+ btnEdit.appendChild(editIcon);
839
+ btnEdit.addEventListener("click", function () {
840
+ startInlineEdit(tr, originalValues);
841
+ });
842
+ actionsCell.appendChild(btnEdit);
843
+
844
+ var btnRowDelete = document.createElement("button");
845
+ btnRowDelete.className = "btn-row-delete";
846
+ btnRowDelete.setAttribute("aria-label", "Delete row");
847
+ var deleteIcon = document.createElement("span");
848
+ deleteIcon.className = "material-icons";
849
+ deleteIcon.textContent = "delete";
850
+ btnRowDelete.appendChild(deleteIcon);
851
+ actionsCell.appendChild(btnRowDelete);
852
+ }
853
+
854
+ // === Delete Selected Rows ===
855
+ function deleteSelectedRows() {
856
+ if (state.selectedKeys.length === 0 || !state.pkColumn) return;
857
+
858
+ var count = state.selectedKeys.length;
859
+ var confirmed = confirm(
860
+ "Are you sure you want to delete " + count + " row(s)?",
861
+ );
862
+ if (!confirmed) return;
863
+
864
+ apiDelete(
865
+ "/api/tables/" + encodeURIComponent(state.activeTable) + "/rows",
866
+ {
867
+ keys: state.selectedKeys,
868
+ pkColumn: state.pkColumn,
869
+ },
870
+ ).then(function (result) {
871
+ showToast(result.message || count + " row(s) deleted", "success");
872
+ state.selectedKeys = [];
873
+ updateSelectionButtons();
874
+ fetchRows();
875
+ });
876
+ }
877
+
878
+ // === Export Selected Rows (CSV with Content-Disposition) ===
879
+ function exportSelectedRows() {
880
+ if (state.selectedKeys.length === 0 || !state.pkColumn) return;
881
+
882
+ var url =
883
+ "/api/tables/" + encodeURIComponent(state.activeTable) + "/export";
884
+
885
+ fetch(url, {
886
+ method: "POST",
887
+ headers: { "Content-Type": "application/json" },
888
+ body: JSON.stringify({
889
+ keys: state.selectedKeys,
890
+ pkColumn: state.pkColumn,
891
+ }),
892
+ })
893
+ .then(function (res) {
894
+ if (!res.ok) {
895
+ return res.json().then(function (body) {
896
+ throw new Error(body.message || "Export failed");
897
+ });
898
+ }
899
+ // Parse filename from Content-Disposition header
900
+ var disposition = res.headers.get("Content-Disposition");
901
+ var filename = null;
902
+ if (disposition) {
903
+ var match = disposition.match(/filename="?([^";\s]+)"?/);
904
+ if (match) {
905
+ filename = match[1];
906
+ }
907
+ }
908
+ if (!filename) {
909
+ var timestamp = new Date()
910
+ .toISOString()
911
+ .replace(/[-:]/g, "")
912
+ .replace(/\.\d+Z$/, "");
913
+ filename = state.activeTable + "_" + timestamp + ".csv";
914
+ }
915
+ return res.blob().then(function (blob) {
916
+ return { blob: blob, filename: filename };
917
+ });
918
+ })
919
+ .then(function (result) {
920
+ var a = document.createElement("a");
921
+ a.href = URL.createObjectURL(result.blob);
922
+ a.download = result.filename;
923
+ document.body.appendChild(a);
924
+ a.click();
925
+ document.body.removeChild(a);
926
+ URL.revokeObjectURL(a.href);
927
+ showToast("Export downloaded", "success");
928
+ })
929
+ .catch(function (err) {
930
+ showToast(err.message || "Export failed", "error");
931
+ });
932
+ }
933
+
934
+ // === Query Page Logic ===
935
+ function initQueryPage() {
936
+ var queryContent = document.querySelector(".query-content");
937
+ if (!queryContent) return;
938
+
939
+ var queryEditor = queryContent.querySelector(".query-editor");
940
+ var btnRun = queryContent.querySelector(".btn-run");
941
+ var btnExportQuery = queryContent.querySelector(".btn-export-query");
942
+ var queryError = queryContent.querySelector(".query-error");
943
+ var queryColumnHeaders = queryContent.querySelector(
944
+ ".query-column-headers",
945
+ );
946
+ var queryDataRows = queryContent.querySelector(".query-data-rows");
947
+
948
+ var lastQuery = "";
949
+
950
+ function clearResults() {
951
+ if (queryColumnHeaders) queryColumnHeaders.innerHTML = "";
952
+ if (queryDataRows) queryDataRows.innerHTML = "";
953
+ if (queryError) {
954
+ queryError.style.display = "none";
955
+ queryError.textContent = "";
956
+ }
957
+ if (btnExportQuery) btnExportQuery.disabled = true;
958
+ }
959
+
960
+ function showQueryError(message) {
961
+ if (queryError) {
962
+ queryError.textContent = message;
963
+ queryError.style.display = "block";
964
+ }
965
+ }
966
+
967
+ function renderQueryResults(columns, data) {
968
+ // Render column headers
969
+ if (queryColumnHeaders) {
970
+ queryColumnHeaders.innerHTML = "";
971
+ columns.forEach(function (col) {
972
+ var th = document.createElement("th");
973
+ th.textContent = col;
974
+ queryColumnHeaders.appendChild(th);
975
+ });
976
+ }
977
+
978
+ // Render data rows
979
+ if (queryDataRows) {
980
+ queryDataRows.innerHTML = "";
981
+ data.forEach(function (row) {
982
+ var tr = document.createElement("tr");
983
+ columns.forEach(function (col) {
984
+ var td = document.createElement("td");
985
+ var val = row[col];
986
+ td.textContent = formatCellValue(val);
987
+ tr.appendChild(td);
988
+ });
989
+ queryDataRows.appendChild(tr);
990
+ });
991
+ }
992
+
993
+ if (btnExportQuery) btnExportQuery.disabled = false;
994
+ }
995
+
996
+ // Run button handler
997
+ if (btnRun) {
998
+ btnRun.addEventListener("click", function () {
999
+ var queryText = queryEditor ? queryEditor.value.trim() : "";
1000
+ if (!queryText) {
1001
+ showToast("Please enter a query", "error");
1002
+ return;
1003
+ }
1004
+ lastQuery = queryText;
1005
+ clearResults();
1006
+
1007
+ fetch("/api/query", {
1008
+ method: "POST",
1009
+ headers: { "Content-Type": "application/json" },
1010
+ body: JSON.stringify({ query: queryText }),
1011
+ })
1012
+ .then(function (res) {
1013
+ return res.json().then(function (body) {
1014
+ return { ok: res.ok, body: body };
1015
+ });
1016
+ })
1017
+ .then(function (result) {
1018
+ if (!result.ok || result.body.error) {
1019
+ showQueryError(result.body.message || "Query execution failed");
1020
+ return;
1021
+ }
1022
+ renderQueryResults(
1023
+ result.body.columns || [],
1024
+ result.body.data || [],
1025
+ );
1026
+ })
1027
+ .catch(function (err) {
1028
+ showQueryError(err.message || "Network error");
1029
+ });
1030
+ });
1031
+ }
1032
+
1033
+ // Export button handler
1034
+ if (btnExportQuery) {
1035
+ btnExportQuery.addEventListener("click", function () {
1036
+ if (!lastQuery) return;
1037
+
1038
+ fetch("/api/query/export", {
1039
+ method: "POST",
1040
+ headers: { "Content-Type": "application/json" },
1041
+ body: JSON.stringify({ query: lastQuery }),
1042
+ })
1043
+ .then(function (res) {
1044
+ if (!res.ok) {
1045
+ return res.json().then(function (body) {
1046
+ throw new Error(body.message || "Export failed");
1047
+ });
1048
+ }
1049
+ // Parse filename from Content-Disposition header
1050
+ var disposition = res.headers.get("Content-Disposition");
1051
+ var filename = null;
1052
+ if (disposition) {
1053
+ var match = disposition.match(/filename="?([^";\s]+)"?/);
1054
+ if (match) {
1055
+ filename = match[1];
1056
+ }
1057
+ }
1058
+ if (!filename) {
1059
+ var timestamp = new Date()
1060
+ .toISOString()
1061
+ .replace(/[-:]/g, "")
1062
+ .replace(/\.\d+Z$/, "");
1063
+ filename = "export_" + timestamp + ".csv";
1064
+ }
1065
+ return res.blob().then(function (blob) {
1066
+ return { blob: blob, filename: filename };
1067
+ });
1068
+ })
1069
+ .then(function (result) {
1070
+ var a = document.createElement("a");
1071
+ a.href = URL.createObjectURL(result.blob);
1072
+ a.download = result.filename;
1073
+ document.body.appendChild(a);
1074
+ a.click();
1075
+ document.body.removeChild(a);
1076
+ URL.revokeObjectURL(a.href);
1077
+ showToast("Export downloaded", "success");
1078
+ })
1079
+ .catch(function (err) {
1080
+ showToast(err.message || "Export failed", "error");
1081
+ });
1082
+ });
1083
+ }
1084
+ }
1085
+
1086
+ // === Dashboard Page Logic ===
1087
+ function initDashboardPage() {
1088
+ var dashboardContent = document.querySelector(".dashboard-content");
1089
+ if (!dashboardContent) return;
1090
+
1091
+ var tableBody = dashboardContent.querySelector(".dashboard-table-body");
1092
+ if (!tableBody) return;
1093
+
1094
+ apiGet("/api/dashboard").then(function (data) {
1095
+ var tables = data.tables || [];
1096
+ tableBody.innerHTML = "";
1097
+
1098
+ if (tables.length === 0) {
1099
+ var tr = document.createElement("tr");
1100
+ var td = document.createElement("td");
1101
+ td.colSpan = 5;
1102
+ td.textContent = "No tables found.";
1103
+ td.style.textAlign = "center";
1104
+ td.style.padding = "24px";
1105
+ tr.appendChild(td);
1106
+ tableBody.appendChild(tr);
1107
+ return;
1108
+ }
1109
+
1110
+ tables.forEach(function (table) {
1111
+ var tr = document.createElement("tr");
1112
+ tr.className = "dashboard-row";
1113
+ tr.setAttribute("data-table", table.name);
1114
+
1115
+ var tdName = document.createElement("td");
1116
+ tdName.className = "dashboard-table-name";
1117
+ tdName.textContent = table.name;
1118
+ tr.appendChild(tdName);
1119
+
1120
+ var tdCols = document.createElement("td");
1121
+ tdCols.textContent = table.columnCount;
1122
+ tr.appendChild(tdCols);
1123
+
1124
+ var tdIndexes = document.createElement("td");
1125
+ tdIndexes.textContent =
1126
+ table.indexCount !== undefined ? table.indexCount : "—";
1127
+ tr.appendChild(tdIndexes);
1128
+
1129
+ var tdRows = document.createElement("td");
1130
+ tdRows.textContent = table.rowCount.toLocaleString();
1131
+ tr.appendChild(tdRows);
1132
+
1133
+ var tdSize = document.createElement("td");
1134
+ tdSize.textContent =
1135
+ table.sizeMB !== undefined ? table.sizeMB.toFixed(3) : "—";
1136
+ tr.appendChild(tdSize);
1137
+
1138
+ tr.addEventListener("click", function () {
1139
+ window.location.href =
1140
+ "/tables?table=" + encodeURIComponent(table.name);
1141
+ });
1142
+
1143
+ tableBody.appendChild(tr);
1144
+ });
1145
+ });
1146
+ }
1147
+
1148
+ // === Sidebar Nav Link Active State and Click Handling ===
1149
+ function initNavLinks() {
1150
+ var navLinks = document.querySelectorAll(".nav-link");
1151
+ var currentPath = window.location.pathname;
1152
+
1153
+ for (var i = 0; i < navLinks.length; i++) {
1154
+ var link = navLinks[i];
1155
+ var page = link.getAttribute("data-page");
1156
+
1157
+ // Set active state based on current URL
1158
+ if (
1159
+ (page === "dashboard" && currentPath === "/dashboard") ||
1160
+ (page === "tables" && currentPath === "/tables") ||
1161
+ (page === "query" && currentPath === "/query") ||
1162
+ (page === "history" && currentPath === "/history")
1163
+ ) {
1164
+ link.classList.add("active");
1165
+ }
1166
+
1167
+ // Click handling
1168
+ (function (navLink) {
1169
+ navLink.addEventListener("click", function (e) {
1170
+ e.preventDefault();
1171
+ var href = navLink.getAttribute("href");
1172
+ if (href) {
1173
+ window.location.href = href;
1174
+ }
1175
+ });
1176
+ })(link);
1177
+ }
1178
+ }
1179
+
1180
+ // === History Page Logic ===
1181
+ function initHistoryPage() {
1182
+ var historyContent = document.querySelector(".history-content");
1183
+ if (!historyContent) return;
1184
+
1185
+ var tableBody = historyContent.querySelector(".history-table-body");
1186
+ if (!tableBody) return;
1187
+
1188
+ apiGet("/api/history/queries").then(function (data) {
1189
+ var queries = data.queries || [];
1190
+ tableBody.innerHTML = "";
1191
+
1192
+ if (queries.length === 0) {
1193
+ var tr = document.createElement("tr");
1194
+ var td = document.createElement("td");
1195
+ td.colSpan = 4;
1196
+ td.textContent = "No query history found.";
1197
+ td.style.textAlign = "center";
1198
+ td.style.padding = "24px";
1199
+ tr.appendChild(td);
1200
+ tableBody.appendChild(tr);
1201
+ return;
1202
+ }
1203
+
1204
+ queries.forEach(function (q, idx) {
1205
+ var tr = document.createElement("tr");
1206
+
1207
+ var tdNum = document.createElement("td");
1208
+ tdNum.textContent = idx + 1;
1209
+ tr.appendChild(tdNum);
1210
+
1211
+ var tdQuery = document.createElement("td");
1212
+ tdQuery.className = "history-query-text";
1213
+ tdQuery.textContent = q.query_text || q.queryText || "";
1214
+ tdQuery.title = q.query_text || q.queryText || "";
1215
+ tr.appendChild(tdQuery);
1216
+
1217
+ var tdRows = document.createElement("td");
1218
+ tdRows.textContent =
1219
+ q.row_count !== undefined
1220
+ ? q.row_count
1221
+ : q.rowCount !== undefined
1222
+ ? q.rowCount
1223
+ : "—";
1224
+ tr.appendChild(tdRows);
1225
+
1226
+ var tdTime = document.createElement("td");
1227
+ tdTime.textContent = formatCellValue(
1228
+ q.executed_at || q.executedAt || "—",
1229
+ );
1230
+ tr.appendChild(tdTime);
1231
+
1232
+ tableBody.appendChild(tr);
1233
+ });
1234
+ });
1235
+ }
1236
+
1237
+ // === Event Bindings ===
1238
+ function bindEvents() {
1239
+ if (tableSearchInput) {
1240
+ tableSearchInput.addEventListener("input", onTableSearch);
1241
+ }
1242
+ if (btnAdd) btnAdd.addEventListener("click", showAddForm);
1243
+ if (btnDelete) btnDelete.addEventListener("click", deleteSelectedRows);
1244
+ if (btnExport) btnExport.addEventListener("click", exportSelectedRows);
1245
+ if (btnFilter) btnFilter.addEventListener("click", openFilterModal);
1246
+ if (btnFilterAdd) btnFilterAdd.addEventListener("click", addFilter);
1247
+ if (filterModalClose)
1248
+ filterModalClose.addEventListener("click", closeFilterModal);
1249
+ if (filterModalOverlay) {
1250
+ filterModalOverlay.addEventListener("click", function (e) {
1251
+ if (e.target === filterModalOverlay) closeFilterModal();
1252
+ });
1253
+ }
1254
+ if (filterValInput) {
1255
+ filterValInput.addEventListener("keydown", function (e) {
1256
+ if (e.key === "Enter") addFilter();
1257
+ });
1258
+ }
1259
+ if (btnPrev) btnPrev.addEventListener("click", goToPrevPage);
1260
+ if (btnNext) btnNext.addEventListener("click", goToNextPage);
1261
+ if (pageSize) pageSize.addEventListener("change", onPageSizeChange);
1262
+ }
1263
+
1264
+ // === Theme Switcher ===
1265
+ function initThemeSwitcher() {
1266
+ var themeBtns = document.querySelectorAll(".theme-btn");
1267
+ if (!themeBtns.length) return;
1268
+
1269
+ var currentTheme = localStorage.getItem("dbm-theme") || "system";
1270
+
1271
+ function setActiveBtn(theme) {
1272
+ for (var i = 0; i < themeBtns.length; i++) {
1273
+ var btn = themeBtns[i];
1274
+ if (btn.getAttribute("data-theme-value") === theme) {
1275
+ btn.classList.add("active");
1276
+ btn.setAttribute("aria-checked", "true");
1277
+ } else {
1278
+ btn.classList.remove("active");
1279
+ btn.setAttribute("aria-checked", "false");
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ function applyTheme(theme) {
1285
+ document.documentElement.setAttribute("data-theme", theme);
1286
+ localStorage.setItem("dbm-theme", theme);
1287
+ setActiveBtn(theme);
1288
+ }
1289
+
1290
+ // Set initial state
1291
+ setActiveBtn(currentTheme);
1292
+
1293
+ // Bind click handlers
1294
+ for (var i = 0; i < themeBtns.length; i++) {
1295
+ (function (btn) {
1296
+ btn.addEventListener("click", function () {
1297
+ var theme = btn.getAttribute("data-theme-value");
1298
+ applyTheme(theme);
1299
+ });
1300
+ })(themeBtns[i]);
1301
+ }
1302
+ }
1303
+
1304
+ // === Initialize ===
1305
+ function init() {
1306
+ // Initialize theme switcher on all pages
1307
+ initThemeSwitcher();
1308
+
1309
+ // Initialize nav links on all pages
1310
+ initNavLinks();
1311
+
1312
+ // Setup sidebar tables accordion toggle
1313
+ setupAccordion();
1314
+
1315
+ // Always fetch and display tables in the sidebar (visible on all pages)
1316
+ fetchTables();
1317
+
1318
+ // Setup table search on all pages
1319
+ if (tableSearchInput) {
1320
+ tableSearchInput.addEventListener("input", onTableSearch);
1321
+ }
1322
+
1323
+ // Detect which page we're on and initialize accordingly
1324
+ var queryContent = document.querySelector(".query-content");
1325
+ var dashboardContent = document.querySelector(".dashboard-content");
1326
+ var historyContent = document.querySelector(".history-content");
1327
+
1328
+ if (queryContent) {
1329
+ // Query page
1330
+ initQueryPage();
1331
+ return;
1332
+ }
1333
+
1334
+ if (dashboardContent) {
1335
+ // Dashboard page
1336
+ initDashboardPage();
1337
+ return;
1338
+ }
1339
+
1340
+ if (historyContent) {
1341
+ // History page
1342
+ initHistoryPage();
1343
+ return;
1344
+ }
1345
+
1346
+ // Default: Table browser page
1347
+ bindEvents();
1348
+
1349
+ // Check if a table is specified in the URL
1350
+ var urlParams = new URLSearchParams(window.location.search);
1351
+ var tableParam = urlParams.get("table");
1352
+ if (tableParam) {
1353
+ // Wait for tables to load, then select the specified table
1354
+ apiGet("/api/tables").then(function (data) {
1355
+ state.tables = data.tables || [];
1356
+ renderTableList(state.tables);
1357
+ if (state.tables.indexOf(tableParam) !== -1) {
1358
+ selectTable(tableParam);
1359
+ }
1360
+ });
1361
+ }
1362
+ }
1363
+
1364
+ // Start the app when DOM is ready
1365
+ if (document.readyState === "loading") {
1366
+ document.addEventListener("DOMContentLoaded", init);
1367
+ } else {
1368
+ init();
1369
+ }
1370
+ })();