admindb 1.2.1 → 2.1.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 (208) hide show
  1. package/README.md +262 -329
  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 -146
  56. package/dist/db/database.js +208 -461
  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 +74 -11
  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 +55 -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 +185 -52
  96. package/dist/public/js/seed.js +865 -152
  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 +157 -64
  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 +7 -3
  131. package/dist/sql/generator.js +144 -7
  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 +40 -11
  159. package/dist/views/pages/databases.hbs +182 -74
  160. package/dist/views/pages/designer.hbs +29 -11
  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 +8 -6
  166. package/dist/views/pages/schema.hbs +41 -13
  167. package/dist/views/pages/seed.hbs +200 -54
  168. package/dist/views/pages/table.hbs +152 -72
  169. package/dist/views/partials/icon.hbs +2 -0
  170. package/dist/views/partials/navbar.hbs +69 -25
  171. package/dist/views/partials/sidebar.hbs +108 -94
  172. package/docs/API.md +144 -0
  173. package/docs/EXAMPLES.md +576 -0
  174. package/docs/SECURITY.md +265 -0
  175. package/package.json +15 -4
  176. package/dist/args.d.ts +0 -28
  177. package/dist/args.js +0 -187
  178. package/dist/args.js.map +0 -1
  179. package/dist/config.d.ts +0 -17
  180. package/dist/config.js +0 -23
  181. package/dist/config.js.map +0 -1
  182. package/dist/csv.js.map +0 -1
  183. package/dist/data/generator.d.ts +0 -86
  184. package/dist/data/generator.js +0 -1031
  185. package/dist/data/generator.js.map +0 -1
  186. package/dist/logger.js +0 -25
  187. package/dist/logger.js.map +0 -1
  188. package/dist/routes/api.d.ts +0 -9
  189. package/dist/routes/api.js +0 -848
  190. package/dist/routes/api.js.map +0 -1
  191. package/dist/test/classifier.test.d.ts +0 -1
  192. package/dist/test/classifier.test.js +0 -44
  193. package/dist/test/classifier.test.js.map +0 -1
  194. package/dist/test/csv.test.d.ts +0 -1
  195. package/dist/test/csv.test.js +0 -64
  196. package/dist/test/csv.test.js.map +0 -1
  197. package/dist/test/database.test.d.ts +0 -1
  198. package/dist/test/database.test.js +0 -402
  199. package/dist/test/database.test.js.map +0 -1
  200. package/dist/test/generator.test.d.ts +0 -1
  201. package/dist/test/generator.test.js +0 -132
  202. package/dist/test/generator.test.js.map +0 -1
  203. package/dist/test/manager.test.d.ts +0 -1
  204. package/dist/test/manager.test.js +0 -146
  205. package/dist/test/manager.test.js.map +0 -1
  206. package/dist/util.js.map +0 -1
  207. /package/dist/{csv.d.ts → utils/csv.d.ts} +0 -0
  208. /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,47 @@
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 SQLITE_TYPES = [
8
+ 'TEXT',
9
+ 'INTEGER',
10
+ 'REAL',
11
+ 'BLOB',
12
+ 'BOOLEAN',
13
+ 'DATE',
14
+ 'DATETIME',
15
+ 'NUMERIC',
16
+ 'VARCHAR(255)',
17
+ 'JSON',
18
+ ];
19
+
20
+ const POSTGRES_TYPES = [
21
+ 'TEXT',
22
+ 'VARCHAR(255)',
23
+ 'INTEGER',
24
+ 'BIGINT',
25
+ 'SMALLINT',
26
+ 'SERIAL',
27
+ 'BIGSERIAL',
28
+ 'REAL',
29
+ 'DECIMAL(10,2)',
30
+ 'NUMERIC',
31
+ 'BOOLEAN',
32
+ 'DATE',
33
+ 'DATETIME',
34
+ 'TIMESTAMP',
35
+ 'TIMESTAMPTZ',
36
+ 'TIME',
37
+ 'INTERVAL',
38
+ 'JSON',
39
+ 'JSONB',
40
+ 'UUID',
41
+ 'BYTEA',
42
+ 'INET',
43
+ ];
44
+
45
+ const isPostgres = (window.APP && window.APP.dialect === 'postgres') || (cfg && cfg.dialect === 'postgres');
46
+ const DESIGNER_TYPES = isPostgres ? POSTGRES_TYPES : SQLITE_TYPES;
47
+
8
48
  const NAME_RE = /^[A-Za-z_][A-Za-z0-9_$]*$/;
9
49
  const t = encodeURIComponent(cfg.table);
10
50
 
@@ -58,15 +98,26 @@
58
98
  }
59
99
  }
60
100
 
