admindb 1.2.0 → 2.0.0

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 (202) hide show
  1. package/README.md +250 -352
  2. package/dist/app.d.ts +25 -4
  3. package/dist/app.js +79 -15
  4. package/dist/app.js.map +1 -1
  5. package/dist/auth/config.d.ts +19 -0
  6. package/dist/auth/config.js +147 -0
  7. package/dist/auth/config.js.map +1 -0
  8. package/dist/auth/crypto.d.ts +27 -0
  9. package/dist/auth/crypto.js +133 -0
  10. package/dist/auth/crypto.js.map +1 -0
  11. package/dist/auth/index.d.ts +5 -0
  12. package/dist/auth/index.js +22 -0
  13. package/dist/auth/index.js.map +1 -0
  14. package/dist/auth/middleware.d.ts +14 -0
  15. package/dist/auth/middleware.js +100 -0
  16. package/dist/auth/middleware.js.map +1 -0
  17. package/dist/auth/routes.d.ts +7 -0
  18. package/dist/auth/routes.js +78 -0
  19. package/dist/auth/routes.js.map +1 -0
  20. package/dist/auth/types.d.ts +35 -0
  21. package/dist/auth/types.js +11 -0
  22. package/dist/auth/types.js.map +1 -0
  23. package/dist/cli/args.d.ts +45 -0
  24. package/dist/cli/args.js +328 -0
  25. package/dist/cli/args.js.map +1 -0
  26. package/dist/cli/config.d.ts +56 -0
  27. package/dist/cli/config.js +133 -0
  28. package/dist/cli/config.js.map +1 -0
  29. package/dist/cli/index.d.ts +4 -0
  30. package/dist/cli/index.js +21 -0
  31. package/dist/cli/index.js.map +1 -0
  32. package/dist/cli/runner.d.ts +1 -0
  33. package/dist/cli/runner.js +222 -0
  34. package/dist/cli/runner.js.map +1 -0
  35. package/dist/cli.js +2 -127
  36. package/dist/cli.js.map +1 -1
  37. package/dist/data/datasets.d.ts +17 -0
  38. package/dist/data/datasets.js +133 -0
  39. package/dist/data/datasets.js.map +1 -0
  40. package/dist/data/detector.d.ts +30 -0
  41. package/dist/data/detector.js +472 -0
  42. package/dist/data/detector.js.map +1 -0
  43. package/dist/data/engine.d.ts +10 -0
  44. package/dist/data/engine.js +211 -0
  45. package/dist/data/engine.js.map +1 -0
  46. package/dist/data/index.d.ts +5 -0
  47. package/dist/data/index.js +22 -0
  48. package/dist/data/index.js.map +1 -0
  49. package/dist/data/strategies.d.ts +30 -0
  50. package/dist/data/strategies.js +343 -0
  51. package/dist/data/strategies.js.map +1 -0
  52. package/dist/data/types.d.ts +73 -0
  53. package/dist/data/types.js +8 -0
  54. package/dist/data/types.js.map +1 -0
  55. package/dist/db/database.d.ts +14 -142
  56. package/dist/db/database.js +209 -435
  57. package/dist/db/database.js.map +1 -1
  58. package/dist/db/export.d.ts +2 -2
  59. package/dist/db/export.js +7 -2
  60. package/dist/db/export.js.map +1 -1
  61. package/dist/db/filters.d.ts +22 -0
  62. package/dist/db/filters.js +158 -0
  63. package/dist/db/filters.js.map +1 -0
  64. package/dist/db/index.d.ts +8 -0
  65. package/dist/db/index.js +25 -0
  66. package/dist/db/index.js.map +1 -0
  67. package/dist/db/introspection.d.ts +6 -0
  68. package/dist/db/introspection.js +172 -0
  69. package/dist/db/introspection.js.map +1 -0
  70. package/dist/db/manager.d.ts +32 -19
  71. package/dist/db/manager.js +203 -44
  72. package/dist/db/manager.js.map +1 -1
  73. package/dist/db/migrations.d.ts +5 -0
  74. package/dist/db/migrations.js +110 -0
  75. package/dist/db/migrations.js.map +1 -0
  76. package/dist/db/postgres.d.ts +85 -0
  77. package/dist/db/postgres.js +745 -0
  78. package/dist/db/postgres.js.map +1 -0
  79. package/dist/db/types.d.ts +206 -0
  80. package/dist/db/types.js +7 -0
  81. package/dist/db/types.js.map +1 -0
  82. package/dist/index.d.ts +9 -2
  83. package/dist/index.js +46 -4
  84. package/dist/index.js.map +1 -1
  85. package/dist/public/css/app.css +1 -1
  86. package/dist/public/css/input.css +83 -13
  87. package/dist/public/js/app.js +142 -0
  88. package/dist/public/js/browse.js +591 -104
  89. package/dist/public/js/databases.js +105 -13
  90. package/dist/public/js/designer.js +40 -2
  91. package/dist/public/js/forms.js +610 -28
  92. package/dist/public/js/icons.js +51 -0
  93. package/dist/public/js/inspector.js +638 -0
  94. package/dist/public/js/query.js +93 -3
  95. package/dist/public/js/schema.js +170 -52
  96. package/dist/public/js/seed.js +1090 -0
  97. package/dist/routes/api/helpers.d.ts +42 -0
  98. package/dist/routes/api/helpers.js +133 -0
  99. package/dist/routes/api/helpers.js.map +1 -0
  100. package/dist/routes/api/import-export.d.ts +3 -0
  101. package/dist/routes/api/import-export.js +85 -0
  102. package/dist/routes/api/import-export.js.map +1 -0
  103. package/dist/routes/api/index.d.ts +4 -0
  104. package/dist/routes/api/index.js +37 -0
  105. package/dist/routes/api/index.js.map +1 -0
  106. package/dist/routes/api/query.d.ts +3 -0
  107. package/dist/routes/api/query.js +76 -0
  108. package/dist/routes/api/query.js.map +1 -0
  109. package/dist/routes/api/rows.d.ts +3 -0
  110. package/dist/routes/api/rows.js +373 -0
  111. package/dist/routes/api/rows.js.map +1 -0
  112. package/dist/routes/api/seed.d.ts +3 -0
  113. package/dist/routes/api/seed.js +82 -0
  114. package/dist/routes/api/seed.js.map +1 -0
  115. package/dist/routes/api/tables.d.ts +3 -0
  116. package/dist/routes/api/tables.js +160 -0
  117. package/dist/routes/api/tables.js.map +1 -0
  118. package/dist/routes/databases.d.ts +8 -11
  119. package/dist/routes/databases.js +100 -66
  120. package/dist/routes/databases.js.map +1 -1
  121. package/dist/routes/index.d.ts +3 -0
  122. package/dist/routes/index.js +20 -0
  123. package/dist/routes/index.js.map +1 -0
  124. package/dist/routes/pages.d.ts +3 -3
  125. package/dist/routes/pages.js +163 -54
  126. package/dist/routes/pages.js.map +1 -1
  127. package/dist/serverless.d.ts +40 -0
  128. package/dist/serverless.js +210 -0
  129. package/dist/serverless.js.map +1 -0
  130. package/dist/sql/generator.d.ts +4 -3
  131. package/dist/sql/generator.js +103 -6
  132. package/dist/sql/generator.js.map +1 -1
  133. package/dist/sql/index.d.ts +2 -0
  134. package/dist/sql/index.js +19 -0
  135. package/dist/sql/index.js.map +1 -0
  136. package/dist/types/api.d.ts +412 -0
  137. package/dist/types/api.js +9 -0
  138. package/dist/types/api.js.map +1 -0
  139. package/dist/utils/colors.d.ts +40 -0
  140. package/dist/utils/colors.js +64 -0
  141. package/dist/utils/colors.js.map +1 -0
  142. package/dist/{util.d.ts → utils/common.d.ts} +10 -6
  143. package/dist/{util.js → utils/common.js} +80 -13
  144. package/dist/utils/common.js.map +1 -0
  145. package/dist/utils/csv.js.map +1 -0
  146. package/dist/utils/datatype.d.ts +49 -0
  147. package/dist/utils/datatype.js +222 -0
  148. package/dist/utils/datatype.js.map +1 -0
  149. package/dist/utils/icons.d.ts +11 -0
  150. package/dist/utils/icons.js +72 -0
  151. package/dist/utils/icons.js.map +1 -0
  152. package/dist/utils/index.d.ts +4 -0
  153. package/dist/utils/index.js +21 -0
  154. package/dist/utils/index.js.map +1 -0
  155. package/dist/{logger.d.ts → utils/logger.d.ts} +1 -1
  156. package/dist/utils/logger.js +40 -0
  157. package/dist/utils/logger.js.map +1 -0
  158. package/dist/views/layouts/main.hbs +33 -11
  159. package/dist/views/pages/databases.hbs +182 -74
  160. package/dist/views/pages/designer.hbs +9 -6
  161. package/dist/views/pages/error.hbs +3 -3
  162. package/dist/views/pages/form.hbs +5 -3
  163. package/dist/views/pages/home.hbs +22 -13
  164. package/dist/views/pages/login.hbs +111 -0
  165. package/dist/views/pages/query.hbs +10 -8
  166. package/dist/views/pages/schema.hbs +41 -13
  167. package/dist/views/pages/seed.hbs +232 -0
  168. package/dist/views/pages/table.hbs +155 -70
  169. package/dist/views/partials/icon.hbs +2 -0
  170. package/dist/views/partials/navbar.hbs +70 -31
  171. package/dist/views/partials/sidebar.hbs +108 -99
  172. package/package.json +12 -5
  173. package/dist/args.d.ts +0 -28
  174. package/dist/args.js +0 -187
  175. package/dist/args.js.map +0 -1
  176. package/dist/config.d.ts +0 -17
  177. package/dist/config.js +0 -23
  178. package/dist/config.js.map +0 -1
  179. package/dist/csv.js.map +0 -1
  180. package/dist/logger.js +0 -25
  181. package/dist/logger.js.map +0 -1
  182. package/dist/routes/api.d.ts +0 -9
  183. package/dist/routes/api.js +0 -786
  184. package/dist/routes/api.js.map +0 -1
  185. package/dist/test/classifier.test.d.ts +0 -1
  186. package/dist/test/classifier.test.js +0 -44
  187. package/dist/test/classifier.test.js.map +0 -1
  188. package/dist/test/csv.test.d.ts +0 -1
  189. package/dist/test/csv.test.js +0 -64
  190. package/dist/test/csv.test.js.map +0 -1
  191. package/dist/test/database.test.d.ts +0 -1
  192. package/dist/test/database.test.js +0 -402
  193. package/dist/test/database.test.js.map +0 -1
  194. package/dist/test/generator.test.d.ts +0 -1
  195. package/dist/test/generator.test.js +0 -132
  196. package/dist/test/generator.test.js.map +0 -1
  197. package/dist/test/manager.test.d.ts +0 -1
  198. package/dist/test/manager.test.js +0 -146
  199. package/dist/test/manager.test.js.map +0 -1
  200. package/dist/util.js.map +0 -1
  201. /package/dist/{csv.d.ts → utils/csv.d.ts} +0 -0
  202. /package/dist/{csv.js → utils/csv.js} +0 -0
