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.
- package/README.md +262 -329
- 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 -146
- package/dist/db/database.js +208 -461
- 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 +74 -11
- 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 +55 -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 +185 -52
- package/dist/public/js/seed.js +865 -152
- 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 +157 -64
- 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 +7 -3
- package/dist/sql/generator.js +144 -7
- 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 +40 -11
- package/dist/views/pages/databases.hbs +182 -74
- package/dist/views/pages/designer.hbs +29 -11
- 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 +8 -6
- package/dist/views/pages/schema.hbs +41 -13
- package/dist/views/pages/seed.hbs +200 -54
- package/dist/views/pages/table.hbs +152 -72
- package/dist/views/partials/icon.hbs +2 -0
- package/dist/views/partials/navbar.hbs +69 -25
- package/dist/views/partials/sidebar.hbs +108 -94
- package/docs/API.md +144 -0
- package/docs/EXAMPLES.md +576 -0
- package/docs/SECURITY.md +265 -0
- package/package.json +15 -4
- 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/data/generator.d.ts +0 -86
- package/dist/data/generator.js +0 -1031
- package/dist/data/generator.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 -848
- 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,47 @@
|
|
|
4
4
|
const cfgEl = document.getElementById('schema-config');
|
|
5
5
|
if (!cfgEl) return;
|
|
6
6
|
const cfg = JSON.parse(cfgEl.textContent);
|
|
7
|
-
const
|
|
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
|
|
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">' +
|
|
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
|
-
// ----
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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');
|