61
- function buildAddForm() {
101
+ function renderTypeOptions(selectedType) {
102
+ const upper = (selectedType || 'TEXT').toUpperCase();
103
+ const all = [...DESIGNER_TYPES];
104
+ if (selectedType && !all.map((x) => x.toUpperCase()).includes(upper)) {
105
+ all.unshift(selectedType);
106
+ }
107
+ return all.map((x) => '<option value="' + escapeHtml(x) + '"' + (x.toUpperCase() === upper ? ' selected' : '') + '>' + escapeHtml(x) + '</option>').join('');
108
+ }
109
+
110
+ async function buildAddForm() {
111
+ if (!fkTables.length) await loadFkTables();
62
112
  formEl.innerHTML = '';
63
113
  formEl.innerHTML = [
64
114
  '<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>',
115
+ '<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
116
  '<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
117
  '<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
118
  '<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">',
119
+ '<div class="col-span-2 flex items-end gap-1.5 pb-0.5 sm:col-span-1">',
120
+ '<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
121
  '<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
122
  '<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
123
  '</div>',
@@ -95,6 +146,7 @@
95
146
  const column = {
96
147
  name,
97
148
  type: formEl.querySelector('.ac-type').value,
149
+ primaryKey: formEl.querySelector('.ac-pk').checked,
98
150
  notNull: formEl.querySelector('.ac-notnull').checked,
99
151
  unique: formEl.querySelector('.ac-unique').checked,
100
152
  defaultValue: formEl.querySelector('.ac-default').value.trim() || null,
@@ -110,73 +162,137 @@
110
162
  }
111
163
 
112
164
  if (addBtn) {
113
- addBtn.addEventListener('click', () => {
165
+ addBtn.addEventListener('click', async () => {
166
+ if (editPanel) editPanel.classList.add('hidden');
114
167
  panel.classList.toggle('hidden');
115
168
  if (!panel.classList.contains('hidden')) {
116
169
  errEl.classList.add('hidden');
117
- buildAddForm();
170
+ await buildAddForm();
118
171
  }
119
172
  });
120
173
  }
121
174
  if (cancelBtn) cancelBtn.addEventListener('click', () => { panel.classList.add('hidden'); formEl.innerHTML = ''; });
122
175
  if (submitBtn) submitBtn.addEventListener('click', addColumn);
123
176
 
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();
177
+ // ---- Edit column (type & constraints) ------------------------------------
178
+
179
+ const editPanel = document.getElementById('edit-column-panel');
180
+ const editFormEl = document.getElementById('edit-column-form');
181
+ const editErrEl = document.getElementById('edit-column-error');
182
+ const editSubmitBtn = document.getElementById('edit-column-submit');
183
+ const editCancelBtn = document.getElementById('edit-column-cancel');
184
+ const editTargetSpan = document.getElementById('edit-column-target');
185
+ let currentEditingCol = null;
186
+
187
+ async function openEditColumn(colData) {
188
+ if (!editPanel || !editFormEl) return;
189
+ if (panel) panel.classList.add('hidden');
190
+ if (!fkTables.length) await loadFkTables();
191
+ currentEditingCol = colData.name;
192
+ if (editTargetSpan) editTargetSpan.textContent = '"' + colData.name + '"';
193
+ editErrEl.classList.add('hidden');
194
+
195
+ editFormEl.innerHTML = [
196
+ '<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>',
197
+ '<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>',
198
+ '<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>',
199
+ '<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>',
200
+ '<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>',
201
+ '<div class="col-span-2 flex items-end gap-1.5 pb-0.5 sm:col-span-1">',
202
+ '<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>',
203
+ '<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>',
204
+ '<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>',
205
+ '</div>',
206
+ ].join('');
207
+
208
+ const fkTableSel = editFormEl.querySelector('.ec-fk-table');
209
+ const fkColSel = editFormEl.querySelector('.ec-fk-col');
210
+
211
+ async function updateFkCols(table, selectedCol) {
212
+ fkColSel.innerHTML = '<option value="">—</option>';
213
+ if (!table) return;
142
214
  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
- }
215
+ const targetInfo = await Api.get('/api/tables/' + encodeURIComponent(table) + '/info');
216
+ fkColSel.innerHTML = '<option value="">—</option>' + targetInfo.columns.map((c) => '<option value="' + escapeHtml(c.name) + '"' + (c.name === selectedCol ? ' selected' : '') + '>' + escapeHtml(c.name) + '</option>').join('');
217
+ } catch (e) { /* ignore */ }
218
+ }
219
+
220
+ if (colData.fkTable) {
221
+ await updateFkCols(colData.fkTable, colData.fkTo);
222
+ }
223
+
224
+ fkTableSel.addEventListener('change', () => updateFkCols(fkTableSel.value, ''));
225
+
226
+ editPanel.classList.remove('hidden');
227
+ editPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
228
+ }
229
+
230
+ async function saveEditedColumn() {
231
+ if (!currentEditingCol) return;
232
+ editErrEl.classList.add('hidden');
233
+ const name = (editFormEl.querySelector('.ec-name').value || '').trim();
234
+ if (!name) return showError(editErrEl, 'Column name is required.');
235
+ if (!NAME_RE.test(name)) return showError(editErrEl, 'Invalid column name: "' + name + '". Use letters, digits and underscores.');
236
+ const fkTable = editFormEl.querySelector('.ec-fk-table').value;
237
+ const fkCol = editFormEl.querySelector('.ec-fk-col').value;
238
+ const column = {
239
+ name,
240
+ type: editFormEl.querySelector('.ec-type').value,
241
+ primaryKey: editFormEl.querySelector('.ec-pk').checked,
242
+ notNull: editFormEl.querySelector('.ec-notnull').checked,
243
+ unique: editFormEl.querySelector('.ec-unique').checked,
244
+ defaultValue: editFormEl.querySelector('.ec-default').value.trim() || null,
245
+ foreignKey: fkTable && fkCol ? { table: fkTable, column: fkCol } : null,
151
246
  };
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();
247
+
248
+ if (editSubmitBtn) editSubmitBtn.disabled = true;
249
+ try {
250
+ const data = await Api.put('/api/tables/' + t + '/columns/' + encodeURIComponent(currentEditingCol), { column });
251
+ UI.showToast(data.message || 'Column updated.', 'success');
252
+ window.location.reload();
253
+ } catch (e) {
254
+ showError(editErrEl, e.message);
255
+ } finally {
256
+ if (editSubmitBtn) editSubmitBtn.disabled = false;
257
+ }
160
258
  }
161
259
 
162
- document.querySelectorAll('.col-rename').forEach((btn) => {
260
+ if (editCancelBtn) editCancelBtn.addEventListener('click', () => { editPanel.classList.add('hidden'); editFormEl.innerHTML = ''; currentEditingCol = null; });
261
+ if (editSubmitBtn) editSubmitBtn.addEventListener('click', saveEditedColumn);
262
+
263
+ document.querySelectorAll('.col-edit').forEach((btn) => {
163
264
  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);
265
+ openEditColumn({
266
+ name: btn.dataset.column || '',
267
+ type: btn.dataset.type || 'TEXT',
268
+ primaryKey: btn.dataset.pk === 'true',
269
+ notNull: btn.dataset.notnull === 'true',
270
+ unique: btn.dataset.unique === 'true',
271
+ defaultValue: btn.dataset.default || '',
272
+ fkTable: btn.dataset.fkTable || '',
273
+ fkTo: btn.dataset.fkTo || '',
274
+ });
168
275
  });
169
276
  });
170
277
 
171
278
  // ---- Drop column ---------------------------------------------------------
172
279
 
173
280
  document.querySelectorAll('.col-drop').forEach((btn) => {
174
- btn.addEventListener('click', () => {
281
+ btn.addEventListener('click', async () => {
175
282
  const tr = btn.closest('tr');
176
283
  if (!tr) return;
177
284
  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;
285
+ const name = cell ? cell.dataset.column : '';
286
+ if (!name) return;
287
+ const confirmed = await UI.confirm({
288
+ title: 'Drop Column',
289
+ 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.',
290
+ item: name,
291
+ itemType: 'Column',
292
+ confirmText: 'Drop column',
293
+ danger: true,
294
+ });
295
+ if (!confirmed) return;
180
296
  Api.del('/api/tables/' + t + '/columns/' + encodeURIComponent(name))
181
297
  .then((data) => {
182
298
  UI.showToast(data.message || 'Column dropped.', 'success');
@@ -190,8 +306,17 @@
190
306
 
191
307
  const dropBtn = document.getElementById('drop-table');
192
308
  if (dropBtn) {
193
- dropBtn.addEventListener('click', () => {
194
- if (!window.confirm('Drop table "' + cfg.table + '" permanently? All of its data will be lost.')) return;
309
+ dropBtn.addEventListener('click', async () => {
310
+ const confirmed = await UI.confirm({
311
+ title: 'Drop Table',
312
+ message: 'Dropping table "' + cfg.table + '" permanently erases the table schema, all indexes, and all stored records. This action cannot be undone.',
313
+ item: cfg.table,
314
+ itemType: 'Table',
315
+ requireInputMatch: cfg.table,
316
+ confirmText: 'Drop table',
317
+ danger: true,
318
+ });
319
+ if (!confirmed) return;
195
320
  Api.del('/api/tables/' + t)
196
321
  .then(() => {
197
322
  UI.showToast('Table dropped.', 'success');
@@ -297,10 +422,18 @@
297
422
  }
298
423
 
299
424
  document.querySelectorAll('.index-drop').forEach((btn) => {
300
- btn.addEventListener('click', () => {
425
+ btn.addEventListener('click', async () => {
301
426
  const name = btn.dataset.index;
302
427
  if (!name) return;
303
- if (!window.confirm('Drop index "' + name + '" permanently?')) return;
428
+ const confirmed = await UI.confirm({
429
+ title: 'Drop Index',
430
+ message: 'Are you sure you want to permanently drop index "' + name + '" on table "' + cfg.table + '"?',
431
+ item: name,
432
+ itemType: 'Index',
433
+ confirmText: 'Drop index',
434
+ danger: true,
435
+ });
436
+ if (!confirmed) return;
304
437
  Api.del('/api/tables/' + t + '/indexes/' + encodeURIComponent(name))
305
438
  .then((data) => {
306
439
  UI.showToast(data.message || 'Index dropped.', 'success');