admindb 1.0.1 → 1.0.3

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.
@@ -1,244 +1,244 @@
1
- /* Dynamic insert/edit row form.
2
- * The form is built entirely from column metadata returned by the API, so the
3
- * same page handles every table. Supports run mode and "get query" preview. */
4
- (function () {
5
- 'use strict';
6
- const cfgEl = document.getElementById('form-config');
7
- if (!cfgEl) return;
8
-
9
- const cfg = JSON.parse(cfgEl.textContent);
10
- const form = document.getElementById('row-fields');
11
- const errorBox = document.getElementById('form-error');
12
- const sqlBox = document.getElementById('generated-sql');
13
- const sqlCode = document.getElementById('generated-sql-code');
14
- const submitBtn = document.getElementById('row-submit');
15
- const generateBtn = document.getElementById('row-generate');
16
- const copyBtn = document.getElementById('copy-sql');
17
- const t = encodeURIComponent(cfg.table);
18
-
19
- let info = null;
20
- let record = null;
21
- let fkOptions = {};
22
-
23
- function escapeHtml(s) {
24
- return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
25
- }
26
-
27
- function toLocalInput(v) {
28
- const m = String(v).match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})/);
29
- if (m) return m[1] + 'T' + m[2] + ':' + m[3];
30
- return v;
31
- }
32
-
33
- function toLocalNow() {
34
- const d = new Date();
35
- const p = (n) => String(n).padStart(2, '0');
36
- return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + 'T' + p(d.getHours()) + ':' + p(d.getMinutes());
37
- }
38
-
39
- /**
40
- * Convert a column's raw default expression (dflt_value) into a form-friendly
41
- * value. Handles literals we can read safely; anything complex returns null
42
- * (the DB applies the real default when the field is left empty).
43
- */
44
- function defaultToValue(col, dflt) {
45
- if (dflt === null || dflt === undefined) return null;
46
- const s = String(dflt).trim();
47
- if (s === '' || /^null$/i.test(s)) return null;
48
-
49
- const type = (col.type || '').toUpperCase();
50
-
51
- // Single-quoted string literal, e.g. 'N/A', 'user'
52
- const str = s.match(/^'(.*)'$/s);
53
- if (str) return str[1].replace(/''/g, "'");
54
-
55
- // Numeric literal, e.g. 0, 42, 3.14
56
- if (/^[-+]?\d+(\.\d+)?$/.test(s)) return Number(s);
57
-
58
- // Boolean literal
59
- if (/^(true|false)$/i.test(s)) return /^true$/i.test(s);
60
-
61
- // BOOLEAN columns stored as 0/1
62
- if (type === 'BOOLEAN') {
63
- if (s === '1' || /^true$/i.test(s)) return 1;
64
- if (s === '0' || /^false$/i.test(s)) return 0;
65
- }
66
-
67
- // current-timestamp style defaults → "now" for date/datetime columns
68
- if ((type === 'DATE' || type === 'DATETIME') && /current_timestamp|now\s*\(|datetime\s*\(|date\s*\(/i.test(s)) {
69
- return toLocalNow();
70
- }
71
-
72
- return null;
73
- }
74
-
75
- /** Value to pre-fill a control with: edit → the record's value; insert → the column default. */
76
- function initialValue(col) {
77
- if (cfg.mode === 'edit' && record) return record[col.name];
78
- if (cfg.mode === 'insert') return defaultToValue(col, col.dflt_value);
79
- return null;
80
- }
81
-
82
- function isTruthy(v) {
83
- return v === 1 || v === true || v === '1' || v === 'true';
84
- }
85
-
86
- function renderField(col) {
87
- const isPk = info.primaryKey.indexOf(col.name) !== -1;
88
- const wrapper = document.createElement('div');
89
- wrapper.className = 'space-y-1.5';
90
-
91
- const label = document.createElement('label');
92
- label.className = 'field-label';
93
- label.innerHTML =
94
- '<span>' + escapeHtml(col.name) + '</span>' +
95
- (col.notnull ? '<span class="text-error">*</span>' : '') +
96
- (isPk ? '<span class="chip chip-pk">PK</span>' : '') +
97
- (col.type ? '<span class="chip chip-type">' + escapeHtml(col.type) + '</span>' : '') +
98
- (cfg.mode === 'insert' && col.dflt_value ? '<span class="chip chip-type">default</span>' : '');
99
- wrapper.appendChild(label);
100
-
101
- const fk = info.foreignKeys.find((f) => f.from === col.name);
102
- const type = (col.type || '').toUpperCase();
103
- const isBool = type === 'BOOLEAN';
104
- const isNum = type.startsWith('INTEGER') || type === 'REAL';
105
- const isBlob = type === 'BLOB';
106
- const isDate = type === 'DATE' || type === 'DATETIME';
107
- const iv = initialValue(col);
108
-
109
- let control;
110
- if (isBool) {
111
- const boolWrap = document.createElement('label');
112
- boolWrap.className = 'flex cursor-pointer items-center gap-2';
113
- control = document.createElement('input');
114
- control.type = 'checkbox';
115
- control.className = 'field-check';
116
- control.dataset.column = col.name;
117
- if (isTruthy(iv)) control.checked = true;
118
- const span = document.createElement('span');
119
- span.className = 'text-sm text-base-content/60';
120
- span.textContent = 'Enabled';
121
- boolWrap.appendChild(control);
122
- boolWrap.appendChild(span);
123
- wrapper.appendChild(boolWrap);
124
- return wrapper;
125
- } else if (fk) {
126
- control = document.createElement('select');
127
- control.className = 'field-select';
128
- control.dataset.column = col.name;
129
- const opts = fkOptions[col.name] || [];
130
- const empty = document.createElement('option');
131
- empty.value = '';
132
- empty.textContent = '(not set)';
133
- control.appendChild(empty);
134
- opts.forEach((o) => {
135
- const opt = document.createElement('option');
136
- opt.value = String(o.value);
137
- opt.textContent = o.label === '' ? String(o.value) : o.label;
138
- if (iv != null && String(iv) === String(o.value)) opt.selected = true;
139
- control.appendChild(opt);
140
- });
141
- } else if (isDate) {
142
- control = document.createElement('input');
143
- control.type = 'datetime-local';
144
- control.className = 'field-input';
145
- control.dataset.column = col.name;
146
- if (iv != null) control.value = toLocalInput(String(iv));
147
- } else if (isNum) {
148
- control = document.createElement('input');
149
- control.type = 'number';
150
- control.step = type === 'REAL' ? 'any' : '1';
151
- control.className = 'field-input';
152
- control.dataset.column = col.name;
153
- if (iv != null) control.value = String(iv);
154
- } else {
155
- control = document.createElement('input');
156
- control.type = 'text';
157
- control.className = 'field-input';
158
- control.placeholder = isBlob ? '0x… hex or text' : '';
159
- control.dataset.column = col.name;
160
- if (iv != null) control.value = String(iv);
161
- }
162
- if (isPk && cfg.mode === 'edit') control.disabled = true;
163
- wrapper.appendChild(control);
164
- return wrapper;
165
- }
166
-
167
- function render() {
168
- form.innerHTML = '';
169
- info.columns.forEach((col) => form.appendChild(renderField(col)));
170
- }
171
-
172
- function collect() {
173
- const values = {};
174
- form.querySelectorAll('[data-column]').forEach((el) => {
175
- const col = el.dataset.column;
176
- if (el.type === 'checkbox') values[col] = el.checked ? 'true' : '';
177
- else values[col] = el.value;
178
- });
179
- return values;
180
- }
181
-
182
- function showSql(sql) {
183
- sqlCode.textContent = sql;
184
- sqlBox.classList.remove('hidden');
185
- sqlBox.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
186
- }
187
-
188
- async function submit(runMode) {
189
- errorBox.classList.add('hidden');
190
- const values = collect();
191
- const edit = cfg.mode === 'edit';
192
- let path;
193
- if (edit) {
194
- path = '/api/tables/' + t + '/row/' + encodeURIComponent(cfg.pk) + (runMode ? '' : '/generate');
195
- } else {
196
- path = '/api/tables/' + t + '/rows' + (runMode ? '' : '/generate');
197
- }
198
- const method = edit ? 'PUT' : 'POST';
199
- try {
200
- const data = await Api.request(method, path, { values });
201
- if (runMode) {
202
- UI.showToast(data.message || 'Saved.', 'success');
203
- setTimeout(() => {
204
- window.location.href = Api.basePath + '/tables/' + encodeURIComponent(cfg.table);
205
- }, 500);
206
- } else {
207
- showSql(data.sql);
208
- }
209
- } catch (e) {
210
- errorBox.classList.remove('hidden');
211
- errorBox.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(e.message) + '</span></div>';
212
- }
213
- }
214
-
215
- async function init() {
216
- try {
217
- info = await Api.get('/api/tables/' + t + '/info');
218
- if (cfg.mode === 'edit') {
219
- record = await Api.get('/api/tables/' + t + '/row/' + encodeURIComponent(cfg.pk));
220
- }
221
- try {
222
- fkOptions = await Api.get('/api/tables/' + t + '/fk-options');
223
- } catch (e) {
224
- fkOptions = {};
225
- }
226
- render();
227
- } catch (e) {
228
- errorBox.classList.remove('hidden');
229
- errorBox.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(e.message) + '</span></div>';
230
- }
231
- }
232
-
233
- if (submitBtn) submitBtn.addEventListener('click', () => submit(true));
234
- if (generateBtn) generateBtn.addEventListener('click', () => submit(false));
235
- if (copyBtn) {
236
- copyBtn.addEventListener('click', () => {
237
- navigator.clipboard.writeText(sqlCode.textContent)
238
- .then(() => UI.showToast('Copied to clipboard.', 'success'))
239
- .catch(() => UI.showError('Could not copy.'));
240
- });
241
- }
242
-
243
- init();
244
- })();
1
+ /* Dynamic insert/edit row form.
2
+ * The form is built entirely from column metadata returned by the API, so the
3
+ * same page handles every table. Supports run mode and "get query" preview. */
4
+ (function () {
5
+ 'use strict';
6
+ const cfgEl = document.getElementById('form-config');
7
+ if (!cfgEl) return;
8
+
9
+ const cfg = JSON.parse(cfgEl.textContent);
10
+ const form = document.getElementById('row-fields');
11
+ const errorBox = document.getElementById('form-error');
12
+ const sqlBox = document.getElementById('generated-sql');
13
+ const sqlCode = document.getElementById('generated-sql-code');
14
+ const submitBtn = document.getElementById('row-submit');
15
+ const generateBtn = document.getElementById('row-generate');
16
+ const copyBtn = document.getElementById('copy-sql');
17
+ const t = encodeURIComponent(cfg.table);
18
+
19
+ let info = null;
20
+ let record = null;
21
+ let fkOptions = {};
22
+
23
+ function escapeHtml(s) {
24
+ return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
25
+ }
26
+
27
+ function toLocalInput(v) {
28
+ const m = String(v).match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}):(\d{2})/);
29
+ if (m) return m[1] + 'T' + m[2] + ':' + m[3];
30
+ return v;
31
+ }
32
+
33
+ function toLocalNow() {
34
+ const d = new Date();
35
+ const p = (n) => String(n).padStart(2, '0');
36
+ return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + 'T' + p(d.getHours()) + ':' + p(d.getMinutes());
37
+ }
38
+
39
+ /**
40
+ * Convert a column's raw default expression (dflt_value) into a form-friendly
41
+ * value. Handles literals we can read safely; anything complex returns null
42
+ * (the DB applies the real default when the field is left empty).
43
+ */
44
+ function defaultToValue(col, dflt) {
45
+ if (dflt === null || dflt === undefined) return null;
46
+ const s = String(dflt).trim();
47
+ if (s === '' || /^null$/i.test(s)) return null;
48
+
49
+ const type = (col.type || '').toUpperCase();
50
+
51
+ // Single-quoted string literal, e.g. 'N/A', 'user'
52
+ const str = s.match(/^'(.*)'$/s);
53
+ if (str) return str[1].replace(/''/g, "'");
54
+
55
+ // Numeric literal, e.g. 0, 42, 3.14
56
+ if (/^[-+]?\d+(\.\d+)?$/.test(s)) return Number(s);
57
+
58
+ // Boolean literal
59
+ if (/^(true|false)$/i.test(s)) return /^true$/i.test(s);
60
+
61
+ // BOOLEAN columns stored as 0/1
62
+ if (type === 'BOOLEAN') {
63
+ if (s === '1' || /^true$/i.test(s)) return 1;
64
+ if (s === '0' || /^false$/i.test(s)) return 0;
65
+ }
66
+
67
+ // current-timestamp style defaults → "now" for date/datetime columns
68
+ if ((type === 'DATE' || type === 'DATETIME') && /current_timestamp|now\s*\(|datetime\s*\(|date\s*\(/i.test(s)) {
69
+ return toLocalNow();
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ /** Value to pre-fill a control with: edit → the record's value; insert → the column default. */
76
+ function initialValue(col) {
77
+ if (cfg.mode === 'edit' && record) return record[col.name];
78
+ if (cfg.mode === 'insert') return defaultToValue(col, col.dflt_value);
79
+ return null;
80
+ }
81
+
82
+ function isTruthy(v) {
83
+ return v === 1 || v === true || v === '1' || v === 'true';
84
+ }
85
+
86
+ function renderField(col) {
87
+ const isPk = info.primaryKey.indexOf(col.name) !== -1;
88
+ const wrapper = document.createElement('div');
89
+ wrapper.className = 'space-y-1.5';
90
+
91
+ const label = document.createElement('label');
92
+ label.className = 'field-label';
93
+ label.innerHTML =
94
+ '<span>' + escapeHtml(col.name) + '</span>' +
95
+ (col.notnull ? '<span class="text-error">*</span>' : '') +
96
+ (isPk ? '<span class="chip chip-pk">PK</span>' : '') +
97
+ (col.type ? '<span class="chip chip-type">' + escapeHtml(col.type) + '</span>' : '') +
98
+ (cfg.mode === 'insert' && col.dflt_value ? '<span class="chip chip-type">default</span>' : '');
99
+ wrapper.appendChild(label);
100
+
101
+ const fk = info.foreignKeys.find((f) => f.from === col.name);
102
+ const type = (col.type || '').toUpperCase();
103
+ const isBool = type === 'BOOLEAN';
104
+ const isNum = type.startsWith('INTEGER') || type === 'REAL';
105
+ const isBlob = type === 'BLOB';
106
+ const isDate = type === 'DATE' || type === 'DATETIME';
107
+ const iv = initialValue(col);
108
+
109
+ let control;
110
+ if (isBool) {
111
+ const boolWrap = document.createElement('label');
112
+ boolWrap.className = 'flex cursor-pointer items-center gap-2';
113
+ control = document.createElement('input');
114
+ control.type = 'checkbox';
115
+ control.className = 'field-check';
116
+ control.dataset.column = col.name;
117
+ if (isTruthy(iv)) control.checked = true;
118
+ const span = document.createElement('span');
119
+ span.className = 'text-sm text-base-content/60';
120
+ span.textContent = 'Enabled';
121
+ boolWrap.appendChild(control);
122
+ boolWrap.appendChild(span);
123
+ wrapper.appendChild(boolWrap);
124
+ return wrapper;
125
+ } else if (fk) {
126
+ control = document.createElement('select');
127
+ control.className = 'field-select';
128
+ control.dataset.column = col.name;
129
+ const opts = fkOptions[col.name] || [];
130
+ const empty = document.createElement('option');
131
+ empty.value = '';
132
+ empty.textContent = '(not set)';
133
+ control.appendChild(empty);
134
+ opts.forEach((o) => {
135
+ const opt = document.createElement('option');
136
+ opt.value = String(o.value);
137
+ opt.textContent = o.label === '' ? String(o.value) : o.label;
138
+ if (iv != null && String(iv) === String(o.value)) opt.selected = true;
139
+ control.appendChild(opt);
140
+ });
141
+ } else if (isDate) {
142
+ control = document.createElement('input');
143
+ control.type = 'datetime-local';
144
+ control.className = 'field-input';
145
+ control.dataset.column = col.name;
146
+ if (iv != null) control.value = toLocalInput(String(iv));
147
+ } else if (isNum) {
148
+ control = document.createElement('input');
149
+ control.type = 'number';
150
+ control.step = type === 'REAL' ? 'any' : '1';
151
+ control.className = 'field-input';
152
+ control.dataset.column = col.name;
153
+ if (iv != null) control.value = String(iv);
154
+ } else {
155
+ control = document.createElement('input');
156
+ control.type = 'text';
157
+ control.className = 'field-input';
158
+ control.placeholder = isBlob ? '0x… hex or text' : '';
159
+ control.dataset.column = col.name;
160
+ if (iv != null) control.value = String(iv);
161
+ }
162
+ if (isPk && cfg.mode === 'edit') control.disabled = true;
163
+ wrapper.appendChild(control);
164
+ return wrapper;
165
+ }
166
+
167
+ function render() {
168
+ form.innerHTML = '';
169
+ info.columns.forEach((col) => form.appendChild(renderField(col)));
170
+ }
171
+
172
+ function collect() {
173
+ const values = {};
174
+ form.querySelectorAll('[data-column]').forEach((el) => {
175
+ const col = el.dataset.column;
176
+ if (el.type === 'checkbox') values[col] = el.checked ? 'true' : '';
177
+ else values[col] = el.value;
178
+ });
179
+ return values;
180
+ }
181
+
182
+ function showSql(sql) {
183
+ sqlCode.textContent = sql;
184
+ sqlBox.classList.remove('hidden');
185
+ sqlBox.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
186
+ }
187
+
188
+ async function submit(runMode) {
189
+ errorBox.classList.add('hidden');
190
+ const values = collect();
191
+ const edit = cfg.mode === 'edit';
192
+ let path;
193
+ if (edit) {
194
+ path = '/api/tables/' + t + '/row/' + encodeURIComponent(cfg.pk) + (runMode ? '' : '/generate');
195
+ } else {
196
+ path = '/api/tables/' + t + '/rows' + (runMode ? '' : '/generate');
197
+ }
198
+ const method = edit ? 'PUT' : 'POST';
199
+ try {
200
+ const data = await Api.request(method, path, { values });
201
+ if (runMode) {
202
+ UI.showToast(data.message || 'Saved.', 'success');
203
+ setTimeout(() => {
204
+ window.location.href = Api.basePath + '/tables/' + encodeURIComponent(cfg.table);
205
+ }, 500);
206
+ } else {
207
+ showSql(data.sql);
208
+ }
209
+ } catch (e) {
210
+ errorBox.classList.remove('hidden');
211
+ errorBox.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(e.message) + '</span></div>';
212
+ }
213
+ }
214
+
215
+ async function init() {
216
+ try {
217
+ info = await Api.get('/api/tables/' + t + '/info');
218
+ if (cfg.mode === 'edit') {
219
+ record = await Api.get('/api/tables/' + t + '/row/' + encodeURIComponent(cfg.pk));
220
+ }
221
+ try {
222
+ fkOptions = await Api.get('/api/tables/' + t + '/fk-options');
223
+ } catch (e) {
224
+ fkOptions = {};
225
+ }
226
+ render();
227
+ } catch (e) {
228
+ errorBox.classList.remove('hidden');
229
+ errorBox.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(e.message) + '</span></div>';
230
+ }
231
+ }
232
+
233
+ if (submitBtn) submitBtn.addEventListener('click', () => submit(true));
234
+ if (generateBtn) generateBtn.addEventListener('click', () => submit(false));
235
+ if (copyBtn) {
236
+ copyBtn.addEventListener('click', () => {
237
+ navigator.clipboard.writeText(sqlCode.textContent)
238
+ .then(() => UI.showToast('Copied to clipboard.', 'success'))
239
+ .catch(() => UI.showError('Could not copy.'));
240
+ });
241
+ }
242
+
243
+ init();
244
+ })();