@@ -80,6 +80,17 @@
80
80
  (palette[type] || palette.info) + '"><span>' + escapeHtml(text) + '</span></div>';
81
81
  }
82
82
 
83
+ function isJsonStr(s) {
84
+ if (typeof s !== 'string') return false;
85
+ const t = s.trim();
86
+ if (!((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']')))) return false;
87
+ try { JSON.parse(t); return true; } catch { return false; }
88
+ }
89
+
90
+ function isHexBlobStr(s) {
91
+ return typeof s === 'string' && /^0x[0-9a-f]{4,}$/i.test(s.trim());
92
+ }
93
+
83
94
  function renderTable(columns, rows) {
84
95
  headEl.innerHTML = '<tr>' + columns.map((c) => '<th class="whitespace-nowrap font-semibold"><span class="font-mono normal-case">' + escapeHtml(c) + '</span></th>').join('') + '</tr>';
85
96
  if (!rows.length) {
@@ -93,12 +104,64 @@
93
104
  return '<td class="align-middle"><span class="chip chip-null">NULL</span></td>';
94
105
  }
95
106
  const s = String(v);
96
- const short = s.length > 200 ? s.slice(0, 200) + '…' : s;
97
- return '<td class="max-w-xs truncate align-middle font-mono text-[13px]" title="' + escapeHtml(s) + '">' + escapeHtml(short) + '</td>';
107
+ const json = isJsonStr(s);
108
+ const blob = isHexBlobStr(s);
109
+ const short = s.length > 140 ? s.slice(0, 140) + '…' : s;
110
+
111
+ if (json) {
112
+ return '<td class="max-w-xs align-middle js-query-cell" data-col="' + escapeHtml(c) + '" data-value="' + escapeHtml(s) + '" data-json="true">' +
113
+ '<div class="flex items-center gap-1.5">' +
114
+ '<span class="inline-flex items-center gap-1 rounded-md bg-info/10 px-1.5 py-0.5 text-[11px] font-mono font-medium text-info">{ } JSON</span>' +
115
+ '<span class="block max-w-[120px] truncate font-mono text-[12px] text-base-content/70">' + escapeHtml(short) + '</span>' +
116
+ '<button type="button" class="btn btn-ghost btn-xs btn-circle js-query-inspect" title="Inspect JSON"><svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg></button>' +
117
+ '</div></td>';
118
+ }
119
+
120
+ if (blob) {
121
+ return '<td class="max-w-xs align-middle js-query-cell" data-col="' + escapeHtml(c) + '" data-value="' + escapeHtml(s) + '" data-blob="true">' +
122
+ '<div class="flex items-center gap-1.5">' +
123
+ '<span class="inline-flex items-center gap-1 rounded-md bg-secondary/10 px-1.5 py-0.5 text-[11px] font-mono font-medium text-secondary">🗃️ BLOB</span>' +
124
+ '<span class="block max-w-[120px] truncate font-mono text-[12px] text-base-content/70">' + escapeHtml(short) + '</span>' +
125
+ '<button type="button" class="btn btn-ghost btn-xs btn-circle js-query-inspect" title="Inspect BLOB"><svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg></button>' +
126
+ '</div></td>';
127
+ }
128
+
129
+ return '<td class="max-w-xs align-middle js-query-cell" data-col="' + escapeHtml(c) + '" data-value="' + escapeHtml(s) + '">' +
130
+ '<div class="flex items-center justify-between gap-1 group/cell">' +
131
+ '<span class="truncate font-mono text-[13px]" title="' + escapeHtml(s) + '">' + escapeHtml(short) + '</span>' +
132
+ (s.length > 25 ? '<button type="button" class="btn btn-ghost btn-xs btn-circle opacity-0 group-hover/cell:opacity-100 transition-opacity js-query-inspect shrink-0" title="Inspect text"><svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg></button>' : '') +
133
+ '</div></td>';
98
134
  }).join('') + '</tr>',
