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.
- package/README.md +250 -352
- package/dist/app.d.ts +25 -4
- package/dist/app.js +79 -15
- package/dist/app.js.map +1 -1
- package/dist/auth/config.d.ts +19 -0
- package/dist/auth/config.js +147 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/crypto.d.ts +27 -0
- package/dist/auth/crypto.js +133 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +22 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +14 -0
- package/dist/auth/middleware.js +100 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/routes.d.ts +7 -0
- package/dist/auth/routes.js +78 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/types.d.ts +35 -0
- package/dist/auth/types.js +11 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli/args.d.ts +45 -0
- package/dist/cli/args.js +328 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/config.d.ts +56 -0
- package/dist/cli/config.js +133 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/runner.d.ts +1 -0
- package/dist/cli/runner.js +222 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli.js +2 -127
- package/dist/cli.js.map +1 -1
- package/dist/data/datasets.d.ts +17 -0
- package/dist/data/datasets.js +133 -0
- package/dist/data/datasets.js.map +1 -0
- package/dist/data/detector.d.ts +30 -0
- package/dist/data/detector.js +472 -0
- package/dist/data/detector.js.map +1 -0
- package/dist/data/engine.d.ts +10 -0
- package/dist/data/engine.js +211 -0
- package/dist/data/engine.js.map +1 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.js +22 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/strategies.d.ts +30 -0
- package/dist/data/strategies.js +343 -0
- package/dist/data/strategies.js.map +1 -0
- package/dist/data/types.d.ts +73 -0
- package/dist/data/types.js +8 -0
- package/dist/data/types.js.map +1 -0
- package/dist/db/database.d.ts +14 -142
- package/dist/db/database.js +209 -435
- package/dist/db/database.js.map +1 -1
- package/dist/db/export.d.ts +2 -2
- package/dist/db/export.js +7 -2
- package/dist/db/export.js.map +1 -1
- package/dist/db/filters.d.ts +22 -0
- package/dist/db/filters.js +158 -0
- package/dist/db/filters.js.map +1 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +25 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/introspection.d.ts +6 -0
- package/dist/db/introspection.js +172 -0
- package/dist/db/introspection.js.map +1 -0
- package/dist/db/manager.d.ts +32 -19
- package/dist/db/manager.js +203 -44
- package/dist/db/manager.js.map +1 -1
- package/dist/db/migrations.d.ts +5 -0
- package/dist/db/migrations.js +110 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/postgres.d.ts +85 -0
- package/dist/db/postgres.js +745 -0
- package/dist/db/postgres.js.map +1 -0
- package/dist/db/types.d.ts +206 -0
- package/dist/db/types.js +7 -0
- package/dist/db/types.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +46 -4
- package/dist/index.js.map +1 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/css/input.css +83 -13
- package/dist/public/js/app.js +142 -0
- package/dist/public/js/browse.js +591 -104
- package/dist/public/js/databases.js +105 -13
- package/dist/public/js/designer.js +40 -2
- package/dist/public/js/forms.js +610 -28
- package/dist/public/js/icons.js +51 -0
- package/dist/public/js/inspector.js +638 -0
- package/dist/public/js/query.js +93 -3
- package/dist/public/js/schema.js +170 -52
- package/dist/public/js/seed.js +1090 -0
- package/dist/routes/api/helpers.d.ts +42 -0
- package/dist/routes/api/helpers.js +133 -0
- package/dist/routes/api/helpers.js.map +1 -0
- package/dist/routes/api/import-export.d.ts +3 -0
- package/dist/routes/api/import-export.js +85 -0
- package/dist/routes/api/import-export.js.map +1 -0
- package/dist/routes/api/index.d.ts +4 -0
- package/dist/routes/api/index.js +37 -0
- package/dist/routes/api/index.js.map +1 -0
- package/dist/routes/api/query.d.ts +3 -0
- package/dist/routes/api/query.js +76 -0
- package/dist/routes/api/query.js.map +1 -0
- package/dist/routes/api/rows.d.ts +3 -0
- package/dist/routes/api/rows.js +373 -0
- package/dist/routes/api/rows.js.map +1 -0
- package/dist/routes/api/seed.d.ts +3 -0
- package/dist/routes/api/seed.js +82 -0
- package/dist/routes/api/seed.js.map +1 -0
- package/dist/routes/api/tables.d.ts +3 -0
- package/dist/routes/api/tables.js +160 -0
- package/dist/routes/api/tables.js.map +1 -0
- package/dist/routes/databases.d.ts +8 -11
- package/dist/routes/databases.js +100 -66
- package/dist/routes/databases.js.map +1 -1
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.js +20 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/pages.d.ts +3 -3
- package/dist/routes/pages.js +163 -54
- package/dist/routes/pages.js.map +1 -1
- package/dist/serverless.d.ts +40 -0
- package/dist/serverless.js +210 -0
- package/dist/serverless.js.map +1 -0
- package/dist/sql/generator.d.ts +4 -3
- package/dist/sql/generator.js +103 -6
- package/dist/sql/generator.js.map +1 -1
- package/dist/sql/index.d.ts +2 -0
- package/dist/sql/index.js +19 -0
- package/dist/sql/index.js.map +1 -0
- package/dist/types/api.d.ts +412 -0
- package/dist/types/api.js +9 -0
- package/dist/types/api.js.map +1 -0
- package/dist/utils/colors.d.ts +40 -0
- package/dist/utils/colors.js +64 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/{util.d.ts → utils/common.d.ts} +10 -6
- package/dist/{util.js → utils/common.js} +80 -13
- package/dist/utils/common.js.map +1 -0
- package/dist/utils/csv.js.map +1 -0
- package/dist/utils/datatype.d.ts +49 -0
- package/dist/utils/datatype.js +222 -0
- package/dist/utils/datatype.js.map +1 -0
- package/dist/utils/icons.d.ts +11 -0
- package/dist/utils/icons.js +72 -0
- package/dist/utils/icons.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/{logger.d.ts → utils/logger.d.ts} +1 -1
- package/dist/utils/logger.js +40 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/views/layouts/main.hbs +33 -11
- package/dist/views/pages/databases.hbs +182 -74
- package/dist/views/pages/designer.hbs +9 -6
- package/dist/views/pages/error.hbs +3 -3
- package/dist/views/pages/form.hbs +5 -3
- package/dist/views/pages/home.hbs +22 -13
- package/dist/views/pages/login.hbs +111 -0
- package/dist/views/pages/query.hbs +10 -8
- package/dist/views/pages/schema.hbs +41 -13
- package/dist/views/pages/seed.hbs +232 -0
- package/dist/views/pages/table.hbs +155 -70
- package/dist/views/partials/icon.hbs +2 -0
- package/dist/views/partials/navbar.hbs +70 -31
- package/dist/views/partials/sidebar.hbs +108 -99
- package/package.json +12 -5
- package/dist/args.d.ts +0 -28
- package/dist/args.js +0 -187
- package/dist/args.js.map +0 -1
- package/dist/config.d.ts +0 -17
- package/dist/config.js +0 -23
- package/dist/config.js.map +0 -1
- package/dist/csv.js.map +0 -1
- package/dist/logger.js +0 -25
- package/dist/logger.js.map +0 -1
- package/dist/routes/api.d.ts +0 -9
- package/dist/routes/api.js +0 -786
- package/dist/routes/api.js.map +0 -1
- package/dist/test/classifier.test.d.ts +0 -1
- package/dist/test/classifier.test.js +0 -44
- package/dist/test/classifier.test.js.map +0 -1
- package/dist/test/csv.test.d.ts +0 -1
- package/dist/test/csv.test.js +0 -64
- package/dist/test/csv.test.js.map +0 -1
- package/dist/test/database.test.d.ts +0 -1
- package/dist/test/database.test.js +0 -402
- package/dist/test/database.test.js.map +0 -1
- package/dist/test/generator.test.d.ts +0 -1
- package/dist/test/generator.test.js +0 -132
- package/dist/test/generator.test.js.map +0 -1
- package/dist/test/manager.test.d.ts +0 -1
- package/dist/test/manager.test.js +0 -146
- package/dist/test/manager.test.js.map +0 -1
- package/dist/util.js.map +0 -1
- /package/dist/{csv.d.ts → utils/csv.d.ts} +0 -0
- /package/dist/{csv.js → utils/csv.js} +0 -0
package/dist/public/js/query.js
CHANGED
|
@@ -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
|
|
97
|
-
|
|
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
|
-
|
|
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');
|
package/dist/public/js/schema.js
CHANGED
|
@@ -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 = [
|
|
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
|
|
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">' +
|
|
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
|
-
// ----
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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');
|