admindb 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +354 -325
  3. package/dist/db/database.d.ts +61 -39
  4. package/dist/db/database.js +251 -51
  5. package/dist/db/database.js.map +1 -1
  6. package/dist/db/manager.d.ts +2 -2
  7. package/dist/db/manager.js +3 -2
  8. package/dist/db/manager.js.map +1 -1
  9. package/dist/public/css/app.css +1 -1
  10. package/dist/public/css/input.css +232 -179
  11. package/dist/public/js/api.js +47 -47
  12. package/dist/public/js/app.js +120 -120
  13. package/dist/public/js/browse.js +877 -123
  14. package/dist/public/js/databases.js +45 -45
  15. package/dist/public/js/designer.js +205 -205
  16. package/dist/public/js/forms.js +244 -244
  17. package/dist/public/js/query.js +232 -232
  18. package/dist/public/js/schema.js +314 -314
  19. package/dist/routes/api.js +261 -97
  20. package/dist/routes/api.js.map +1 -1
  21. package/dist/routes/pages.js +50 -30
  22. package/dist/routes/pages.js.map +1 -1
  23. package/dist/sql/generator.d.ts +0 -22
  24. package/dist/sql/generator.js +0 -48
  25. package/dist/sql/generator.js.map +1 -1
  26. package/dist/test/database.test.js +241 -63
  27. package/dist/test/database.test.js.map +1 -1
  28. package/dist/test/generator.test.js +0 -32
  29. package/dist/test/generator.test.js.map +1 -1
  30. package/dist/util.d.ts +13 -1
  31. package/dist/util.js +24 -2
  32. package/dist/util.js.map +1 -1
  33. package/dist/views/layouts/main.hbs +49 -49
  34. package/dist/views/pages/databases.hbs +102 -102
  35. package/dist/views/pages/designer.hbs +81 -81
  36. package/dist/views/pages/error.hbs +12 -12
  37. package/dist/views/pages/form.hbs +62 -62
  38. package/dist/views/pages/home.hbs +123 -123
  39. package/dist/views/pages/query.hbs +94 -94
  40. package/dist/views/pages/schema.hbs +290 -290
  41. package/dist/views/pages/table.hbs +256 -203
  42. package/dist/views/partials/navbar.hbs +50 -50
  43. package/dist/views/partials/sidebar.hbs +116 -128
  44. package/package.json +66 -62
  45. package/dist/public/js/triggers.js +0 -155
  46. package/dist/public/js/views.js +0 -176
  47. package/dist/views/pages/triggers.hbs +0 -162
  48. package/dist/views/pages/views.hbs +0 -134