99
135
  ).join('');
100
136
  }
101
137
 
138
+ // Delegate inspection click
139
+ if (bodyEl) {
140
+ bodyEl.addEventListener('click', function (e) {
141
+ const inspectBtn = e.target.closest('.js-query-inspect');
142
+ const cell = e.target.closest('.js-query-cell');
143
+ if (!inspectBtn && (!cell || e.detail < 2)) return;
144
+ const targetCell = inspectBtn ? inspectBtn.closest('.js-query-cell') : cell;
145
+ if (!targetCell) return;
146
+
147
+ const col = targetCell.dataset.col || 'result';
148
+ const val = targetCell.dataset.value || '';
149
+ const isBlob = targetCell.dataset.blob === 'true';
150
+ const isJson = targetCell.dataset.json === 'true';
151
+
152
+ if (window.Inspector && window.Inspector.open) {
153
+ window.Inspector.open({
154
+ col: col,
155
+ value: val,
156
+ isNull: false,
157
+ isBlob: isBlob,
158
+ isJson: isJson,
159
+ readonly: true,
160
+ });
161
+ }
162
+ });
163
+ }
164
+
102
165
  function renderMessageRow(text, kindLabel) {
103
166
  headEl.innerHTML = '';
104
167
  bodyEl.innerHTML = '<tr><td class="px-4 py-8 text-center"><p class="text-2xl font-extrabold text-primary">' +
@@ -109,12 +172,29 @@
109
172
  if (resultSummary) resultSummary.textContent = text;
110
173
  }
