admindb 1.0.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/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/app.d.ts +36 -0
- package/dist/app.js +186 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +61 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +23 -0
- package/dist/config.js.map +1 -0
- package/dist/csv.d.ts +15 -0
- package/dist/csv.js +80 -0
- package/dist/csv.js.map +1 -0
- package/dist/db/database.d.ts +225 -0
- package/dist/db/database.js +497 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/export.d.ts +12 -0
- package/dist/db/export.js +37 -0
- package/dist/db/export.js.map +1 -0
- package/dist/db/manager.d.ts +62 -0
- package/dist/db/manager.js +206 -0
- package/dist/db/manager.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +10 -0
- package/dist/logger.js +25 -0
- package/dist/logger.js.map +1 -0
- package/dist/public/css/app.css +1 -0
- package/dist/public/css/input.css +179 -0
- package/dist/public/js/api.js +47 -0
- package/dist/public/js/app.js +120 -0
- package/dist/public/js/browse.js +123 -0
- package/dist/public/js/databases.js +45 -0
- package/dist/public/js/designer.js +205 -0
- package/dist/public/js/forms.js +244 -0
- package/dist/public/js/query.js +232 -0
- package/dist/public/js/schema.js +314 -0
- package/dist/public/js/triggers.js +155 -0
- package/dist/public/js/views.js +176 -0
- package/dist/routes/api.d.ts +9 -0
- package/dist/routes/api.js +622 -0
- package/dist/routes/api.js.map +1 -0
- package/dist/routes/databases.d.ts +19 -0
- package/dist/routes/databases.js +99 -0
- package/dist/routes/databases.js.map +1 -0
- package/dist/routes/pages.d.ts +9 -0
- package/dist/routes/pages.js +253 -0
- package/dist/routes/pages.js.map +1 -0
- package/dist/sql/classifier.d.ts +19 -0
- package/dist/sql/classifier.js +37 -0
- package/dist/sql/classifier.js.map +1 -0
- package/dist/sql/generator.d.ts +99 -0
- package/dist/sql/generator.js +259 -0
- package/dist/sql/generator.js.map +1 -0
- package/dist/test/classifier.test.d.ts +1 -0
- package/dist/test/classifier.test.js +44 -0
- package/dist/test/classifier.test.js.map +1 -0
- package/dist/test/csv.test.d.ts +1 -0
- package/dist/test/csv.test.js +64 -0
- package/dist/test/csv.test.js.map +1 -0
- package/dist/test/database.test.d.ts +1 -0
- package/dist/test/database.test.js +224 -0
- package/dist/test/database.test.js.map +1 -0
- package/dist/test/generator.test.d.ts +1 -0
- package/dist/test/generator.test.js +164 -0
- package/dist/test/generator.test.js.map +1 -0
- package/dist/test/manager.test.d.ts +1 -0
- package/dist/test/manager.test.js +146 -0
- package/dist/test/manager.test.js.map +1 -0
- package/dist/util.d.ts +29 -0
- package/dist/util.js +127 -0
- package/dist/util.js.map +1 -0
- package/dist/views/layouts/main.hbs +49 -0
- package/dist/views/pages/databases.hbs +102 -0
- package/dist/views/pages/designer.hbs +81 -0
- package/dist/views/pages/error.hbs +12 -0
- package/dist/views/pages/form.hbs +62 -0
- package/dist/views/pages/home.hbs +123 -0
- package/dist/views/pages/query.hbs +94 -0
- package/dist/views/pages/schema.hbs +290 -0
- package/dist/views/pages/table.hbs +203 -0
- package/dist/views/pages/triggers.hbs +162 -0
- package/dist/views/pages/views.hbs +134 -0
- package/dist/views/partials/navbar.hbs +50 -0
- package/dist/views/partials/sidebar.hbs +128 -0
- package/package.json +62 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/* Table schema editor: rename table, add/rename/drop columns, drop table. */
|
|
2
|
+
(function () {
|
|
3
|
+
'use strict';
|
|
4
|
+
const cfgEl = document.getElementById('schema-config');
|
|
5
|
+
if (!cfgEl) return;
|
|
6
|
+
const cfg = JSON.parse(cfgEl.textContent);
|
|
7
|
+
const DESIGNER_TYPES = ['TEXT', 'INTEGER', 'REAL', 'BLOB', 'BOOLEAN', 'DATE', 'DATETIME'];
|
|
8
|
+
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
9
|
+
const t = encodeURIComponent(cfg.table);
|
|
10
|
+
|
|
11
|
+
function escapeHtml(s) {
|
|
12
|
+
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function showError(el, msg) {
|
|
16
|
+
el.classList.remove('hidden');
|
|
17
|
+
el.innerHTML = '<div class="flex items-start gap-2.5 rounded-xl border border-error/40 bg-error/10 px-4 py-3 text-sm font-medium text-error"><span>' + escapeHtml(msg) + '</span></div>';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---- Rename table ------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const renameInput = document.getElementById('table-rename-input');
|
|
23
|
+
const renameBtn = document.getElementById('rename-table');
|
|
24
|
+
|
|
25
|
+
async function renameTable() {
|
|
26
|
+
const name = (renameInput.value || '').trim();
|
|
27
|
+
if (!name) return UI.showError('Table name is required.');
|
|
28
|
+
if (name === cfg.table) return UI.showToast('Name unchanged.', 'info');
|
|
29
|
+
try {
|
|
30
|
+
const data = await Api.post('/api/tables/' + t + '/rename', { name });
|
|
31
|
+
UI.showToast(data.message || 'Table renamed.', 'success');
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
window.location.href = Api.basePath + '/tables/' + encodeURIComponent(name) + '/schema';
|
|
34
|
+
}, 400);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
UI.showError(e.message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (renameBtn) renameBtn.addEventListener('click', renameTable);
|
|
40
|
+
if (renameInput) renameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') renameTable(); });
|
|
41
|
+
|
|
42
|
+
// ---- Add column ---------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const addBtn = document.getElementById('add-column');
|
|
45
|
+
const panel = document.getElementById('add-column-panel');
|
|
46
|
+
const formEl = document.getElementById('add-column-form');
|
|
47
|
+
const errEl = document.getElementById('add-column-error');
|
|
48
|
+
const submitBtn = document.getElementById('add-column-submit');
|
|
49
|
+
const cancelBtn = document.getElementById('add-column-cancel');
|
|
50
|
+
let fkTables = [];
|
|
51
|
+
|
|
52
|
+
async function loadFkTables() {
|
|
53
|
+
try {
|
|
54
|
+
const tables = await Api.get('/api/tables');
|
|
55
|
+
fkTables = tables.map((x) => x.name).filter((n) => n.charAt(0) !== '_');
|
|
56
|
+
} catch (e) {
|
|
57
|
+
fkTables = [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildAddForm() {
|
|
62
|
+
formEl.innerHTML = '';
|
|
63
|
+
formEl.innerHTML = [
|
|
64
|
+
'<div class="col-span-2 sm:col-span-3"><label class="field-label">Name</label><input class="field-input ac-name" placeholder="column_name" autocomplete="off" spellcheck="false"></div>',
|
|
65
|
+
'<div class="col-span-1 sm:col-span-2"><label class="field-label">Type</label><select class="field-select ac-type">' + DESIGNER_TYPES.map((x) => '<option>' + x + '</option>').join('') + '</select></div>',
|
|
66
|
+
'<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
|
+
'<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
|
+
'<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">',
|
|
70
|
+
'<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
|
+
'<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
|
+
'</div>',
|
|
73
|
+
].join('');
|
|
74
|
+
|
|
75
|
+
const fkTableSel = formEl.querySelector('.ac-fk-table');
|
|
76
|
+
const fkColSel = formEl.querySelector('.ac-fk-col');
|
|
77
|
+
fkTableSel.addEventListener('change', async () => {
|
|
78
|
+
fkColSel.innerHTML = '<option value="">—</option>';
|
|
79
|
+
const target = fkTableSel.value;
|
|
80
|
+
if (!target) return;
|
|
81
|
+
try {
|
|
82
|
+
const info = await Api.get('/api/tables/' + encodeURIComponent(target) + '/info');
|
|
83
|
+
fkColSel.innerHTML = '<option value="">—</option>' + info.columns.map((c) => '<option>' + escapeHtml(c.name) + '</option>').join('');
|
|
84
|
+
} catch (e) { /* ignore */ }
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function addColumn() {
|
|
89
|
+
errEl.classList.add('hidden');
|
|
90
|
+
const name = (formEl.querySelector('.ac-name').value || '').trim();
|
|
91
|
+
if (!name) return showError(errEl, 'Column name is required.');
|
|
92
|
+
if (!NAME_RE.test(name)) return showError(errEl, 'Invalid column name: "' + name + '". Use letters, digits and underscores.');
|
|
93
|
+
const fkTable = formEl.querySelector('.ac-fk-table').value;
|
|
94
|
+
const fkCol = formEl.querySelector('.ac-fk-col').value;
|
|
95
|
+
const column = {
|
|
96
|
+
name,
|
|
97
|
+
type: formEl.querySelector('.ac-type').value,
|
|
98
|
+
notNull: formEl.querySelector('.ac-notnull').checked,
|
|
99
|
+
unique: formEl.querySelector('.ac-unique').checked,
|
|
100
|
+
defaultValue: formEl.querySelector('.ac-default').value.trim() || null,
|
|
101
|
+
foreignKey: fkTable && fkCol ? { table: fkTable, column: fkCol } : null,
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
const data = await Api.post('/api/tables/' + t + '/columns', { column });
|
|
105
|
+
UI.showToast(data.message || 'Column added.', 'success');
|
|
106
|
+
window.location.reload();
|
|
107
|
+
} catch (e) {
|
|
108
|
+
showError(errEl, e.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (addBtn) {
|
|
113
|
+
addBtn.addEventListener('click', () => {
|
|
114
|
+
panel.classList.toggle('hidden');
|
|
115
|
+
if (!panel.classList.contains('hidden')) {
|
|
116
|
+
errEl.classList.add('hidden');
|
|
117
|
+
buildAddForm();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (cancelBtn) cancelBtn.addEventListener('click', () => { panel.classList.add('hidden'); formEl.innerHTML = ''; });
|
|
122
|
+
if (submitBtn) submitBtn.addEventListener('click', addColumn);
|
|
123
|
+
|
|
124
|
+
// ---- Column rename (inline) ---------------------------------------------
|
|
125
|
+
|
|
126
|
+
function inlineRename(cell, oldName) {
|
|
127
|
+
const display = cell.querySelector('.name-display');
|
|
128
|
+
if (!display) return;
|
|
129
|
+
const input = document.createElement('input');
|
|
130
|
+
input.type = 'text';
|
|
131
|
+
input.value = oldName;
|
|
132
|
+
input.className = 'field-input !py-1 !text-[13px]';
|
|
133
|
+
input.spellcheck = false;
|
|
134
|
+
let done = false;
|
|
135
|
+
const restore = () => { if (display.parentNode) cell.replaceChild(display, input); };
|
|
136
|
+
const finish = async (save) => {
|
|
137
|
+
if (done) return;
|
|
138
|
+
done = true;
|
|
139
|
+
if (!save) return restore();
|
|
140
|
+
const newName = input.value.trim();
|
|
141
|
+
if (!newName || newName === oldName) return restore();
|
|
142
|
+
try {
|
|
143
|
+
await Api.put('/api/tables/' + t + '/columns/' + encodeURIComponent(oldName), { name: newName });
|
|
144
|
+
UI.showToast('Column renamed.', 'success');
|
|
145
|
+
window.location.reload();
|
|
146
|
+
} catch (e) {
|
|
147
|
+
UI.showError(e.message);
|
|
148
|
+
done = false;
|
|
149
|
+
restore();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
input.addEventListener('keydown', (e) => {
|
|
153
|
+
if (e.key === 'Enter') { e.preventDefault(); finish(true); }
|
|
154
|
+
else if (e.key === 'Escape') { e.preventDefault(); finish(false); }
|
|
155
|
+
});
|
|
156
|
+
input.addEventListener('blur', () => finish(false));
|
|
157
|
+
cell.replaceChild(input, display);
|
|
158
|
+
input.focus();
|
|
159
|
+
input.select();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
document.querySelectorAll('.col-rename').forEach((btn) => {
|
|
163
|
+
btn.addEventListener('click', () => {
|
|
164
|
+
const tr = btn.closest('tr');
|
|
165
|
+
if (!tr) return;
|
|
166
|
+
const cell = tr.querySelector('.col-name-cell');
|
|
167
|
+
if (cell) inlineRename(cell, cell.dataset.column);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ---- Drop column ---------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
document.querySelectorAll('.col-drop').forEach((btn) => {
|
|
174
|
+
btn.addEventListener('click', () => {
|
|
175
|
+
const tr = btn.closest('tr');
|
|
176
|
+
if (!tr) return;
|
|
177
|
+
const cell = tr.querySelector('.col-name-cell');
|
|
178
|
+
const name = cell.dataset.column;
|
|
179
|
+
if (!window.confirm('Drop column "' + name + '" permanently? This cannot be undone.')) return;
|
|
180
|
+
Api.del('/api/tables/' + t + '/columns/' + encodeURIComponent(name))
|
|
181
|
+
.then((data) => {
|
|
182
|
+
UI.showToast(data.message || 'Column dropped.', 'success');
|
|
183
|
+
window.location.reload();
|
|
184
|
+
})
|
|
185
|
+
.catch((e) => UI.showError(e.message));
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ---- Drop table -----------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
const dropBtn = document.getElementById('drop-table');
|
|
192
|
+
if (dropBtn) {
|
|
193
|
+
dropBtn.addEventListener('click', () => {
|
|
194
|
+
if (!window.confirm('Drop table "' + cfg.table + '" permanently? All of its data will be lost.')) return;
|
|
195
|
+
Api.del('/api/tables/' + t)
|
|
196
|
+
.then(() => {
|
|
197
|
+
UI.showToast('Table dropped.', 'success');
|
|
198
|
+
setTimeout(() => { window.location.href = Api.basePath + '/'; }, 400);
|
|
199
|
+
})
|
|
200
|
+
.catch((e) => UI.showError(e.message));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---- Index management ------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
const addIndexBtn = document.getElementById('add-index');
|
|
207
|
+
const addIndexPanel = document.getElementById('add-index-panel');
|
|
208
|
+
const indexNameInput = document.getElementById('index-name');
|
|
209
|
+
const indexUniqueInput = document.getElementById('index-unique');
|
|
210
|
+
const indexErrEl = document.getElementById('add-index-error');
|
|
211
|
+
const addIndexSubmit = document.getElementById('add-index-submit');
|
|
212
|
+
const addIndexCancel = document.getElementById('add-index-cancel');
|
|
213
|
+
const indexPreview = document.getElementById('index-preview');
|
|
214
|
+
const indexPreviewCode = document.getElementById('index-preview-code');
|
|
215
|
+
const indexColBtns = Array.prototype.slice.call(document.querySelectorAll('.index-col-btn'));
|
|
216
|
+
|
|
217
|
+
// Columns selected for the new index, in click order (index order matters!).
|
|
218
|
+
let indexColumns = [];
|
|
219
|
+
|
|
220
|
+
function renderIndexPreview() {
|
|
221
|
+
if (!indexPreview || !indexPreviewCode) return;
|
|
222
|
+
const cols = indexColumns.slice();
|
|
223
|
+
const name = (indexNameInput.value || '').trim();
|
|
224
|
+
const unique = indexUniqueInput && indexUniqueInput.checked ? 'UNIQUE ' : '';
|
|
225
|
+
if (!cols.length) {
|
|
226
|
+
indexPreview.classList.add('hidden');
|
|
227
|
+
indexPreviewCode.textContent = '';
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const finalName = name || 'idx_' + cfg.table + '_' + cols.join('_');
|
|
231
|
+
indexPreviewCode.textContent =
|
|
232
|
+
'CREATE ' + unique + 'INDEX "' + finalName + '" ON "' + cfg.table + '" (' +
|
|
233
|
+
cols.map((c) => '"' + c + '"').join(', ') + ');';
|
|
234
|
+
indexPreview.classList.remove('hidden');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function toggleIndexColumn(btn) {
|
|
238
|
+
const col = btn.dataset.column;
|
|
239
|
+
const i = indexColumns.indexOf(col);
|
|
240
|
+
const num = btn.querySelector('.index-col-num');
|
|
241
|
+
if (i !== -1) {
|
|
242
|
+
indexColumns.splice(i, 1);
|
|
243
|
+
btn.classList.remove('border-primary', 'bg-primary/10');
|
|
244
|
+
if (num) num.classList.add('hidden');
|
|
245
|
+
} else {
|
|
246
|
+
indexColumns.push(col);
|
|
247
|
+
btn.classList.add('border-primary', 'bg-primary/10');
|
|
248
|
+
if (num) num.classList.remove('hidden');
|
|
249
|
+
}
|
|
250
|
+
indexColumns.forEach((c, idx) => {
|
|
251
|
+
const b = indexColBtns.find((x) => x.dataset.column === c);
|
|
252
|
+
const n = b && b.querySelector('.index-col-num');
|
|
253
|
+
if (n) n.textContent = String(idx + 1);
|
|
254
|
+
});
|
|
255
|
+
renderIndexPreview();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
indexColBtns.forEach((btn) => btn.addEventListener('click', () => toggleIndexColumn(btn)));
|
|
259
|
+
if (indexNameInput) indexNameInput.addEventListener('input', renderIndexPreview);
|
|
260
|
+
if (indexUniqueInput) indexUniqueInput.addEventListener('change', renderIndexPreview);
|
|
261
|
+
|
|
262
|
+
if (addIndexBtn && addIndexPanel) {
|
|
263
|
+
addIndexBtn.addEventListener('click', () => {
|
|
264
|
+
addIndexPanel.classList.toggle('hidden');
|
|
265
|
+
if (!addIndexPanel.classList.contains('hidden')) {
|
|
266
|
+
indexErrEl.classList.add('hidden');
|
|
267
|
+
indexNameInput.focus();
|
|
268
|
+
renderIndexPreview();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (addIndexCancel) {
|
|
273
|
+
addIndexCancel.addEventListener('click', () => {
|
|
274
|
+
addIndexPanel.classList.add('hidden');
|
|
275
|
+
indexErrEl.classList.add('hidden');
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (addIndexSubmit) {
|
|
279
|
+
addIndexSubmit.addEventListener('click', async () => {
|
|
280
|
+
indexErrEl.classList.add('hidden');
|
|
281
|
+
const columns = indexColumns.slice();
|
|
282
|
+
if (!columns.length) return showError(indexErrEl, 'Select at least one column for the index.');
|
|
283
|
+
const body = { columns, unique: !!(indexUniqueInput && indexUniqueInput.checked) };
|
|
284
|
+
const name = (indexNameInput.value || '').trim();
|
|
285
|
+
if (name) body.name = name;
|
|
286
|
+
addIndexSubmit.disabled = true;
|
|
287
|
+
try {
|
|
288
|
+
const data = await Api.post('/api/tables/' + t + '/indexes', body);
|
|
289
|
+
UI.showToast(data.message || 'Index created.', 'success');
|
|
290
|
+
window.location.reload();
|
|
291
|
+
} catch (e) {
|
|
292
|
+
showError(indexErrEl, e.message);
|
|
293
|
+
} finally {
|
|
294
|
+
addIndexSubmit.disabled = false;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
document.querySelectorAll('.index-drop').forEach((btn) => {
|
|
300
|
+
btn.addEventListener('click', () => {
|
|
301
|
+
const name = btn.dataset.index;
|
|
302
|
+
if (!name) return;
|
|
303
|
+
if (!window.confirm('Drop index "' + name + '" permanently?')) return;
|
|
304
|
+
Api.del('/api/tables/' + t + '/indexes/' + encodeURIComponent(name))
|
|
305
|
+
.then((data) => {
|
|
306
|
+
UI.showToast(data.message || 'Index dropped.', 'success');
|
|
307
|
+
window.location.reload();
|
|
308
|
+
})
|
|
309
|
+
.catch((e) => UI.showError(e.message));
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
loadFkTables();
|
|
314
|
+
})();
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* Triggers page: create / drop triggers with a live SQL preview. */
|
|
2
|
+
(function () {
|
|
3
|
+
'use strict';
|
|
4
|
+
const cfgEl = document.getElementById('triggers-config');
|
|
5
|
+
if (!cfgEl) return;
|
|
6
|
+
|
|
7
|
+
function escapeHtml(s) {
|
|
8
|
+
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function showError(el, msg) {
|
|
12
|
+
el.classList.remove('hidden');
|
|
13
|
+
el.innerHTML = '<div class="flex items-start gap-2.5 rounded-xl border border-error/40 bg-error/10 px-4 py-3 text-sm font-medium text-error"><span>' + escapeHtml(msg) + '</span></div>';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ---- Editor gutter ------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const editor = document.getElementById('trigger-body');
|
|
19
|
+
const gutter = document.getElementById('trigger-gutter');
|
|
20
|
+
function updateGutter() {
|
|
21
|
+
if (!gutter || !editor) return;
|
|
22
|
+
const lines = editor.value.split('\n').length;
|
|
23
|
+
let html = '';
|
|
24
|
+
for (let i = 1; i <= lines; i++) html += i + '\n';
|
|
25
|
+
gutter.textContent = html;
|
|
26
|
+
gutter.scrollTop = editor.scrollTop;
|
|
27
|
+
}
|
|
28
|
+
if (editor && gutter) {
|
|
29
|
+
editor.addEventListener('scroll', () => { gutter.scrollTop = editor.scrollTop; });
|
|
30
|
+
editor.addEventListener('input', updateGutter);
|
|
31
|
+
updateGutter();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---- New trigger form ---------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const newBtn = document.getElementById('new-trigger');
|
|
37
|
+
const panel = document.getElementById('new-trigger-panel');
|
|
38
|
+
const nameInput = document.getElementById('trigger-name');
|
|
39
|
+
const tableInput = document.getElementById('trigger-table');
|
|
40
|
+
const timingInput = document.getElementById('trigger-timing');
|
|
41
|
+
const eventInput = document.getElementById('trigger-event');
|
|
42
|
+
const whenInput = document.getElementById('trigger-when');
|
|
43
|
+
const bodyInput = document.getElementById('trigger-body');
|
|
44
|
+
const errEl = document.getElementById('trigger-error');
|
|
45
|
+
const createBtn = document.getElementById('create-trigger');
|
|
46
|
+
const cancelBtn = document.getElementById('cancel-trigger');
|
|
47
|
+
const preview = document.getElementById('trigger-preview');
|
|
48
|
+
const previewCode = document.getElementById('trigger-preview-code');
|
|
49
|
+
const previewHint = document.getElementById('trigger-preview-hint');
|
|
50
|
+
let previewTimer = null;
|
|
51
|
+
|
|
52
|
+
function currentEvent() {
|
|
53
|
+
return eventInput ? eventInput.value : 'INSERT';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderPreview() {
|
|
57
|
+
clearTimeout(previewTimer);
|
|
58
|
+
const name = (nameInput.value || '').trim();
|
|
59
|
+
const table = tableInput.value || '';
|
|
60
|
+
const event = currentEvent();
|
|
61
|
+
if (!name && !table && !(bodyInput.value || '').trim()) {
|
|
62
|
+
preview.classList.add('hidden');
|
|
63
|
+
previewCode.textContent = '';
|
|
64
|
+
if (previewHint) previewHint.classList.remove('hidden');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
previewTimer = setTimeout(async () => {
|
|
68
|
+
try {
|
|
69
|
+
const data = await Api.post('/api/triggers/generate', {
|
|
70
|
+
name,
|
|
71
|
+
table,
|
|
72
|
+
timing: timingInput.value,
|
|
73
|
+
event,
|
|
74
|
+
when: (whenInput.value || '').trim() || undefined,
|
|
75
|
+
body: bodyInput.value,
|
|
76
|
+
});
|
|
77
|
+
previewCode.textContent = data.sql;
|
|
78
|
+
preview.classList.remove('hidden');
|
|
79
|
+
if (previewHint) previewHint.classList.add('hidden');
|
|
80
|
+
} catch (e) {
|
|
81
|
+
preview.classList.add('hidden');
|
|
82
|
+
previewCode.textContent = '';
|
|
83
|
+
if (previewHint) previewHint.classList.remove('hidden');
|
|
84
|
+
}
|
|
85
|
+
}, 250);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (newBtn && panel) {
|
|
89
|
+
newBtn.addEventListener('click', () => {
|
|
90
|
+
panel.classList.toggle('hidden');
|
|
91
|
+
if (!panel.classList.contains('hidden')) {
|
|
92
|
+
errEl.classList.add('hidden');
|
|
93
|
+
nameInput.focus();
|
|
94
|
+
renderPreview();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
[nameInput, tableInput, timingInput, eventInput, whenInput, bodyInput].forEach((el) => {
|
|
99
|
+
if (el) el.addEventListener('input', renderPreview);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (cancelBtn) {
|
|
103
|
+
cancelBtn.addEventListener('click', () => {
|
|
104
|
+
panel.classList.add('hidden');
|
|
105
|
+
errEl.classList.add('hidden');
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (createBtn) {
|
|
110
|
+
createBtn.addEventListener('click', async () => {
|
|
111
|
+
errEl.classList.add('hidden');
|
|
112
|
+
const name = (nameInput.value || '').trim();
|
|
113
|
+
const table = tableInput.value || '';
|
|
114
|
+
const body = (bodyInput.value || '').trim();
|
|
115
|
+
const event = currentEvent();
|
|
116
|
+
if (!name) return showError(errEl, 'Trigger name is required.');
|
|
117
|
+
if (!table) return showError(errEl, 'Select a table for the trigger.');
|
|
118
|
+
if (!body) return showError(errEl, 'Trigger body is required.');
|
|
119
|
+
createBtn.disabled = true;
|
|
120
|
+
try {
|
|
121
|
+
const data = await Api.post('/api/triggers', {
|
|
122
|
+
name,
|
|
123
|
+
table,
|
|
124
|
+
timing: timingInput.value,
|
|
125
|
+
event,
|
|
126
|
+
when: (whenInput.value || '').trim() || undefined,
|
|
127
|
+
body,
|
|
128
|
+
});
|
|
129
|
+
UI.showToast(data.message || 'Trigger created.', 'success');
|
|
130
|
+
window.location.reload();
|
|
131
|
+
} catch (e) {
|
|
132
|
+
showError(errEl, e.message);
|
|
133
|
+
} finally {
|
|
134
|
+
createBtn.disabled = false;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---- Drop trigger -------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
document.querySelectorAll('.trigger-drop').forEach((btn) => {
|
|
142
|
+
btn.addEventListener('click', async () => {
|
|
143
|
+
const name = btn.dataset.trigger;
|
|
144
|
+
if (!name) return;
|
|
145
|
+
if (!window.confirm('Drop trigger "' + name + '" permanently?')) return;
|
|
146
|
+
try {
|
|
147
|
+
const data = await Api.del('/api/triggers/' + encodeURIComponent(name));
|
|
148
|
+
UI.showToast(data.message || 'Trigger dropped.', 'success');
|
|
149
|
+
window.location.reload();
|
|
150
|
+
} catch (e) {
|
|
151
|
+
UI.showError(e.message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
})();
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* Views page: create / drop views, preview rows. */
|
|
2
|
+
(function () {
|
|
3
|
+
'use strict';
|
|
4
|
+
const cfgEl = document.getElementById('views-config');
|
|
5
|
+
if (!cfgEl) return;
|
|
6
|
+
|
|
7
|
+
function escapeHtml(s) {
|
|
8
|
+
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function showError(el, msg) {
|
|
12
|
+
el.classList.remove('hidden');
|
|
13
|
+
el.innerHTML = '<div class="flex items-start gap-2.5 rounded-xl border border-error/40 bg-error/10 px-4 py-3 text-sm font-medium text-error"><span>' + escapeHtml(msg) + '</span></div>';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ---- Editor gutter ------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const editor = document.getElementById('view-sql');
|
|
19
|
+
const gutter = document.getElementById('view-gutter');
|
|
20
|
+
function updateGutter() {
|
|
21
|
+
if (!gutter || !editor) return;
|
|
22
|
+
const lines = editor.value.split('\n').length;
|
|
23
|
+
let html = '';
|
|
24
|
+
for (let i = 1; i <= lines; i++) html += i + '\n';
|
|
25
|
+
gutter.textContent = html;
|
|
26
|
+
gutter.scrollTop = editor.scrollTop;
|
|
27
|
+
}
|
|
28
|
+
if (editor && gutter) {
|
|
29
|
+
editor.addEventListener('scroll', () => { gutter.scrollTop = editor.scrollTop; });
|
|
30
|
+
editor.addEventListener('input', updateGutter);
|
|
31
|
+
updateGutter();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---- New view form ------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const newBtn = document.getElementById('new-view');
|
|
37
|
+
const panel = document.getElementById('new-view-panel');
|
|
38
|
+
const nameInput = document.getElementById('view-name');
|
|
39
|
+
const sqlInput = document.getElementById('view-sql');
|
|
40
|
+
const errEl = document.getElementById('view-error');
|
|
41
|
+
const createBtn = document.getElementById('create-view');
|
|
42
|
+
const cancelBtn = document.getElementById('cancel-view');
|
|
43
|
+
const preview = document.getElementById('view-preview');
|
|
44
|
+
const previewCode = document.getElementById('view-preview-code');
|
|
45
|
+
const previewHint = document.getElementById('view-preview-hint');
|
|
46
|
+
let previewTimer = null;
|
|
47
|
+
|
|
48
|
+
function renderPreview() {
|
|
49
|
+
clearTimeout(previewTimer);
|
|
50
|
+
const name = (nameInput.value || '').trim();
|
|
51
|
+
const sql = (sqlInput.value || '').trim();
|
|
52
|
+
if (!name && !sql) {
|
|
53
|
+
preview.classList.add('hidden');
|
|
54
|
+
previewCode.textContent = '';
|
|
55
|
+
if (previewHint) previewHint.classList.remove('hidden');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
previewTimer = setTimeout(async () => {
|
|
59
|
+
try {
|
|
60
|
+
const data = await Api.post('/api/views/generate', { name, sql });
|
|
61
|
+
previewCode.textContent = data.sql;
|
|
62
|
+
preview.classList.remove('hidden');
|
|
63
|
+
if (previewHint) previewHint.classList.add('hidden');
|
|
64
|
+
} catch (e) {
|
|
65
|
+
preview.classList.add('hidden');
|
|
66
|
+
previewCode.textContent = '';
|
|
67
|
+
if (previewHint) previewHint.classList.remove('hidden');
|
|
68
|
+
}
|
|
69
|
+
}, 250);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (newBtn && panel) {
|
|
73
|
+
newBtn.addEventListener('click', () => {
|
|
74
|
+
panel.classList.toggle('hidden');
|
|
75
|
+
if (!panel.classList.contains('hidden')) {
|
|
76
|
+
errEl.classList.add('hidden');
|
|
77
|
+
nameInput.focus();
|
|
78
|
+
renderPreview();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (nameInput) nameInput.addEventListener('input', renderPreview);
|
|
83
|
+
if (sqlInput) sqlInput.addEventListener('input', renderPreview);
|
|
84
|
+
|
|
85
|
+
if (cancelBtn) {
|
|
86
|
+
cancelBtn.addEventListener('click', () => {
|
|
87
|
+
panel.classList.add('hidden');
|
|
88
|
+
errEl.classList.add('hidden');
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (createBtn) {
|
|
93
|
+
createBtn.addEventListener('click', async () => {
|
|
94
|
+
errEl.classList.add('hidden');
|
|
95
|
+
const name = (nameInput.value || '').trim();
|
|
96
|
+
const sql = (sqlInput.value || '').trim();
|
|
97
|
+
if (!name) return showError(errEl, 'View name is required.');
|
|
98
|
+
if (!sql) return showError(errEl, 'A SELECT statement is required.');
|
|
99
|
+
createBtn.disabled = true;
|
|
100
|
+
try {
|
|
101
|
+
const data = await Api.post('/api/views', { name, sql });
|
|
102
|
+
UI.showToast(data.message || 'View created.', 'success');
|
|
103
|
+
window.location.reload();
|
|
104
|
+
} catch (e) {
|
|
105
|
+
showError(errEl, e.message);
|
|
106
|
+
} finally {
|
|
107
|
+
createBtn.disabled = false;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- Preview rows -------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
document.querySelectorAll('.view-browse').forEach((btn) => {
|
|
115
|
+
btn.addEventListener('click', async () => {
|
|
116
|
+
const name = btn.dataset.view;
|
|
117
|
+
const tr = btn.closest('tr');
|
|
118
|
+
const previewTr = tr && tr.nextElementSibling;
|
|
119
|
+
if (!previewTr) return;
|
|
120
|
+
const container = previewTr.querySelector('td > div');
|
|
121
|
+
if (!container) return;
|
|
122
|
+
if (!previewTr.classList.contains('hidden')) {
|
|
123
|
+
previewTr.classList.add('hidden');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
container.innerHTML = '<p class="text-sm text-base-content/45">Loading…</p>';
|
|
127
|
+
try {
|
|
128
|
+
const data = await Api.get('/api/views/' + encodeURIComponent(name) + '/rows');
|
|
129
|
+
const rows = data.rows || [];
|
|
130
|
+
if (!rows.length) {
|
|
131
|
+
container.innerHTML = '<p class="text-sm text-base-content/45">View returned no rows.</p>';
|
|
132
|
+
previewTr.classList.remove('hidden');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const cols = Object.keys(rows[0]);
|
|
136
|
+
let html = '<div class="overflow-x-auto rounded-lg border border-base-200"><table class="table table-xs">' +
|
|
137
|
+
'<thead><tr class="bg-base-200/70">' + cols.map((c) => '<th class="font-mono text-[11px]">' + escapeHtml(c) + '</th>').join('') + '</tr></thead><tbody>';
|
|
138
|
+
const shown = rows.slice(0, 50);
|
|
139
|
+
for (const row of shown) {
|
|
140
|
+
html += '<tr>' + cols.map((c) => {
|
|
141
|
+
const v = row[c];
|
|
142
|
+
const s = v == null ? '' : String(v);
|
|
143
|
+
const display = v == null ? '<span class="text-base-content/30">NULL</span>' : (s.length > 120 ? escapeHtml(s.slice(0, 120)) + '…' : escapeHtml(s));
|
|
144
|
+
return '<td class="max-w-xs truncate font-mono text-[12px]">' + display + '</td>';
|
|
145
|
+
}).join('') + '</tr>';
|
|
146
|
+
}
|
|
147
|
+
html += '</tbody></table></div>';
|
|
148
|
+
if (rows.length > shown.length) {
|
|
149
|
+
html += '<p class="mt-2 text-xs text-base-content/45">Showing first ' + shown.length + ' of ' + rows.length + ' row(s).</p>';
|
|
150
|
+
}
|
|
151
|
+
container.innerHTML = html;
|
|
152
|
+
previewTr.classList.remove('hidden');
|
|
153
|
+
} catch (e) {
|
|
154
|
+
container.innerHTML = '<p class="text-sm font-medium text-error">' + escapeHtml(e.message) + '</p>';
|
|
155
|
+
previewTr.classList.remove('hidden');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ---- Drop view ----------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
document.querySelectorAll('.view-drop').forEach((btn) => {
|
|
163
|
+
btn.addEventListener('click', async () => {
|
|
164
|
+
const name = btn.dataset.view;
|
|
165
|
+
if (!name) return;
|
|
166
|
+
if (!window.confirm('Drop view "' + name + '" permanently?')) return;
|
|
167
|
+
try {
|
|
168
|
+
const data = await Api.del('/api/views/' + encodeURIComponent(name));
|
|
169
|
+
UI.showToast(data.message || 'View dropped.', 'success');
|
|
170
|
+
window.location.reload();
|
|
171
|
+
} catch (e) {
|
|
172
|
+
UI.showError(e.message);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
})();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Router } from 'express';
|
|
2
|
+
import type { SqliteDatabase } from '../db/database';
|
|
3
|
+
import type { Logger } from '../logger';
|
|
4
|
+
interface ApiContext {
|
|
5
|
+
db: SqliteDatabase;
|
|
6
|
+
logger: Logger;
|
|
7
|
+
}
|
|
8
|
+
export declare function registerApi(router: Router, ctx: ApiContext): void;
|
|
9
|
+
export {};
|