@@ -1,45 +1,45 @@
1
- /* Databases landing page: create and delete SQLite database files. */
2
- (function () {
3
- 'use strict';
4
- const nameInput = document.getElementById('new-db-name');
5
- const createBtn = document.getElementById('create-db');
6
-
7
- function escapeHtml(s) {
8
- return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
9
- }
10
-
11
- async function createDb() {
12
- const name = (nameInput.value || '').trim();
13
- if (!name) return UI.showError('Enter a database name.');
14
- try {
15
- const data = await Api.post('/api/databases', { name });
16
- UI.showToast(data.message || 'Database created.', 'success');
17
- setTimeout(() => {
18
- window.location.href = Api.basePath + '/' + encodeURIComponent(data.id);
19
- }, 400);
20
- } catch (e) {
21
- UI.showError(e.message);
22
- }
23
- }
24
-
25
- if (createBtn) createBtn.addEventListener('click', createDb);
26
- if (nameInput) {
27
- nameInput.addEventListener('keydown', (e) => {
28
- if (e.key === 'Enter') createDb();
29
- });
30
- }
31
-
32
- document.querySelectorAll('.js-delete-db').forEach((btn) => {
33
- btn.addEventListener('click', () => {
34
- const id = btn.dataset.id;
35
- if (!id) return;
36
- if (!window.confirm('Delete database "' + id + '" permanently? This removes the file and all of its data.')) return;
37
- Api.del('/api/databases/' + encodeURIComponent(id))
38
- .then((data) => {
39
- UI.showToast(data.message || 'Database deleted.', 'success');
40
- window.location.reload();
41
- })
42
- .catch((e) => UI.showError(e.message));
43
- });
44
- });
45
- })();
1
+ /* Databases landing page: create and delete SQLite database files. */
2
+ (function () {
3
+ 'use strict';
4
+ const nameInput = document.getElementById('new-db-name');
5
+ const createBtn = document.getElementById('create-db');
6
+
7
+ function escapeHtml(s) {
8
+ return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
9
+ }
10
+
11
+ async function createDb() {
12
+ const name = (nameInput.value || '').trim();
13
+ if (!name) return UI.showError('Enter a database name.');
14
+ try {
15
+ const data = await Api.post('/api/databases', { name });
16
+ UI.showToast(data.message || 'Database created.', 'success');
17
+ setTimeout(() => {
18
+ window.location.href = Api.basePath + '/' + encodeURIComponent(data.id);
19
+ }, 400);
20
+ } catch (e) {
21
+ UI.showError(e.message);
22
+ }
23
+ }
24
+
25
+ if (createBtn) createBtn.addEventListener('click', createDb);
26
+ if (nameInput) {
27
+ nameInput.addEventListener('keydown', (e) => {
28
+ if (e.key === 'Enter') createDb();
29
+ });
30
+ }
31
+
32
+ document.querySelectorAll('.js-delete-db').forEach((btn) => {
33
+ btn.addEventListener('click', () => {
34
+ const id = btn.dataset.id;
35
+ if (!id) return;
36
+ if (!window.confirm('Delete database "' + id + '" permanently? This removes the file and all of its data.')) return;
37
+ Api.del('/api/databases/' + encodeURIComponent(id))
38
+ .then((data) => {
39
+ UI.showToast(data.message || 'Database deleted.', 'success');
40
+ window.location.reload();
41
+ })
42
+ .catch((e) => UI.showError(e.message));
43
+ });
44
+ });
45
+ })();
@@ -1,205 +1,205 @@
1
- /* Visual table designer: add column definitions one at a time, validate them,
2
- * and preview the generated CREATE TABLE live. Create executes; Copy copies. */
3
- (function () {
4
- 'use strict';
5
- const DESIGNER_TYPES = ['TEXT', 'INTEGER', 'REAL', 'BLOB', 'BOOLEAN', 'DATE', 'DATETIME'];
6
- const NAME_RE = /^[A-Za-z_][A-Za-z0-9_$]*$/;
7
-
8
- const columnsEl = document.getElementById('columns');
9
- const errorBox = document.getElementById('designer-error');
10
- const tableNameEl = document.getElementById('table-name');
11
- const addBtn = document.getElementById('add-column');
12
- const createBtn = document.getElementById('create-table');
13
- const copyBtn = document.getElementById('copy-sql');
14
- const previewBox = document.getElementById('sql-preview');
15
- const previewCode = document.getElementById('preview-code');
16
- const previewPlaceholder = document.getElementById('preview-placeholder');
17
- const previewStatus = document.getElementById('preview-status');
18
-
19
- const columnCache = {};
20
- let fkTables = [];
21
- let previewTimer = null;
22
-
23
- function escapeHtml(s) {
24
- return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
25
- }
26
-
27
- async function loadColumns(table) {
28
- if (columnCache[table]) return columnCache[table];
29
- const info = await Api.get('/api/tables/' + encodeURIComponent(table) + '/info');
30
- columnCache[table] = info.columns.map((c) => c.name);
31
- return columnCache[table];
32
- }
33
-
34
- function addColumnRow() {
35
- const row = document.createElement('div');
36
- 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';
37
- row.innerHTML = [
38
- '<div class="col-span-2 sm:col-span-2"><label class="field-label">Name</label>',
39
- '<input class="field-input col-name" placeholder="column_name" autocomplete="off" spellcheck="false"></div>',
40
- '<div class="col-span-1 sm:col-span-2"><label class="field-label">Type</label>',
41
- '<select class="field-select col-type">' + DESIGNER_TYPES.map((t) => '<option>' + t + '</option>').join('') + '</select></div>',
42
- '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK table</label>',
43
- '<select class="field-select col-fk-table"><option value="">— none —</option>' +
44
- fkTables.map((t) => '<option>' + escapeHtml(t) + '</option>').join('') + '</select></div>',
45
- '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK column</label>',
46
- '<select class="field-select col-fk-col"><option value="">—</option></select></div>',
47
- '<div class="col-span-1 sm:col-span-2"><label class="field-label">Default</label>',
48
- '<input class="field-input col-default" placeholder="0, CURRENT_TIMESTAMP" autocomplete="off" spellcheck="false"></div>',
49
- '<div class="col-span-2 flex items-end justify-between gap-1 pb-0.5 sm:col-span-2">',
50
- '<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>',
51
- '<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>',
52
- '<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>',
53
- '<button type="button" class="btn btn-ghost btn-xs text-error remove-col" title="Remove column">&times;</button>',
54
- '</div>',
55
- ].join('');
56
- columnsEl.appendChild(row);
57
-
58
- const fkTableSel = row.querySelector('.col-fk-table');
59
- const fkColSel = row.querySelector('.col-fk-col');
60
- fkTableSel.addEventListener('change', () => {
61
- fkColSel.innerHTML = '<option value="">—</option>';
62
- const target = fkTableSel.value;
63
- if (!target) return;
64
- loadColumns(target)
65
- .then((cols) => {
66
- fkColSel.innerHTML = '<option value="">—</option>' + cols.map((c) => '<option>' + escapeHtml(c) + '</option>').join('');
67
- })
68
- .catch(() => {});
69
- });
70
- row.querySelector('.remove-col').addEventListener('click', () => row.remove());
71
-
72
- row.querySelectorAll('input, select').forEach((el) => {
73
- el.addEventListener('input', schedulePreview);
74
- el.addEventListener('change', schedulePreview);
75
- });
76
- }
77
-
78
- function collectColumns() {
79
- const cols = [];
80
- document.querySelectorAll('#columns > div').forEach((row) => {
81
- const name = row.querySelector('.col-name').value.trim();
82
- if (!name) return;
83
- const fkTable = row.querySelector('.col-fk-table').value;
84
- const fkCol = row.querySelector('.col-fk-col').value;
85
- cols.push({
86
- name,
87
- type: row.querySelector('.col-type').value,
88
- primaryKey: row.querySelector('.col-pk').checked,
89
- notNull: row.querySelector('.col-notnull').checked,
90
- unique: row.querySelector('.col-unique').checked,
91
- defaultValue: row.querySelector('.col-default').value.trim() || null,
92
- foreignKey: fkTable && fkCol ? { table: fkTable, column: fkCol } : null,
93
- });
94
- });
95
- return cols;
96
- }
97
-
98
- function validate() {
99
- const name = tableNameEl.value.trim();
100
- const cols = collectColumns();
101
- if (!name) return 'Table name is required.';
102
- if (!NAME_RE.test(name)) return 'Invalid table name: "' + name + '". Use letters, digits and underscores.';
103
- if (!cols.length) return 'Add at least one column.';
104
- let pkCount = 0;
105
- const seen = {};
106
- for (const c of cols) {
107
- if (!NAME_RE.test(c.name)) return 'Invalid column name: "' + c.name + '".';
108
- if (seen[c.name]) return 'Duplicate column name: "' + c.name + '".';
109
- seen[c.name] = true;
110
- if (c.primaryKey) pkCount += 1;
111
- if (c.foreignKey && (!c.foreignKey.table || !c.foreignKey.column)) {
112
- return 'Column "' + c.name + '" has an incomplete foreign key (choose both table and column).';
113
- }
114
- }
115
- if (pkCount > 1) return 'At most one column can be a primary key.';
116
- return null;
117
- }
118
-
119
- function showError(msg) {
120
- errorBox.classList.remove('hidden');
121
- errorBox.innerHTML = '<div class="alert alert-error shadow-lg"><span>' + escapeHtml(msg) + '</span></div>';
122
- }
123
-
124
- function schedulePreview() {
125
- clearTimeout(previewTimer);
126
- previewTimer = setTimeout(runPreview, 250);
127
- }
128
-
129
- async function runPreview() {
130
- const err = validate();
131
- if (err) {
132
- errorBox.classList.add('hidden');
133
- previewBox.classList.add('hidden');
134
- previewPlaceholder.classList.remove('hidden');
135
- if (previewStatus) previewStatus.textContent = 'fix validation errors…';
136
- return;
137
- }
138
- errorBox.classList.add('hidden');
139
- try {
140
- const data = await Api.post('/api/tables/generate', {
141
- name: tableNameEl.value.trim(),
142
- columns: collectColumns(),
143
- });
144
- previewCode.textContent = data.sql;
145
- previewBox.classList.remove('hidden');
146
- previewPlaceholder.classList.add('hidden');
147
- if (previewStatus) {
148
- previewStatus.textContent = 'live preview · not executed';
149
- previewStatus.className = 'text-[11px] font-medium text-accent';
150
- }
151
- } catch (e) {
152
- previewBox.classList.add('hidden');
153
- previewPlaceholder.classList.remove('hidden');
154
- showError(e.message);
155
- }
156
- }
157
-
158
- async function submit() {
159
- errorBox.classList.add('hidden');
160
- const err = validate();
161
- if (err) {
162
- showError(err);
163
- return;
164
- }
165
- const body = { name: tableNameEl.value.trim(), columns: collectColumns() };
166
- try {
167
- const data = await Api.post('/api/tables', body);
168
- UI.showToast(data.message || 'Table created.', 'success');
169
- setTimeout(() => {
170
- window.location.href = Api.basePath + '/tables/' + encodeURIComponent(body.name);
171
- }, 500);
172
- } catch (e) {
173
- showError(e.message);
174
- }
175
- }
176
-
177
- async function init() {
178
- try {
179
- const tables = await Api.get('/api/tables');
180
- fkTables = tables.map((t) => t.name).filter((n) => n.charAt(0) !== '_');
181
- } catch (e) {
182
- fkTables = [];
183
- }
184
- addColumnRow();
185
- addColumnRow();
186
- tableNameEl.addEventListener('input', schedulePreview);
187
- }
188
-
189
- if (addBtn) addBtn.addEventListener('click', addColumnRow);
190
- if (createBtn) createBtn.addEventListener('click', submit);
191
- if (copyBtn) {
192
- copyBtn.addEventListener('click', () => {
193
- const text = previewCode.textContent || '';
194
- if (!text) {
195
- UI.showError('Nothing to copy yet.');
196
- return;
197
- }
198
- navigator.clipboard.writeText(text)
199
- .then(() => UI.showToast('SQL copied to clipboard.', 'success'))
200
- .catch(() => UI.showError('Could not copy.'));
201
- });
202
- }
203
-
204
- init();
205
- })();
1
+ /* Visual table designer: add column definitions one at a time, validate them,
2
+ * and preview the generated CREATE TABLE live. Create executes; Copy copies. */
3
+ (function () {
4
+ 'use strict';
5
+ const DESIGNER_TYPES = ['TEXT', 'INTEGER', 'REAL', 'BLOB', 'BOOLEAN', 'DATE', 'DATETIME'];
6
+ const NAME_RE = /^[A-Za-z_][A-Za-z0-9_$]*$/;
7
+
8
+ const columnsEl = document.getElementById('columns');
9
+ const errorBox = document.getElementById('designer-error');
10
+ const tableNameEl = document.getElementById('table-name');
11
+ const addBtn = document.getElementById('add-column');
12
+ const createBtn = document.getElementById('create-table');
13
+ const copyBtn = document.getElementById('copy-sql');
14
+ const previewBox = document.getElementById('sql-preview');
15
+ const previewCode = document.getElementById('preview-code');
16
+ const previewPlaceholder = document.getElementById('preview-placeholder');
17
+ const previewStatus = document.getElementById('preview-status');
18
+
19
+ const columnCache = {};
20
+ let fkTables = [];
21
+ let previewTimer = null;
22
+
23
+ function escapeHtml(s) {
24
+ return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
25
+ }
26
+
27
+ async function loadColumns(table) {
28
+ if (columnCache[table]) return columnCache[table];
29
+ const info = await Api.get('/api/tables/' + encodeURIComponent(table) + '/info');
30
+ columnCache[table] = info.columns.map((c) => c.name);
31
+ return columnCache[table];
32
+ }
33
+
34
+ function addColumnRow() {
35
+ const row = document.createElement('div');
36
+ 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';
37
+ row.innerHTML = [
38
+ '<div class="col-span-2 sm:col-span-2"><label class="field-label">Name</label>',
39
+ '<input class="field-input col-name" placeholder="column_name" autocomplete="off" spellcheck="false"></div>',
40
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">Type</label>',
41
+ '<select class="field-select col-type">' + DESIGNER_TYPES.map((t) => '<option>' + t + '</option>').join('') + '</select></div>',
42
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK table</label>',
43
+ '<select class="field-select col-fk-table"><option value="">— none —</option>' +
44
+ fkTables.map((t) => '<option>' + escapeHtml(t) + '</option>').join('') + '</select></div>',
45
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK column</label>',
46
+ '<select class="field-select col-fk-col"><option value="">—</option></select></div>',
47
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">Default</label>',
48
+ '<input class="field-input col-default" placeholder="0, CURRENT_TIMESTAMP" autocomplete="off" spellcheck="false"></div>',
49
+ '<div class="col-span-2 flex items-end justify-between gap-1 pb-0.5 sm:col-span-2">',
50
+ '<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>',
51
+ '<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>',
52
+ '<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>',
53
+ '<button type="button" class="btn btn-ghost btn-xs text-error remove-col" title="Remove column">&times;</button>',
54
+ '</div>',
55
+ ].join('');
56
+ columnsEl.appendChild(row);
57
+
58
+ const fkTableSel = row.querySelector('.col-fk-table');
59
+ const fkColSel = row.querySelector('.col-fk-col');
60
+ fkTableSel.addEventListener('change', () => {
61
+ fkColSel.innerHTML = '<option value="">—</option>';
62
+ const target = fkTableSel.value;
63
+ if (!target) return;
64
+ loadColumns(target)
65
+ .then((cols) => {
66
+ fkColSel.innerHTML = '<option value="">—</option>' + cols.map((c) => '<option>' + escapeHtml(c) + '</option>').join('');
67
+ })
68
+ .catch(() => {});
69
+ });
70
+ row.querySelector('.remove-col').addEventListener('click', () => row.remove());
71
+
72
+ row.querySelectorAll('input, select').forEach((el) => {
73
+ el.addEventListener('input', schedulePreview);
74
+ el.addEventListener('change', schedulePreview);
75
+ });
76
+ }
77
+
78
+ function collectColumns() {
79
+ const cols = [];
80
+ document.querySelectorAll('#columns > div').forEach((row) => {
81
+ const name = row.querySelector('.col-name').value.trim();
82
+ if (!name) return;
83
+ const fkTable = row.querySelector('.col-fk-table').value;
84
+ const fkCol = row.querySelector('.col-fk-col').value;
85
+ cols.push({
86
+ name,
87
+ type: row.querySelector('.col-type').value,
88
+ primaryKey: row.querySelector('.col-pk').checked,
89
+ notNull: row.querySelector('.col-notnull').checked,
90
+ unique: row.querySelector('.col-unique').checked,
91
+ defaultValue: row.querySelector('.col-default').value.trim() || null,
92
+ foreignKey: fkTable && fkCol ? { table: fkTable, column: fkCol } : null,
93
+ });
94
+ });
95
+ return cols;
96
+ }
97
+
98
+ function validate() {
99
+ const name = tableNameEl.value.trim();
100
+ const cols = collectColumns();
101
+ if (!name) return 'Table name is required.';
102
+ if (!NAME_RE.test(name)) return 'Invalid table name: "' + name + '". Use letters, digits and underscores.';
103
+ if (!cols.length) return 'Add at least one column.';
104
+ let pkCount = 0;
105
+ const seen = {};
106
+ for (const c of cols) {
107
+ if (!NAME_RE.test(c.name)) return 'Invalid column name: "' + c.name + '".';
108
+ if (seen[c.name]) return 'Duplicate column name: "' + c.name + '".';
109
+ seen[c.name] = true;
110
+ if (c.primaryKey) pkCount += 1;
111
+ if (c.foreignKey && (!c.foreignKey.table || !c.foreignKey.column)) {
112
+ return 'Column "' + c.name + '" has an incomplete foreign key (choose both table and column).';
113
+ }
114
+ }
115
+ if (pkCount > 1) return 'At most one column can be a primary key.';
116
+ return null;
117
+ }
118
+
119
+ function showError(msg) {
120
+ errorBox.classList.remove('hidden');
121
+ errorBox.innerHTML = '<div class="alert alert-error shadow-lg"><span>' + escapeHtml(msg) + '</span></div>';
122
+ }
123
+
124
+ function schedulePreview() {
125
+ clearTimeout(previewTimer);
126
+ previewTimer = setTimeout(runPreview, 250);
127
+ }
128
+
129
+ async function runPreview() {
130
+ const err = validate();
131
+ if (err) {
132
+ errorBox.classList.add('hidden');
133
+ previewBox.classList.add('hidden');
134
+ previewPlaceholder.classList.remove('hidden');
135
+ if (previewStatus) previewStatus.textContent = 'fix validation errors…';
136
+ return;
137
+ }
138
+ errorBox.classList.add('hidden');
139
+ try {
140
+ const data = await Api.post('/api/tables/generate', {
141
+ name: tableNameEl.value.trim(),
142
+ columns: collectColumns(),
143
+ });
144
+ previewCode.textContent = data.sql;
145
+ previewBox.classList.remove('hidden');
146
+ previewPlaceholder.classList.add('hidden');
147
+ if (previewStatus) {
148
+ previewStatus.textContent = 'live preview · not executed';
149
+ previewStatus.className = 'text-[11px] font-medium text-accent';
150
+ }
151
+ } catch (e) {
152
+ previewBox.classList.add('hidden');
153
+ previewPlaceholder.classList.remove('hidden');
154
+ showError(e.message);
155
+ }
156
+ }
157
+
158
+ async function submit() {
159
+ errorBox.classList.add('hidden');
160
+ const err = validate();
161
+ if (err) {
162
+ showError(err);
163
+ return;
164
+ }
165
+ const body = { name: tableNameEl.value.trim(), columns: collectColumns() };
166
+ try {
167
+ const data = await Api.post('/api/tables', body);
168
+ UI.showToast(data.message || 'Table created.', 'success');
169
+ setTimeout(() => {
170
+ window.location.href = Api.basePath + '/tables/' + encodeURIComponent(body.name);
171
+ }, 500);
172
+ } catch (e) {
173
+ showError(e.message);
174
+ }
175
+ }
176
+
177
+ async function init() {
178
+ try {
179
+ const tables = await Api.get('/api/tables');
180
+ fkTables = tables.map((t) => t.name).filter((n) => n.charAt(0) !== '_');
181
+ } catch (e) {
182
+ fkTables = [];
183
+ }
184
+ addColumnRow();
185
+ addColumnRow();
186
+ tableNameEl.addEventListener('input', schedulePreview);
187
+ }
188
+
189
+ if (addBtn) addBtn.addEventListener('click', addColumnRow);
190
+ if (createBtn) createBtn.addEventListener('click', submit);
191
+ if (copyBtn) {
192
+ copyBtn.addEventListener('click', () => {
193
+ const text = previewCode.textContent || '';
194
+ if (!text) {
195
+ UI.showError('Nothing to copy yet.');
196
+ return;
197
+ }
198
+ navigator.clipboard.writeText(text)
199
+ .then(() => UI.showToast('SQL copied to clipboard.', 'success'))
200
+ .catch(() => UI.showError('Could not copy.'));
201
+ });
202
+ }
203
+
204
+ init();
205
+ })();