admindb 2.1.0 → 2.1.1
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 +1 -1
- package/dist/app.d.ts +1 -69
- package/dist/app.js +1 -257
- package/dist/auth/config.d.ts +1 -19
- package/dist/auth/config.js +1 -147
- package/dist/auth/crypto.d.ts +1 -27
- package/dist/auth/crypto.js +1 -133
- package/dist/auth/index.d.ts +1 -5
- package/dist/auth/index.js +1 -22
- package/dist/auth/middleware.d.ts +1 -14
- package/dist/auth/middleware.js +1 -100
- package/dist/auth/routes.d.ts +1 -7
- package/dist/auth/routes.js +1 -78
- package/dist/auth/types.d.ts +1 -35
- package/dist/auth/types.js +1 -11
- package/dist/cli/args.d.ts +1 -45
- package/dist/cli/args.js +1 -328
- package/dist/cli/config.d.ts +1 -56
- package/dist/cli/config.js +1 -133
- package/dist/cli/index.d.ts +1 -4
- package/dist/cli/index.js +1 -21
- package/dist/cli/runner.d.ts +1 -1
- package/dist/cli/runner.js +1 -222
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +1 -5
- package/dist/data/datasets.d.ts +1 -17
- package/dist/data/datasets.js +1 -133
- package/dist/data/detector.d.ts +1 -30
- package/dist/data/detector.js +1 -472
- package/dist/data/engine.d.ts +1 -10
- package/dist/data/engine.js +1 -211
- package/dist/data/index.d.ts +1 -5
- package/dist/data/index.js +1 -22
- package/dist/data/strategies.d.ts +1 -30
- package/dist/data/strategies.js +1 -343
- package/dist/data/types.d.ts +1 -73
- package/dist/data/types.js +1 -8
- package/dist/db/database.d.ts +1 -119
- package/dist/db/database.js +1 -471
- package/dist/db/export.d.ts +1 -12
- package/dist/db/export.js +1 -42
- package/dist/db/filters.d.ts +1 -22
- package/dist/db/filters.js +1 -158
- package/dist/db/index.d.ts +1 -8
- package/dist/db/index.js +1 -25
- package/dist/db/introspection.d.ts +1 -6
- package/dist/db/introspection.js +1 -172
- package/dist/db/manager.d.ts +1 -81
- package/dist/db/manager.js +1 -397
- package/dist/db/migrations.d.ts +1 -5
- package/dist/db/migrations.js +1 -110
- package/dist/db/postgres.d.ts +1 -85
- package/dist/db/postgres.js +1 -745
- package/dist/db/types.d.ts +1 -206
- package/dist/db/types.js +1 -7
- package/dist/index.d.ts +1 -23
- package/dist/index.js +1 -66
- package/dist/public/js/api.js +1 -47
- package/dist/public/js/app.js +1 -262
- package/dist/public/js/browse.js +1 -1371
- package/dist/public/js/databases.js +1 -235
- package/dist/public/js/designer.js +1 -258
- package/dist/public/js/forms.js +1 -826
- package/dist/public/js/icons.js +1 -51
- package/dist/public/js/inspector.js +1 -638
- package/dist/public/js/query.js +1 -322
- package/dist/public/js/schema.js +1 -447
- package/dist/public/js/seed.js +1 -1090
- package/dist/routes/api/helpers.d.ts +1 -42
- package/dist/routes/api/helpers.js +1 -133
- package/dist/routes/api/import-export.d.ts +1 -3
- package/dist/routes/api/import-export.js +1 -85
- package/dist/routes/api/index.d.ts +1 -4
- package/dist/routes/api/index.js +1 -37
- package/dist/routes/api/query.d.ts +1 -3
- package/dist/routes/api/query.js +1 -76
- package/dist/routes/api/rows.d.ts +1 -3
- package/dist/routes/api/rows.js +1 -373
- package/dist/routes/api/seed.d.ts +1 -3
- package/dist/routes/api/seed.js +1 -82
- package/dist/routes/api/tables.d.ts +1 -3
- package/dist/routes/api/tables.js +1 -160
- package/dist/routes/databases.d.ts +1 -26
- package/dist/routes/databases.js +1 -230
- package/dist/routes/index.d.ts +1 -3
- package/dist/routes/index.js +1 -20
- package/dist/routes/pages.d.ts +1 -9
- package/dist/routes/pages.js +1 -390
- package/dist/serverless.d.ts +1 -40
- package/dist/serverless.js +1 -210
- package/dist/sql/classifier.d.ts +1 -19
- package/dist/sql/classifier.js +1 -37
- package/dist/sql/generator.d.ts +1 -81
- package/dist/sql/generator.js +1 -348
- package/dist/sql/index.d.ts +1 -2
- package/dist/sql/index.js +1 -19
- package/dist/types/api.d.ts +1 -412
- package/dist/types/api.js +1 -9
- package/dist/utils/colors.d.ts +1 -40
- package/dist/utils/colors.js +1 -64
- package/dist/utils/common.d.ts +1 -50
- package/dist/utils/common.js +1 -229
- package/dist/utils/csv.d.ts +1 -15
- package/dist/utils/csv.js +1 -80
- package/dist/utils/datatype.d.ts +1 -49
- package/dist/utils/datatype.js +1 -222
- package/dist/utils/icons.d.ts +1 -11
- package/dist/utils/icons.js +1 -72
- package/dist/utils/index.d.ts +1 -4
- package/dist/utils/index.js +1 -21
- package/dist/utils/logger.d.ts +1 -10
- package/dist/utils/logger.js +1 -40
- package/dist/views/layouts/main.hbs +0 -4
- package/dist/views/pages/databases.hbs +0 -18
- package/dist/views/pages/designer.hbs +0 -10
- package/dist/views/pages/error.hbs +0 -2
- package/dist/views/pages/form.hbs +0 -5
- package/dist/views/pages/home.hbs +0 -8
- package/dist/views/pages/login.hbs +0 -13
- package/dist/views/pages/query.hbs +0 -9
- package/dist/views/pages/schema.hbs +0 -20
- package/dist/views/pages/seed.hbs +0 -31
- package/dist/views/pages/table.hbs +0 -14
- package/dist/views/partials/icon.hbs +0 -1
- package/dist/views/partials/navbar.hbs +0 -6
- package/dist/views/partials/sidebar.hbs +0 -8
- package/package.json +3 -2
- package/dist/app.js.map +0 -1
- package/dist/auth/config.js.map +0 -1
- package/dist/auth/crypto.js.map +0 -1
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/middleware.js.map +0 -1
- package/dist/auth/routes.js.map +0 -1
- package/dist/auth/types.js.map +0 -1
- package/dist/cli/args.js.map +0 -1
- package/dist/cli/config.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/runner.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/data/datasets.js.map +0 -1
- package/dist/data/detector.js.map +0 -1
- package/dist/data/engine.js.map +0 -1
- package/dist/data/index.js.map +0 -1
- package/dist/data/strategies.js.map +0 -1
- package/dist/data/types.js.map +0 -1
- package/dist/db/database.js.map +0 -1
- package/dist/db/export.js.map +0 -1
- package/dist/db/filters.js.map +0 -1
- package/dist/db/index.js.map +0 -1
- package/dist/db/introspection.js.map +0 -1
- package/dist/db/manager.js.map +0 -1
- package/dist/db/migrations.js.map +0 -1
- package/dist/db/postgres.js.map +0 -1
- package/dist/db/types.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/public/css/input.css +0 -302
- package/dist/routes/api/helpers.js.map +0 -1
- package/dist/routes/api/import-export.js.map +0 -1
- package/dist/routes/api/index.js.map +0 -1
- package/dist/routes/api/query.js.map +0 -1
- package/dist/routes/api/rows.js.map +0 -1
- package/dist/routes/api/seed.js.map +0 -1
- package/dist/routes/api/tables.js.map +0 -1
- package/dist/routes/databases.js.map +0 -1
- package/dist/routes/index.js.map +0 -1
- package/dist/routes/pages.js.map +0 -1
- package/dist/serverless.js.map +0 -1
- package/dist/sql/classifier.js.map +0 -1
- package/dist/sql/generator.js.map +0 -1
- package/dist/sql/index.js.map +0 -1
- package/dist/types/api.js.map +0 -1
- package/dist/utils/colors.js.map +0 -1
- package/dist/utils/common.js.map +0 -1
- package/dist/utils/csv.js.map +0 -1
- package/dist/utils/datatype.js.map +0 -1
- package/dist/utils/icons.js.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/logger.js.map +0 -1
|
@@ -1,235 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* toggle readonly mode, delete/disconnect databases, and browse filesystem. */
|
|
3
|
-
(function () {
|
|
4
|
-
'use strict';
|
|
5
|
-
const nameInput = document.getElementById('new-db-name');
|
|
6
|
-
const createBtn = document.getElementById('create-db');
|
|
7
|
-
const pgNameInput = document.getElementById('pg-conn-name');
|
|
8
|
-
const pgUriInput = document.getElementById('pg-conn-uri');
|
|
9
|
-
const pgRoInput = document.getElementById('pg-conn-readonly');
|
|
10
|
-
const connectPgBtn = document.getElementById('connect-pg');
|
|
11
|
-
const fsRoInput = document.getElementById('fs-readonly');
|
|
12
|
-
|
|
13
|
-
function escapeHtml(s) {
|
|
14
|
-
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function fmtSize(n) {
|
|
18
|
-
if (!Number.isFinite(n) || n <= 0) return '0 B';
|
|
19
|
-
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
20
|
-
let v = n;
|
|
21
|
-
let u = 0;
|
|
22
|
-
while (v >= 1024 && u < units.length - 1) {
|
|
23
|
-
v /= 1024;
|
|
24
|
-
u += 1;
|
|
25
|
-
}
|
|
26
|
-
return (u === 0 ? Math.round(v) : v.toFixed(1)) + ' ' + units[u];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ---- Create SQLite DB ----------------------------------------------------
|
|
30
|
-
|
|
31
|
-
async function createDb() {
|
|
32
|
-
const name = (nameInput.value || '').trim();
|
|
33
|
-
if (!name) return UI.showError('Enter a database name.');
|
|
34
|
-
try {
|
|
35
|
-
const data = await Api.post('/api/databases', { name });
|
|
36
|
-
UI.showToast(data.message || 'Database created.', 'success');
|
|
37
|
-
setTimeout(() => {
|
|
38
|
-
window.location.href = Api.basePath + '/' + encodeURIComponent(data.id);
|
|
39
|
-
}, 400);
|
|
40
|
-
} catch (e) {
|
|
41
|
-
UI.showError(e.message);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (createBtn) createBtn.addEventListener('click', createDb);
|
|
46
|
-
if (nameInput) {
|
|
47
|
-
nameInput.addEventListener('keydown', (e) => {
|
|
48
|
-
if (e.key === 'Enter') createDb();
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ---- Connect PostgreSQL DB -----------------------------------------------
|
|
53
|
-
|
|
54
|
-
async function connectPg() {
|
|
55
|
-
const name = (pgNameInput ? pgNameInput.value : '').trim();
|
|
56
|
-
const connectionString = (pgUriInput ? pgUriInput.value : '').trim();
|
|
57
|
-
const readonly = pgRoInput ? pgRoInput.checked : false;
|
|
58
|
-
if (!connectionString) {
|
|
59
|
-
return UI.showError('Enter a PostgreSQL connection URI (e.g. postgresql://user:password@localhost:5432/dbname).');
|
|
60
|
-
}
|
|
61
|
-
if (connectPgBtn) {
|
|
62
|
-
connectPgBtn.disabled = true;
|
|
63
|
-
connectPgBtn.textContent = 'Connecting…';
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
const data = await Api.post('/api/databases/connect-postgres', { name, connectionString, readonly });
|
|
67
|
-
UI.showToast(data.message || 'PostgreSQL connected.', 'success');
|
|
68
|
-
setTimeout(() => {
|
|
69
|
-
window.location.href = Api.basePath + '/' + encodeURIComponent(data.id);
|
|
70
|
-
}, 400);
|
|
71
|
-
} catch (e) {
|
|
72
|
-
UI.showError(e.message);
|
|
73
|
-
if (connectPgBtn) {
|
|
74
|
-
connectPgBtn.disabled = false;
|
|
75
|
-
connectPgBtn.innerHTML = (window.AdminIcons ? AdminIcons.get('plus', 'h-4 w-4') : '') + ' Connect PostgreSQL';
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (connectPgBtn) connectPgBtn.addEventListener('click', connectPg);
|
|
81
|
-
if (pgUriInput) {
|
|
82
|
-
pgUriInput.addEventListener('keydown', (e) => {
|
|
83
|
-
if (e.key === 'Enter') connectPg();
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---- Toggle Read-only / Writable Mode ------------------------------------
|
|
88
|
-
|
|
89
|
-
document.querySelectorAll('.js-toggle-db-mode').forEach((btn) => {
|
|
90
|
-
btn.addEventListener('click', async () => {
|
|
91
|
-
const id = btn.dataset.id;
|
|
92
|
-
if (!id) return;
|
|
93
|
-
const currentRo = btn.dataset.readonly === 'true';
|
|
94
|
-
const targetRo = !currentRo;
|
|
95
|
-
try {
|
|
96
|
-
const data = await Api.post('/api/databases/' + encodeURIComponent(id) + '/mode', { readonly: targetRo });
|
|
97
|
-
UI.showToast(data.message || `Mode updated to ${targetRo ? 'Read-only' : 'Writable'}.`, 'success');
|
|
98
|
-
window.location.reload();
|
|
99
|
-
} catch (e) {
|
|
100
|
-
UI.showError(e.message);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// ---- Delete SQLite DB ----------------------------------------------------
|
|
106
|
-
|
|
107
|
-
document.querySelectorAll('.js-delete-db').forEach((btn) => {
|
|
108
|
-
btn.addEventListener('click', async () => {
|
|
109
|
-
const id = btn.dataset.id;
|
|
110
|
-
if (!id) return;
|
|
111
|
-
const confirmed = await UI.confirm({
|
|
112
|
-
title: 'Delete Database',
|
|
113
|
-
message: 'Are you sure you want to permanently delete database "' + id + '"? This will remove the SQLite database file and all stored tables and records from the disk.',
|
|
114
|
-
item: id,
|
|
115
|
-
itemType: 'Database',
|
|
116
|
-
requireInputMatch: id,
|
|
117
|
-
confirmText: 'Delete database',
|
|
118
|
-
danger: true,
|
|
119
|
-
});
|
|
120
|
-
if (!confirmed) return;
|
|
121
|
-
Api.del('/api/databases/' + encodeURIComponent(id))
|
|
122
|
-
.then((data) => {
|
|
123
|
-
UI.showToast(data.message || 'Database deleted.', 'success');
|
|
124
|
-
window.location.reload();
|
|
125
|
-
})
|
|
126
|
-
.catch((e) => UI.showError(e.message));
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ---- Disconnect PostgreSQL DB --------------------------------------------
|
|
131
|
-
|
|
132
|
-
document.querySelectorAll('.js-disconnect-db').forEach((btn) => {
|
|
133
|
-
btn.addEventListener('click', async () => {
|
|
134
|
-
const id = btn.dataset.id;
|
|
135
|
-
const name = btn.dataset.name || id;
|
|
136
|
-
if (!id) return;
|
|
137
|
-
const confirmed = await UI.confirm({
|
|
138
|
-
title: 'Disconnect PostgreSQL Database',
|
|
139
|
-
message: 'Are you sure you want to disconnect database "' + name + '" from AdminDB? This closes the connection pool in AdminDB; your remote database and data are untouched.',
|
|
140
|
-
confirmText: 'Disconnect database',
|
|
141
|
-
danger: false,
|
|
142
|
-
});
|
|
143
|
-
if (!confirmed) return;
|
|
144
|
-
Api.del('/api/databases/' + encodeURIComponent(id))
|
|
145
|
-
.then((data) => {
|
|
146
|
-
UI.showToast(data.message || 'Database disconnected.', 'success');
|
|
147
|
-
window.location.reload();
|
|
148
|
-
})
|
|
149
|
-
.catch((e) => UI.showError(e.message));
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// ---- Filesystem browser --------------------------------------------------
|
|
154
|
-
|
|
155
|
-
const fsPathInput = document.getElementById('fs-path');
|
|
156
|
-
const fsGoBtn = document.getElementById('fs-go');
|
|
157
|
-
const fsUpBtn = document.getElementById('fs-up');
|
|
158
|
-
const fsCurrent = document.getElementById('fs-current');
|
|
159
|
-
const fsListing = document.getElementById('fs-listing');
|
|
160
|
-
|
|
161
|
-
let fsState = { path: '', parent: null };
|
|
162
|
-
|
|
163
|
-
function renderFs(data) {
|
|
164
|
-
fsState = { path: data.path, parent: data.parent };
|
|
165
|
-
if (fsCurrent) fsCurrent.textContent = data.path;
|
|
166
|
-
if (fsPathInput) fsPathInput.value = data.path;
|
|
167
|
-
if (fsUpBtn) fsUpBtn.disabled = !data.parent;
|
|
168
|
-
if (!fsListing) return;
|
|
169
|
-
fsListing.innerHTML = '';
|
|
170
|
-
if (!data.entries.length) {
|
|
171
|
-
fsListing.innerHTML = '<div class="px-4 py-6 text-center text-sm text-base-content/40">No folders or database files here.</div>';
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const folderIcon = window.AdminIcons ? AdminIcons.get('folder', 'h-3.5 w-3.5') : '📁';
|
|
175
|
-
const dbIcon = window.AdminIcons ? AdminIcons.get('database', 'h-3.5 w-3.5') : '🗄️';
|
|
176
|
-
|
|
177
|
-
data.entries.forEach((e) => {
|
|
178
|
-
const row = document.createElement('button');
|
|
179
|
-
row.type = 'button';
|
|
180
|
-
row.className = 'flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm transition hover:bg-primary/[0.06]';
|
|
181
|
-
row.title = e.path;
|
|
182
|
-
if (e.isDir) {
|
|
183
|
-
row.innerHTML =
|
|
184
|
-
'<span class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-base-200 text-base-content/50">' + folderIcon + '</span>' +
|
|
185
|
-
'<span class="min-w-0 flex-1 truncate font-medium text-base-content/80">' + escapeHtml(e.name) + '</span>' +
|
|
186
|
-
'<span class="text-xs text-base-content/35">folder</span>';
|
|
187
|
-
row.addEventListener('click', () => loadFs(e.path));
|
|
188
|
-
} else {
|
|
189
|
-
row.innerHTML =
|
|
190
|
-
'<span class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">' + dbIcon + '</span>' +
|
|
191
|
-
'<span class="min-w-0 flex-1 truncate font-mono text-[13px] text-base-content/85">' + escapeHtml(e.name) + '</span>' +
|
|
192
|
-
'<span class="text-xs text-base-content/40">' + fmtSize(e.size) + '</span>';
|
|
193
|
-
row.addEventListener('click', () => openDb(e.path));
|
|
194
|
-
}
|
|
195
|
-
fsListing.appendChild(row);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function loadFs(p) {
|
|
200
|
-
try {
|
|
201
|
-
const data = await Api.get('/api/fs/list' + (p ? '?path=' + encodeURIComponent(p) : ''));
|
|
202
|
-
renderFs(data);
|
|
203
|
-
} catch (e) {
|
|
204
|
-
UI.showError(e.message);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function goUp() {
|
|
209
|
-
if (fsState.parent) loadFs(fsState.parent);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function openDb(p) {
|
|
213
|
-
const readonly = fsRoInput ? fsRoInput.checked : false;
|
|
214
|
-
try {
|
|
215
|
-
const data = await Api.post('/api/databases/open', { path: p, readonly });
|
|
216
|
-
UI.showToast(data.message || 'Database opened.', 'success');
|
|
217
|
-
window.location.href = Api.basePath + '/' + encodeURIComponent(data.id);
|
|
218
|
-
} catch (e) {
|
|
219
|
-
UI.showError(e.message);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (fsListing) {
|
|
224
|
-
loadFs('');
|
|
225
|
-
if (fsGoBtn) {
|
|
226
|
-
fsGoBtn.addEventListener('click', () => loadFs((fsPathInput.value || '').trim() || ''));
|
|
227
|
-
}
|
|
228
|
-
if (fsPathInput) {
|
|
229
|
-
fsPathInput.addEventListener('keydown', (e) => {
|
|
230
|
-
if (e.key === 'Enter') loadFs((fsPathInput.value || '').trim() || '');
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
if (fsUpBtn) fsUpBtn.addEventListener('click', goUp);
|
|
234
|
-
}
|
|
235
|
-
})();
|
|
1
|
+
!function(){"use strict";const e=document.getElementById("new-db-name"),t=document.getElementById("create-db"),n=document.getElementById("pg-conn-name"),a=document.getElementById("pg-conn-uri"),s=document.getElementById("pg-conn-readonly"),o=document.getElementById("connect-pg"),c=document.getElementById("fs-readonly");function r(e){return window.UI&&UI.escapeHtml?UI.escapeHtml(e):String(null==e?"":e)}async function d(){const t=(e.value||"").trim();if(!t)return UI.showError("Enter a database name.");try{const e=await Api.post("/api/databases",{name:t});UI.showToast(e.message||"Database created.","success"),setTimeout(()=>{window.location.href=Api.basePath+"/"+encodeURIComponent(e.id)},400)}catch(e){UI.showError(e.message)}}async function i(){const e=(n?n.value:"").trim(),t=(a?a.value:"").trim(),c=!!s&&s.checked;if(!t)return UI.showError("Enter a PostgreSQL connection URI (e.g. postgresql://user:password@localhost:5432/dbname).");o&&(o.disabled=!0,o.textContent="Connecting…");try{const n=await Api.post("/api/databases/connect-postgres",{name:e,connectionString:t,readonly:c});UI.showToast(n.message||"PostgreSQL connected.","success"),setTimeout(()=>{window.location.href=Api.basePath+"/"+encodeURIComponent(n.id)},400)}catch(e){UI.showError(e.message),o&&(o.disabled=!1,o.innerHTML=(window.AdminIcons?AdminIcons.get("plus","h-4 w-4"):"")+" Connect PostgreSQL")}}t&&t.addEventListener("click",d),e&&e.addEventListener("keydown",e=>{"Enter"===e.key&&d()}),o&&o.addEventListener("click",i),a&&a.addEventListener("keydown",e=>{"Enter"===e.key&&i()}),document.querySelectorAll(".js-toggle-db-mode").forEach(e=>{e.addEventListener("click",async()=>{const t=e.dataset.id;if(!t)return;const n=!("true"===e.dataset.readonly);try{const e=await Api.post("/api/databases/"+encodeURIComponent(t)+"/mode",{readonly:n});UI.showToast(e.message||`Mode updated to ${n?"Read-only":"Writable"}.`,"success"),window.location.reload()}catch(e){UI.showError(e.message)}})}),document.querySelectorAll(".js-delete-db").forEach(e=>{e.addEventListener("click",async()=>{const t=e.dataset.id;if(!t)return;await UI.confirm({title:"Delete Database",message:'Are you sure you want to permanently delete database "'+t+'"? This will remove the SQLite database file and all stored tables and records from the disk.',item:t,itemType:"Database",requireInputMatch:t,confirmText:"Delete database",danger:!0})&&Api.del("/api/databases/"+encodeURIComponent(t)).then(e=>{UI.showToast(e.message||"Database deleted.","success"),window.location.reload()}).catch(e=>UI.showError(e.message))})}),document.querySelectorAll(".js-disconnect-db").forEach(e=>{e.addEventListener("click",async()=>{const t=e.dataset.id,n=e.dataset.name||t;if(!t)return;await UI.confirm({title:"Disconnect PostgreSQL Database",message:'Are you sure you want to disconnect database "'+n+'" from AdminDB? This closes the connection pool in AdminDB; your remote database and data are untouched.',confirmText:"Disconnect database",danger:!1})&&Api.del("/api/databases/"+encodeURIComponent(t)).then(e=>{UI.showToast(e.message||"Database disconnected.","success"),window.location.reload()}).catch(e=>UI.showError(e.message))})});const l=document.getElementById("fs-path"),m=document.getElementById("fs-go"),p=document.getElementById("fs-up"),u=document.getElementById("fs-current"),h=document.getElementById("fs-listing");let g={path:"",parent:null};function w(e){if(g={path:e.path,parent:e.parent},u&&(u.textContent=e.path),l&&(l.value=e.path),p&&(p.disabled=!e.parent),!h)return;if(h.innerHTML="",!e.entries.length)return void(h.innerHTML='<div class="px-4 py-6 text-center text-sm text-base-content/40">No folders or database files here.</div>');const t=window.AdminIcons?AdminIcons.get("folder","h-3.5 w-3.5"):"📁",n=window.AdminIcons?AdminIcons.get("database","h-3.5 w-3.5"):"🗄️";e.entries.forEach(e=>{const a=document.createElement("button");a.type="button",a.className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm transition hover:bg-primary/[0.06]",a.title=e.path,e.isDir?(a.innerHTML='<span class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-base-200 text-base-content/50">'+t+'</span><span class="min-w-0 flex-1 truncate font-medium text-base-content/80">'+r(e.name)+'</span><span class="text-xs text-base-content/35">folder</span>',a.addEventListener("click",()=>y(e.path))):(a.innerHTML='<span class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">'+n+'</span><span class="min-w-0 flex-1 truncate font-mono text-[13px] text-base-content/85">'+r(e.name)+'</span><span class="text-xs text-base-content/40">'+function(e){if(!Number.isFinite(e)||e<=0)return"0 B";const t=["B","KB","MB","GB","TB"];let n=e,a=0;for(;n>=1024&&a<t.length-1;)n/=1024,a+=1;return(0===a?Math.round(n):n.toFixed(1))+" "+t[a]}(e.size)+"</span>",a.addEventListener("click",()=>async function(e){const t=!!c&&c.checked;try{const n=await Api.post("/api/databases/open",{path:e,readonly:t});UI.showToast(n.message||"Database opened.","success"),window.location.href=Api.basePath+"/"+encodeURIComponent(n.id)}catch(e){UI.showError(e.message)}}(e.path))),h.appendChild(a)})}async function y(e){try{w(await Api.get("/api/fs/list"+(e?"?path="+encodeURIComponent(e):"")))}catch(e){UI.showError(e.message)}}h&&(y(""),m&&m.addEventListener("click",()=>y((l.value||"").trim()||"")),l&&l.addEventListener("keydown",e=>{"Enter"===e.key&&y((l.value||"").trim()||"")}),p&&p.addEventListener("click",function(){g.parent&&y(g.parent)}))}();
|
|
@@ -1,258 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* and preview the generated CREATE TABLE live. Create executes; Copy copies. */
|
|
3
|
-
(function () {
|
|
4
|
-
'use strict';
|
|
5
|
-
const SQLITE_TYPES = [
|
|
6
|
-
'TEXT',
|
|
7
|
-
'INTEGER',
|
|
8
|
-
'REAL',
|
|
9
|
-
'BLOB',
|
|
10
|
-
'BOOLEAN',
|
|
11
|
-
'DATE',
|
|
12
|
-
'DATETIME',
|
|
13
|
-
'NUMERIC',
|
|
14
|
-
'VARCHAR(255)',
|
|
15
|
-
'JSON',
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
const POSTGRES_TYPES = [
|
|
19
|
-
'TEXT',
|
|
20
|
-
'VARCHAR(255)',
|
|
21
|
-
'INTEGER',
|
|
22
|
-
'BIGINT',
|
|
23
|
-
'SMALLINT',
|
|
24
|
-
'SERIAL',
|
|
25
|
-
'BIGSERIAL',
|
|
26
|
-
'REAL',
|
|
27
|
-
'DECIMAL(10,2)',
|
|
28
|
-
'NUMERIC',
|
|
29
|
-
'BOOLEAN',
|
|
30
|
-
'DATE',
|
|
31
|
-
'DATETIME',
|
|
32
|
-
'TIMESTAMP',
|
|
33
|
-
'TIMESTAMPTZ',
|
|
34
|
-
'TIME',
|
|
35
|
-
'INTERVAL',
|
|
36
|
-
'JSON',
|
|
37
|
-
'JSONB',
|
|
38
|
-
'UUID',
|
|
39
|
-
'BYTEA',
|
|
40
|
-
'INET',
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
const isPostgres = window.APP && window.APP.dialect === 'postgres';
|
|
44
|
-
const DESIGNER_TYPES = isPostgres ? POSTGRES_TYPES : SQLITE_TYPES;
|
|
45
|
-
|
|
46
|
-
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
47
|
-
|
|
48
|
-
const columnsEl = document.getElementById('columns');
|
|
49
|
-
const errorBox = document.getElementById('designer-error');
|
|
50
|
-
const tableNameEl = document.getElementById('table-name');
|
|
51
|
-
const addBtn = document.getElementById('add-column');
|
|
52
|
-
const createBtn = document.getElementById('create-table');
|
|
53
|
-
const copyBtn = document.getElementById('copy-sql');
|
|
54
|
-
const previewBox = document.getElementById('sql-preview');
|
|
55
|
-
const previewCode = document.getElementById('preview-code');
|
|
56
|
-
const previewPlaceholder = document.getElementById('preview-placeholder');
|
|
57
|
-
const previewStatus = document.getElementById('preview-status');
|
|
58
|
-
|
|
59
|
-
const columnCache = {};
|
|
60
|
-
let fkTables = [];
|
|
61
|
-
let previewTimer = null;
|
|
62
|
-
|
|
63
|
-
function escapeHtml(s) {
|
|
64
|
-
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function loadColumns(table) {
|
|
68
|
-
if (columnCache[table]) return columnCache[table];
|
|
69
|
-
const info = await Api.get('/api/tables/' + encodeURIComponent(table) + '/info');
|
|
70
|
-
columnCache[table] = info.columns.map((c) => c.name);
|
|
71
|
-
return columnCache[table];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function addColumnRow() {
|
|
75
|
-
const row = document.createElement('div');
|
|
76
|
-
row.className = 'grid grid-cols-2 items-end gap-2.5 rounded-xl border border-base-300 bg-base-100 p-3 shadow-soft sm:grid-cols-12';
|
|
77
|
-
row.innerHTML = [
|
|
78
|
-
'<div class="col-span-2 sm:col-span-2"><label class="field-label">Name</label>',
|
|
79
|
-
'<input class="field-input col-name" placeholder="column_name" autocomplete="off" spellcheck="false"></div>',
|
|
80
|
-
'<div class="col-span-1 sm:col-span-2"><label class="field-label">Type</label>',
|
|
81
|
-
'<select class="field-select col-type">' + DESIGNER_TYPES.map((t) => '<option>' + t + '</option>').join('') + '</select></div>',
|
|
82
|
-
'<div class="col-span-1 sm:col-span-2"><label class="field-label">FK table</label>',
|
|
83
|
-
'<select class="field-select col-fk-table"><option value="">— none —</option>' +
|
|
84
|
-
fkTables.map((t) => '<option>' + escapeHtml(t) + '</option>').join('') + '</select></div>',
|
|
85
|
-
'<div class="col-span-1 sm:col-span-2"><label class="field-label">FK column</label>',
|
|
86
|
-
'<select class="field-select col-fk-col"><option value="">—</option></select></div>',
|
|
87
|
-
'<div class="col-span-1 sm:col-span-2"><label class="field-label">Default</label>',
|
|
88
|
-
'<input class="field-input col-default" placeholder="0, CURRENT_TIMESTAMP" autocomplete="off" spellcheck="false"></div>',
|
|
89
|
-
'<div class="col-span-2 flex items-end justify-between gap-1 pb-0.5 sm:col-span-2">',
|
|
90
|
-
'<label class="flex cursor-pointer flex-col items-center gap-1" title="Primary key"><input type="checkbox" class="checkbox checkbox-primary checkbox-xs col-pk"><span class="text-[10px] font-semibold uppercase text-base-content/50">PK</span></label>',
|
|
91
|
-
'<label class="flex cursor-pointer flex-col items-center gap-1" title="Not null"><input type="checkbox" class="checkbox checkbox-xs col-notnull"><span class="text-[10px] font-semibold uppercase text-base-content/50">NN</span></label>',
|
|
92
|
-
'<label class="flex cursor-pointer flex-col items-center gap-1" title="Unique"><input type="checkbox" class="checkbox checkbox-xs col-unique"><span class="text-[10px] font-semibold uppercase text-base-content/50">UQ</span></label>',
|
|
93
|
-
'<button type="button" class="btn btn-ghost btn-xs text-error remove-col" title="Remove column">×</button>',
|
|
94
|
-
'</div>',
|
|
95
|
-
].join('');
|
|
96
|
-
columnsEl.appendChild(row);
|
|
97
|
-
|
|
98
|
-
const fkTableSel = row.querySelector('.col-fk-table');
|
|
99
|
-
const fkColSel = row.querySelector('.col-fk-col');
|
|
100
|
-
fkTableSel.addEventListener('change', () => {
|
|
101
|
-
fkColSel.innerHTML = '<option value="">—</option>';
|
|
102
|
-
const target = fkTableSel.value;
|
|
103
|
-
if (!target) return;
|
|
104
|
-
loadColumns(target)
|
|
105
|
-
.then((cols) => {
|
|
106
|
-
fkColSel.innerHTML = '<option value="">—</option>' + cols.map((c) => '<option>' + escapeHtml(c) + '</option>').join('');
|
|
107
|
-
})
|
|
108
|
-
.catch(() => {});
|
|
109
|
-
});
|
|
110
|
-
row.querySelector('.remove-col').addEventListener('click', async () => {
|
|
111
|
-
const colName = row.querySelector('.col-name').value.trim();
|
|
112
|
-
if (colName) {
|
|
113
|
-
const confirmed = await UI.confirm({
|
|
114
|
-
title: 'Remove Column',
|
|
115
|
-
message: 'Remove column "' + colName + '" from the table definition?',
|
|
116
|
-
confirmText: 'Remove',
|
|
117
|
-
danger: true,
|
|
118
|
-
});
|
|
119
|
-
if (!confirmed) return;
|
|
120
|
-
}
|
|
121
|
-
row.remove();
|
|
122
|
-
schedulePreview();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
row.querySelectorAll('input, select').forEach((el) => {
|
|
126
|
-
el.addEventListener('input', schedulePreview);
|
|
127
|
-
el.addEventListener('change', schedulePreview);
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function collectColumns() {
|
|
132
|
-
const cols = [];
|
|
133
|
-
document.querySelectorAll('#columns > div').forEach((row) => {
|
|
134
|
-
const name = row.querySelector('.col-name').value.trim();
|
|
135
|
-
if (!name) return;
|
|
136
|
-
const fkTable = row.querySelector('.col-fk-table').value;
|
|
137
|
-
const fkCol = row.querySelector('.col-fk-col').value;
|
|
138
|
-
cols.push({
|
|
139
|
-
name,
|
|
140
|
-
type: row.querySelector('.col-type').value,
|
|
141
|
-
primaryKey: row.querySelector('.col-pk').checked,
|
|
142
|
-
notNull: row.querySelector('.col-notnull').checked,
|
|
143
|
-
unique: row.querySelector('.col-unique').checked,
|
|
144
|
-
defaultValue: row.querySelector('.col-default').value.trim() || null,
|
|
145
|
-
foreignKey: fkTable && fkCol ? { table: fkTable, column: fkCol } : null,
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
return cols;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function validate() {
|
|
152
|
-
const name = tableNameEl.value.trim();
|
|
153
|
-
const cols = collectColumns();
|
|
154
|
-
if (!name) return 'Table name is required.';
|
|
155
|
-
if (!NAME_RE.test(name)) return 'Invalid table name: "' + name + '". Use letters, digits and underscores.';
|
|
156
|
-
if (!cols.length) return 'Add at least one column.';
|
|
157
|
-
let pkCount = 0;
|
|
158
|
-
const seen = {};
|
|
159
|
-
for (const c of cols) {
|
|
160
|
-
if (!NAME_RE.test(c.name)) return 'Invalid column name: "' + c.name + '".';
|
|
161
|
-
if (seen[c.name]) return 'Duplicate column name: "' + c.name + '".';
|
|
162
|
-
seen[c.name] = true;
|
|
163
|
-
if (c.primaryKey) pkCount += 1;
|
|
164
|
-
if (c.foreignKey && (!c.foreignKey.table || !c.foreignKey.column)) {
|
|
165
|
-
return 'Column "' + c.name + '" has an incomplete foreign key (choose both table and column).';
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (pkCount > 1) return 'At most one column can be a primary key.';
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function showError(msg) {
|
|
173
|
-
errorBox.classList.remove('hidden');
|
|
174
|
-
errorBox.innerHTML = '<div class="alert alert-error shadow-lg"><span>' + escapeHtml(msg) + '</span></div>';
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function schedulePreview() {
|
|
178
|
-
clearTimeout(previewTimer);
|
|
179
|
-
previewTimer = setTimeout(runPreview, 250);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function runPreview() {
|
|
183
|
-
const err = validate();
|
|
184
|
-
if (err) {
|
|
185
|
-
errorBox.classList.add('hidden');
|
|
186
|
-
previewBox.classList.add('hidden');
|
|
187
|
-
previewPlaceholder.classList.remove('hidden');
|
|
188
|
-
if (previewStatus) previewStatus.textContent = 'fix validation errors…';
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
errorBox.classList.add('hidden');
|
|
192
|
-
try {
|
|
193
|
-
const data = await Api.post('/api/tables/generate', {
|
|
194
|
-
name: tableNameEl.value.trim(),
|
|
195
|
-
columns: collectColumns(),
|
|
196
|
-
});
|
|
197
|
-
previewCode.textContent = data.sql;
|
|
198
|
-
previewBox.classList.remove('hidden');
|
|
199
|
-
previewPlaceholder.classList.add('hidden');
|
|
200
|
-
if (previewStatus) {
|
|
201
|
-
previewStatus.textContent = 'live preview · not executed';
|
|
202
|
-
previewStatus.className = 'text-[11px] font-medium text-accent';
|
|
203
|
-
}
|
|
204
|
-
} catch (e) {
|
|
205
|
-
previewBox.classList.add('hidden');
|
|
206
|
-
previewPlaceholder.classList.remove('hidden');
|
|
207
|
-
showError(e.message);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function submit() {
|
|
212
|
-
errorBox.classList.add('hidden');
|
|
213
|
-
const err = validate();
|
|
214
|
-
if (err) {
|
|
215
|
-
showError(err);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const body = { name: tableNameEl.value.trim(), columns: collectColumns() };
|
|
219
|
-
try {
|
|
220
|
-
const data = await Api.post('/api/tables', body);
|
|
221
|
-
UI.showToast(data.message || 'Table created.', 'success');
|
|
222
|
-
setTimeout(() => {
|
|
223
|
-
window.location.href = Api.basePath + '/tables/' + encodeURIComponent(body.name);
|
|
224
|
-
}, 500);
|
|
225
|
-
} catch (e) {
|
|
226
|
-
showError(e.message);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function init() {
|
|
231
|
-
try {
|
|
232
|
-
const tables = await Api.get('/api/tables');
|
|
233
|
-
fkTables = tables.map((t) => t.name).filter((n) => n.charAt(0) !== '_');
|
|
234
|
-
} catch (e) {
|
|
235
|
-
fkTables = [];
|
|
236
|
-
}
|
|
237
|
-
addColumnRow();
|
|
238
|
-
addColumnRow();
|
|
239
|
-
tableNameEl.addEventListener('input', schedulePreview);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (addBtn) addBtn.addEventListener('click', addColumnRow);
|
|
243
|
-
if (createBtn) createBtn.addEventListener('click', submit);
|
|
244
|
-
if (copyBtn) {
|
|
245
|
-
copyBtn.addEventListener('click', () => {
|
|
246
|
-
const text = previewCode.textContent || '';
|
|
247
|
-
if (!text) {
|
|
248
|
-
UI.showError('Nothing to copy yet.');
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
navigator.clipboard.writeText(text)
|
|
252
|
-
.then(() => UI.showToast('SQL copied to clipboard.', 'success'))
|
|
253
|
-
.catch(() => UI.showError('Could not copy.'));
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
init();
|
|
258
|
-
})();
|
|
1
|
+
!function(){"use strict";const e=window.APP&&"postgres"===window.APP.dialect?["TEXT","VARCHAR(255)","INTEGER","BIGINT","SMALLINT","SERIAL","BIGSERIAL","REAL","DECIMAL(10,2)","NUMERIC","BOOLEAN","DATE","DATETIME","TIMESTAMP","TIMESTAMPTZ","TIME","INTERVAL","JSON","JSONB","UUID","BYTEA","INET"]:["TEXT","INTEGER","REAL","BLOB","BOOLEAN","DATE","DATETIME","NUMERIC","VARCHAR(255)","JSON"],t=/^[A-Za-z_][A-Za-z0-9_$]*$/,l=document.getElementById("columns"),n=document.getElementById("designer-error"),o=document.getElementById("table-name"),c=document.getElementById("add-column"),a=document.getElementById("create-table"),s=document.getElementById("copy-sql"),i=document.getElementById("sql-preview"),r=document.getElementById("preview-code"),d=document.getElementById("preview-placeholder"),u=document.getElementById("preview-status"),m={};let p=[],f=null;function b(e){return window.UI&&UI.escapeHtml?UI.escapeHtml(e):String(null==e?"":e)}function v(){const t=document.createElement("div");t.className="grid grid-cols-2 items-end gap-2.5 rounded-xl border border-base-300 bg-base-100 p-3 shadow-soft sm:grid-cols-12",t.innerHTML=['<div class="col-span-2 sm:col-span-2"><label class="field-label">Name</label>','<input class="field-input col-name" placeholder="column_name" autocomplete="off" spellcheck="false"></div>','<div class="col-span-1 sm:col-span-2"><label class="field-label">Type</label>','<select class="field-select col-type">'+e.map(e=>"<option>"+e+"</option>").join("")+"</select></div>",'<div class="col-span-1 sm:col-span-2"><label class="field-label">FK table</label>','<select class="field-select col-fk-table"><option value="">— none —</option>'+p.map(e=>"<option>"+b(e)+"</option>").join("")+"</select></div>",'<div class="col-span-1 sm:col-span-2"><label class="field-label">FK column</label>','<select class="field-select col-fk-col"><option value="">—</option></select></div>','<div class="col-span-1 sm:col-span-2"><label class="field-label">Default</label>','<input class="field-input col-default" placeholder="0, CURRENT_TIMESTAMP" autocomplete="off" spellcheck="false"></div>','<div class="col-span-2 flex items-end justify-between gap-1 pb-0.5 sm:col-span-2">','<label class="flex cursor-pointer flex-col items-center gap-1" title="Primary key"><input type="checkbox" class="checkbox checkbox-primary checkbox-xs col-pk"><span class="text-[10px] font-semibold uppercase text-base-content/50">PK</span></label>','<label class="flex cursor-pointer flex-col items-center gap-1" title="Not null"><input type="checkbox" class="checkbox checkbox-xs col-notnull"><span class="text-[10px] font-semibold uppercase text-base-content/50">NN</span></label>','<label class="flex cursor-pointer flex-col items-center gap-1" title="Unique"><input type="checkbox" class="checkbox checkbox-xs col-unique"><span class="text-[10px] font-semibold uppercase text-base-content/50">UQ</span></label>','<button type="button" class="btn btn-ghost btn-xs text-error remove-col" title="Remove column">×</button>',"</div>"].join(""),l.appendChild(t);const n=t.querySelector(".col-fk-table"),o=t.querySelector(".col-fk-col");n.addEventListener("change",()=>{o.innerHTML='<option value="">—</option>';const e=n.value;e&&async function(e){if(m[e])return m[e];const t=await Api.get("/api/tables/"+encodeURIComponent(e)+"/info");return m[e]=t.columns.map(e=>e.name),m[e]}(e).then(e=>{o.innerHTML='<option value="">—</option>'+e.map(e=>"<option>"+b(e)+"</option>").join("")}).catch(()=>{})}),t.querySelector(".remove-col").addEventListener("click",async()=>{const e=t.querySelector(".col-name").value.trim();if(e){if(!await UI.confirm({title:"Remove Column",message:'Remove column "'+e+'" from the table definition?',confirmText:"Remove",danger:!0}))return}t.remove(),g()}),t.querySelectorAll("input, select").forEach(e=>{e.addEventListener("input",g),e.addEventListener("change",g)})}function y(){const e=[];return document.querySelectorAll("#columns > div").forEach(t=>{const l=t.querySelector(".col-name").value.trim();if(!l)return;const n=t.querySelector(".col-fk-table").value,o=t.querySelector(".col-fk-col").value;e.push({name:l,type:t.querySelector(".col-type").value,primaryKey:t.querySelector(".col-pk").checked,notNull:t.querySelector(".col-notnull").checked,unique:t.querySelector(".col-unique").checked,defaultValue:t.querySelector(".col-default").value.trim()||null,foreignKey:n&&o?{table:n,column:o}:null})}),e}function h(){const e=o.value.trim(),l=y();if(!e)return"Table name is required.";if(!t.test(e))return'Invalid table name: "'+e+'". Use letters, digits and underscores.';if(!l.length)return"Add at least one column.";let n=0;const c={};for(const e of l){if(!t.test(e.name))return'Invalid column name: "'+e.name+'".';if(c[e.name])return'Duplicate column name: "'+e.name+'".';if(c[e.name]=!0,e.primaryKey&&(n+=1),e.foreignKey&&(!e.foreignKey.table||!e.foreignKey.column))return'Column "'+e.name+'" has an incomplete foreign key (choose both table and column).'}return n>1?"At most one column can be a primary key.":null}function E(e){n.classList.remove("hidden"),n.innerHTML='<div class="alert alert-error shadow-lg"><span>'+b(e)+"</span></div>"}function g(){clearTimeout(f),f=setTimeout(x,250)}async function x(){if(h())return n.classList.add("hidden"),i.classList.add("hidden"),d.classList.remove("hidden"),void(u&&(u.textContent="fix validation errors…"));n.classList.add("hidden");try{const e=await Api.post("/api/tables/generate",{name:o.value.trim(),columns:y()});r.textContent=e.sql,i.classList.remove("hidden"),d.classList.add("hidden"),u&&(u.textContent="live preview · not executed",u.className="text-[11px] font-medium text-accent")}catch(e){i.classList.add("hidden"),d.classList.remove("hidden"),E(e.message)}}c&&c.addEventListener("click",v),a&&a.addEventListener("click",async function(){n.classList.add("hidden");const e=h();if(e)return void E(e);const t={name:o.value.trim(),columns:y()};try{const e=await Api.post("/api/tables",t);UI.showToast(e.message||"Table created.","success"),setTimeout(()=>{window.location.href=Api.basePath+"/tables/"+encodeURIComponent(t.name)},500)}catch(e){E(e.message)}}),s&&s.addEventListener("click",()=>{const e=r.textContent||"";e?navigator.clipboard.writeText(e).then(()=>UI.showToast("SQL copied to clipboard.","success")).catch(()=>UI.showError("Could not copy.")):UI.showError("Nothing to copy yet.")}),async function(){try{const e=await Api.get("/api/tables");p=e.map(e=>e.name).filter(e=>"_"!==e.charAt(0))}catch(e){p=[]}v(),v(),o.addEventListener("input",g)}()}();
|