111
174
 
175
+ function isDestructiveSql(sql) {
176
+ var cleaned = sql.replace(/--.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '').trim();
177
+ if (/\b(drop\s+(table|view|index|trigger)|truncate\b)/i.test(cleaned)) return true;
178
+ if (/\bdelete\s+from\b/i.test(cleaned) && !/\bwhere\b/i.test(cleaned)) return true;
179
+ return false;
180
+ }
181
+
112
182
  async function run() {
113
183
  const sql = editor.value.trim();
114
184
  if (!sql) {
115
185
  setMessage('Enter a SQL statement first.', 'warning');
116
186
  return;
117
187
  }
188
+ if (isDestructiveSql(sql)) {
189
+ const confirmed = await UI.confirm({
190
+ title: 'Execute Destructive SQL',
191
+ message: 'This SQL statement contains potentially destructive operations (e.g. DROP, TRUNCATE, or unrestricted DELETE) that may permanently erase database schemas or data.',
192
+ detailsHtml: '<pre class="font-mono text-xs text-base-content/85 whitespace-pre-wrap break-all">' + escapeHtml(sql.length > 350 ? sql.slice(0, 350) + '…' : sql) + '</pre>',
193
+ confirmText: 'Execute anyway',
194
+ danger: true,
195
+ });
196
+ if (!confirmed) return;
197
+ }
118
198
  messageEl.innerHTML = '';
119
199
  resultEl.classList.add('hidden');
120
200
  setExportEnabled(false);
@@ -202,7 +282,17 @@
202
282
  async function deleteQuery() {
203
283
  const id = selectEl.value;
204
284
  if (!id) return;
205
- if (!window.confirm('Delete this saved query?')) return;
285
+ const q = queriesCache.find((x) => String(x.id) === String(id));
286
+ const queryName = q ? q.name : 'Saved query';
287
+ const confirmed = await UI.confirm({
288
+ title: 'Delete Saved Query',
289
+ message: 'Are you sure you want to delete the saved query "' + queryName + '"?',
290
+ item: queryName,
291
+ itemType: 'Saved Query',
292
+ confirmText: 'Delete query',
293
+ danger: true,
294
+ });
295
+ if (!confirmed) return;
206
296
  try {
207
297
  await Api.del('/api/queries/' + id);
208
298
  UI.showToast('Query deleted.', 'success');
@@ -4,7 +4,32 @@
4
4
  const cfgEl = document.getElementById('schema-config');
5
5
  if (!cfgEl) return;
6
6
  const cfg = JSON.parse(cfgEl.textContent);
7
- const DESIGNER_TYPES = ['TEXT', 'INTEGER', 'REAL', 'BLOB', 'BOOLEAN', 'DATE', 'DATETIME'];
7
+ const DESIGNER_TYPES = [
8
+ 'TEXT',
9
+ 'VARCHAR(255)',
10
+ 'INTEGER',
11
+ 'BIGINT',
12
+ 'SMALLINT',
13
+ 'SERIAL',
14
+ 'BIGSERIAL',
15
+ 'REAL',
16
+ 'DECIMAL(10,2)',
17
+ 'NUMERIC',
18
+ 'BOOLEAN',
19
+ 'DATE',
20
+ 'DATETIME',
21
+ 'TIMESTAMP',
22
+ 'TIMESTAMPTZ',
23
+ 'TIME',
24
+ 'INTERVAL',
25
+ 'JSON',
26
+ 'JSONB',
27
+ 'UUID',
28
+ 'BYTEA',
29
+ 'BLOB',
30
+ 'INET',
31
+ ];
32
+
8
33
  const NAME_RE = /^[A-Za-z_][A-Za-z0-9_$]*$/;
9
34
  const t = encodeURIComponent(cfg.table);
10
35
 
@@ -58,15 +83,26 @@
58
83
  }
59
84
  }
60
85
 
61
- function buildAddForm() {
86
+ function renderTypeOptions(selectedType) {
87
+ const upper = (selectedType || 'TEXT').toUpperCase();
88
+ const all = [...DESIGNER_TYPES];
89
+ if (selectedType && !all.map((x) => x.toUpperCase()).includes(upper)) {
90
+ all.unshift(selectedType);
91
+ }
92
+ return all.map((x) => '<option value="' + escapeHtml(x) + '"' + (x.toUpperCase() === upper ? ' selected' : '') + '>' + escapeHtml(x) + '</option>').join('');
93
+ }
94
+
95
+ async function buildAddForm() {
96
+ if (!fkTables.length) await loadFkTables();
62
97
  formEl.innerHTML = '';
63
98
  formEl.innerHTML = [
64
99
  '<div class="col-span-2 sm:col-span-3"><label class="field-label">Name</label><input class="field-input ac-name" placeholder="column_name" autocomplete="off" spellcheck="false"></div>',
65
- '<div class="col-span-1 sm:col-span-2"><label class="field-label">Type</label><select class="field-select ac-type">' + DESIGNER_TYPES.map((x) => '<option>' + x + '</option>').join('') + '</select></div>',
100
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">Type</label><select class="field-select ac-type">' + renderTypeOptions('TEXT') + '</select></div>',
66
101
  '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK table</label><select class="field-select ac-fk-table"><option value="">— none —</option>' + fkTables.map((x) => '<option>' + escapeHtml(x) + '</option>').join('') + '</select></div>',
67
102
  '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK column</label><select class="field-select ac-fk-col"><option value="">—</option></select></div>',
68
103
  '<div class="col-span-1 sm:col-span-2"><label class="field-label">Default</label><input class="field-input ac-default" placeholder="0, CURRENT_TIMESTAMP" autocomplete="off" spellcheck="false"></div>',
69
- '<div class="col-span-2 flex items-end gap-1 pb-0.5 sm:col-span-1">',
104
+ '<div class="col-span-2 flex items-end gap-1.5 pb-0.5 sm:col-span-1">',
105
+ '<label class="flex cursor-pointer flex-col items-center gap-1" title="Primary key"><input type="checkbox" class="checkbox checkbox-xs ac-pk"><span class="text-[10px] font-semibold uppercase text-base-content/50">PK</span></label>',
70
106
  '<label class="flex cursor-pointer flex-col items-center gap-1" title="Not null"><input type="checkbox" class="checkbox checkbox-xs ac-notnull"><span class="text-[10px] font-semibold uppercase text-base-content/50">NN</span></label>',
71
107
  '<label class="flex cursor-pointer flex-col items-center gap-1" title="Unique"><input type="checkbox" class="checkbox checkbox-xs ac-unique"><span class="text-[10px] font-semibold uppercase text-base-content/50">UQ</span></label>',
72
108
  '</div>',
@@ -95,6 +131,7 @@
95
131
  const column = {
96
132
  name,
97
133
  type: formEl.querySelector('.ac-type').value,
134
+ primaryKey: formEl.querySelector('.ac-pk').checked,
98
135
  notNull: formEl.querySelector('.ac-notnull').checked,
99
136
  unique: formEl.querySelector('.ac-unique').checked,
100
137
  defaultValue: formEl.querySelector('.ac-default').value.trim() || null,
@@ -110,73 +147,137 @@
110
147
  }
111
148
 
112
149
  if (addBtn) {
113
- addBtn.addEventListener('click', () => {
150
+ addBtn.addEventListener('click', async () => {
151
+ if (editPanel) editPanel.classList.add('hidden');
114
152
  panel.classList.toggle('hidden');
115
153
  if (!panel.classList.contains('hidden')) {
116
154
  errEl.classList.add('hidden');
117
- buildAddForm();
155
+ await buildAddForm();
118
156
  }
119
157
  });
120
158
  }
121
159
  if (cancelBtn) cancelBtn.addEventListener('click', () => { panel.classList.add('hidden'); formEl.innerHTML = ''; });
122
160
  if (submitBtn) submitBtn.addEventListener('click', addColumn);
123
161
 
124
- // ---- Column rename (inline) ---------------------------------------------
125
-
126
- function inlineRename(cell, oldName) {
127
- const display = cell.querySelector('.name-display');
128
- if (!display) return;
129
- const input = document.createElement('input');
130
- input.type = 'text';
131
- input.value = oldName;
132
- input.className = 'field-input !py-1 !text-[13px]';
133
- input.spellcheck = false;
134
- let done = false;
135
- const restore = () => { if (display.parentNode) cell.replaceChild(display, input); };
136
- const finish = async (save) => {
137
- if (done) return;
138
- done = true;
139
- if (!save) return restore();
140
- const newName = input.value.trim();
141
- if (!newName || newName === oldName) return restore();
162
+ // ---- Edit column (type & constraints) ------------------------------------
163
+
164
+ const editPanel = document.getElementById('edit-column-panel');
165
+ const editFormEl = document.getElementById('edit-column-form');
166
+ const editErrEl = document.getElementById('edit-column-error');
167
+ const editSubmitBtn = document.getElementById('edit-column-submit');
168
+ const editCancelBtn = document.getElementById('edit-column-cancel');
169
+ const editTargetSpan = document.getElementById('edit-column-target');
170
+ let currentEditingCol = null;
171
+
172
+ async function openEditColumn(colData) {
173
+ if (!editPanel || !editFormEl) return;
174
+ if (panel) panel.classList.add('hidden');
175
+ if (!fkTables.length) await loadFkTables();
176
+ currentEditingCol = colData.name;
177
+ if (editTargetSpan) editTargetSpan.textContent = '"' + colData.name + '"';
178
+ editErrEl.classList.add('hidden');
179
+
180
+ editFormEl.innerHTML = [
181
+ '<div class="col-span-2 sm:col-span-3"><label class="field-label">Name</label><input class="field-input ec-name" value="' + escapeHtml(colData.name) + '" autocomplete="off" spellcheck="false"></div>',
182
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">Type</label><select class="field-select ec-type">' + renderTypeOptions(colData.type) + '</select></div>',
183
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK table</label><select class="field-select ec-fk-table"><option value="">— none —</option>' + fkTables.map((x) => '<option value="' + escapeHtml(x) + '"' + (colData.fkTable === x ? ' selected' : '') + '>' + escapeHtml(x) + '</option>').join('') + '</select></div>',
184
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK column</label><select class="field-select ec-fk-col"><option value="">—</option></select></div>',
185
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">Default</label><input class="field-input ec-default" value="' + escapeHtml(colData.defaultValue || '') + '" placeholder="0, CURRENT_TIMESTAMP" autocomplete="off" spellcheck="false"></div>',
186
+ '<div class="col-span-2 flex items-end gap-1.5 pb-0.5 sm:col-span-1">',
187
+ '<label class="flex cursor-pointer flex-col items-center gap-1" title="Primary key"><input type="checkbox" class="checkbox checkbox-xs ec-pk"' + (colData.primaryKey ? ' checked' : '') + '><span class="text-[10px] font-semibold uppercase text-base-content/50">PK</span></label>',
188
+ '<label class="flex cursor-pointer flex-col items-center gap-1" title="Not null"><input type="checkbox" class="checkbox checkbox-xs ec-notnull"' + (colData.notNull ? ' checked' : '') + '><span class="text-[10px] font-semibold uppercase text-base-content/50">NN</span></label>',
189
+ '<label class="flex cursor-pointer flex-col items-center gap-1" title="Unique"><input type="checkbox" class="checkbox checkbox-xs ec-unique"' + (colData.unique ? ' checked' : '') + '><span class="text-[10px] font-semibold uppercase text-base-content/50">UQ</span></label>',
190
+ '</div>',
191
+ ].join('');
192
+
193
+ const fkTableSel = editFormEl.querySelector('.ec-fk-table');
194
+ const fkColSel = editFormEl.querySelector('.ec-fk-col');
195
+
196
+ async function updateFkCols(table, selectedCol) {
197
+ fkColSel.innerHTML = '<option value="">—</option>';
198
+ if (!table) return;
142
199
  try {
143
- await Api.put('/api/tables/' + t + '/columns/' + encodeURIComponent(oldName), { name: newName });
144
- UI.showToast('Column renamed.', 'success');
145
- window.location.reload();
146
- } catch (e) {
147
- UI.showError(e.message);
148
- done = false;
149
- restore();
150
- }
200
+ const targetInfo = await Api.get('/api/tables/' + encodeURIComponent(table) + '/info');
201
+ fkColSel.innerHTML = '<option value="">—</option>' + targetInfo.columns.map((c) => '<option value="' + escapeHtml(c.name) + '"' + (c.name === selectedCol ? ' selected' : '') + '>' + escapeHtml(c.name) + '</option>').join('');
202
+ } catch (e) { /* ignore */ }
203
+ }
204
+
205
+ if (colData.fkTable) {
206
+ await updateFkCols(colData.fkTable, colData.fkTo);
207
+ }
208
+
209
+ fkTableSel.addEventListener('change', () => updateFkCols(fkTableSel.value, ''));
210
+
211
+ editPanel.classList.remove('hidden');
212
+ editPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
213
+ }
214
+
215
+ async function saveEditedColumn() {
216
+ if (!currentEditingCol) return;
217
+ editErrEl.classList.add('hidden');
218
+ const name = (editFormEl.querySelector('.ec-name').value || '').trim();
219
+ if (!name) return showError(editErrEl, 'Column name is required.');
220
+ if (!NAME_RE.test(name)) return showError(editErrEl, 'Invalid column name: "' + name + '". Use letters, digits and underscores.');
221
+ const fkTable = editFormEl.querySelector('.ec-fk-table').value;
222
+ const fkCol = editFormEl.querySelector('.ec-fk-col').value;
223
+ const column = {
224
+ name,
225
+ type: editFormEl.querySelector('.ec-type').value,
226
+ primaryKey: editFormEl.querySelector('.ec-pk').checked,
227
+ notNull: editFormEl.querySelector('.ec-notnull').checked,
228
+ unique: editFormEl.querySelector('.ec-unique').checked,
229
+ defaultValue: editFormEl.querySelector('.ec-default').value.trim() || null,
230
+ foreignKey: fkTable && fkCol ? { table: fkTable, column: fkCol } : null,
151
231
  };
152
- input.addEventListener('keydown', (e) => {
153
- if (e.key === 'Enter') { e.preventDefault(); finish(true); }
154
- else if (e.key === 'Escape') { e.preventDefault(); finish(false); }
155
- });
156
- input.addEventListener('blur', () => finish(false));
157
- cell.replaceChild(input, display);
158
- input.focus();
159
- input.select();
232
+
233
+ if (editSubmitBtn) editSubmitBtn.disabled = true;
234
+ try {
235
+ const data = await Api.put('/api/tables/' + t + '/columns/' + encodeURIComponent(currentEditingCol), { column });
236
+ UI.showToast(data.message || 'Column updated.', 'success');
237
+ window.location.reload();
238
+ } catch (e) {
239
+ showError(editErrEl, e.message);
240
+ } finally {
241
+ if (editSubmitBtn) editSubmitBtn.disabled = false;
242
+ }
160
243
  }
161
244
 
162
- document.querySelectorAll('.col-rename').forEach((btn) => {
245
+ if (editCancelBtn) editCancelBtn.addEventListener('click', () => { editPanel.classList.add('hidden'); editFormEl.innerHTML = ''; currentEditingCol = null; });
246
+ if (editSubmitBtn) editSubmitBtn.addEventListener('click', saveEditedColumn);
247
+
248
+ document.querySelectorAll('.col-edit').forEach((btn) => {
163
249
  btn.addEventListener('click', () => {
164
- const tr = btn.closest('tr');
165
- if (!tr) return;
166
- const cell = tr.querySelector('.col-name-cell');
167
- if (cell) inlineRename(cell, cell.dataset.column);
250
+ openEditColumn({
251
+ name: btn.dataset.column || '',
252
+ type: btn.dataset.type || 'TEXT',
253
+ primaryKey: btn.dataset.pk === 'true',
254
+ notNull: btn.dataset.notnull === 'true',
255
+ unique: btn.dataset.unique === 'true',
256
+ defaultValue: btn.dataset.default || '',
257
+ fkTable: btn.dataset.fkTable || '',
258
+ fkTo: btn.dataset.fkTo || '',
259
+ });
168
260
  });
169
261
  });
170
262
 
171
263
  // ---- Drop column ---------------------------------------------------------
172
264
 
173
265
  document.querySelectorAll('.col-drop').forEach((btn) => {
174
- btn.addEventListener('click', () => {
266
+ btn.addEventListener('click', async () => {
175
267
  const tr = btn.closest('tr');
176
268
  if (!tr) return;
177
269
  const cell = tr.querySelector('.col-name-cell');
178
- const name = cell.dataset.column;
179
- if (!window.confirm('Drop column "' + name + '" permanently? This cannot be undone.')) return;
270
+ const name = cell ? cell.dataset.column : '';
271
+ if (!name) return;
272
+ const confirmed = await UI.confirm({
273
+ title: 'Drop Column',
274
+ message: 'Dropping column "' + name + '" will permanently delete all data stored in this column for all rows in table "' + cfg.table + '". This action cannot be undone.',
275
+ item: name,
276
+ itemType: 'Column',
277
+ confirmText: 'Drop column',
278
+ danger: true,
279
+ });
280
+ if (!confirmed) return;
180
281
  Api.del('/api/tables/' + t + '/columns/' + encodeURIComponent(name))
181
282
  .then((data) => {
182
283
  UI.showToast(data.message || 'Column dropped.', 'success');
@@ -190,8 +291,17 @@
190
291
 
191
292
  const dropBtn = document.getElementById('drop-table');
192
293
  if (dropBtn) {
193
- dropBtn.addEventListener('click', () => {
194
- if (!window.confirm('Drop table "' + cfg.table + '" permanently? All of its data will be lost.')) return;
294
+ dropBtn.addEventListener('click', async () => {
295
+ const confirmed = await UI.confirm({
296
+ title: 'Drop Table',
297
+ message: 'Dropping table "' + cfg.table + '" permanently erases the table schema, all indexes, and all stored records. This action cannot be undone.',
298
+ item: cfg.table,
299
+ itemType: 'Table',
300
+ requireInputMatch: cfg.table,
301
+ confirmText: 'Drop table',
302
+ danger: true,
303
+ });
304
+ if (!confirmed) return;
195
305
  Api.del('/api/tables/' + t)
196
306
  .then(() => {
197
307
  UI.showToast('Table dropped.', 'success');
@@ -297,10 +407,18 @@
297
407
  }
298
408
 
299
409
  document.querySelectorAll('.index-drop').forEach((btn) => {
300
- btn.addEventListener('click', () => {
410
+ btn.addEventListener('click', async () => {
301
411
  const name = btn.dataset.index;
302
412
  if (!name) return;
303
- if (!window.confirm('Drop index "' + name + '" permanently?')) return;
413
+ const confirmed = await UI.confirm({
414
+ title: 'Drop Index',
415
+ message: 'Are you sure you want to permanently drop index "' + name + '" on table "' + cfg.table + '"?',
416
+ item: name,
417
+ itemType: 'Index',
418
+ confirmText: 'Drop index',
419
+ danger: true,
420
+ });
421
+ if (!confirmed) return;
304
422
  Api.del('/api/tables/' + t + '/indexes/' + encodeURIComponent(name))
305
423
  .then((data) => {
306
424
  UI.showToast(data.message || 'Index dropped.', 'success');