admindb 1.1.0 → 1.2.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.
Files changed (41) hide show
  1. package/README.md +276 -228
  2. package/dist/app.d.ts +12 -0
  3. package/dist/app.js +7 -0
  4. package/dist/app.js.map +1 -1
  5. package/dist/args.d.ts +28 -0
  6. package/dist/args.js +187 -0
  7. package/dist/args.js.map +1 -0
  8. package/dist/cli.js +85 -15
  9. package/dist/cli.js.map +1 -1
  10. package/dist/data/generator.d.ts +86 -0
  11. package/dist/data/generator.js +1031 -0
  12. package/dist/data/generator.js.map +1 -0
  13. package/dist/db/database.d.ts +4 -0
  14. package/dist/db/database.js +28 -1
  15. package/dist/db/database.js.map +1 -1
  16. package/dist/db/manager.d.ts +6 -0
  17. package/dist/db/manager.js +31 -0
  18. package/dist/db/manager.js.map +1 -1
  19. package/dist/public/css/app.css +1 -1
  20. package/dist/public/css/input.css +9 -2
  21. package/dist/public/js/browse.js +8 -1
  22. package/dist/public/js/databases.js +99 -1
  23. package/dist/public/js/seed.js +377 -0
  24. package/dist/routes/api.js +62 -0
  25. package/dist/routes/api.js.map +1 -1
  26. package/dist/routes/databases.d.ts +10 -0
  27. package/dist/routes/databases.js +97 -0
  28. package/dist/routes/databases.js.map +1 -1
  29. package/dist/routes/pages.js +24 -0
  30. package/dist/routes/pages.js.map +1 -1
  31. package/dist/util.d.ts +5 -0
  32. package/dist/util.js +13 -0
  33. package/dist/util.js.map +1 -1
  34. package/dist/views/layouts/main.hbs +1 -1
  35. package/dist/views/pages/databases.hbs +41 -0
  36. package/dist/views/pages/query.hbs +2 -2
  37. package/dist/views/pages/seed.hbs +86 -0
  38. package/dist/views/pages/table.hbs +5 -0
  39. package/dist/views/partials/navbar.hbs +1 -6
  40. package/dist/views/partials/sidebar.hbs +1 -6
  41. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
- /* Databases landing page: create and delete SQLite database files. */
1
+ /* Databases landing page: create/delete database files, and a filesystem
2
+ * browser for opening existing SQLite files. */
2
3
  (function () {
3
4
  'use strict';
4
5
  const nameInput = document.getElementById('new-db-name');
@@ -8,6 +9,18 @@
8
9
  return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
9
10
  }
10
11
 
12
+ function fmtSize(n) {
13
+ if (!Number.isFinite(n) || n <= 0) return '0 B';
14
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
15
+ let v = n;
16
+ let u = 0;
17
+ while (v >= 1024 && u < units.length - 1) {
18
+ v /= 1024;
19
+ u += 1;
20
+ }
21
+ return (u === 0 ? Math.round(v) : v.toFixed(1)) + ' ' + units[u];
22
+ }
23
+
11
24
  async function createDb() {
12
25
  const name = (nameInput.value || '').trim();
13
26
  if (!name) return UI.showError('Enter a database name.');
@@ -42,4 +55,89 @@
42
55
  .catch((e) => UI.showError(e.message));
43
56
  });
44
57
  });
58
+
59
+ // ---- Filesystem browser --------------------------------------------------
60
+
61
+ const fsPathInput = document.getElementById('fs-path');
62
+ const fsGoBtn = document.getElementById('fs-go');
63
+ const fsUpBtn = document.getElementById('fs-up');
64
+ const fsCurrent = document.getElementById('fs-current');
65
+ const fsListing = document.getElementById('fs-listing');
66
+
67
+ const FOLDER_ICON =
68
+ '<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" /></svg>';
69
+ const DB_ICON =
70
+ '<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 7v10c0 1.1.9 2 2 2h12a2 2 0 002-2V7M4 7c0-1.1.9-2 2-2h12a2 2 0 012 2M4 7c0 1.1.9 2 2 2h12a2 2 0 002-2" /></svg>';
71
+
72
+ let fsState = { path: '', parent: null };
73
+
74
+ function renderFs(data) {
75
+ fsState = { path: data.path, parent: data.parent };
76
+ if (fsCurrent) fsCurrent.textContent = data.path;
77
+ if (fsPathInput) fsPathInput.value = data.path;
78
+ if (fsUpBtn) fsUpBtn.disabled = !data.parent;
79
+ if (!fsListing) return;
80
+ fsListing.innerHTML = '';
81
+ if (!data.entries.length) {
82
+ fsListing.innerHTML = '<div class="px-4 py-6 text-center text-sm text-base-content/40">No folders or database files here.</div>';
83
+ return;
84
+ }
85
+ data.entries.forEach((e) => {
86
+ const row = document.createElement('button');
87
+ row.type = 'button';
88
+ row.className = 'flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm transition hover:bg-primary/[0.06]';
89
+ row.title = e.path;
90
+ if (e.isDir) {
91
+ row.innerHTML =
92
+ '<span class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-base-200 text-base-content/50">' + FOLDER_ICON + '</span>' +
93
+ '<span class="min-w-0 flex-1 truncate font-medium text-base-content/80">' + escapeHtml(e.name) + '</span>' +
94
+ '<span class="text-xs text-base-content/35">folder</span>';
95
+ row.addEventListener('click', () => loadFs(e.path));
96
+ } else {
97
+ row.innerHTML =
98
+ '<span class="grid h-6 w-6 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">' + DB_ICON + '</span>' +
99
+ '<span class="min-w-0 flex-1 truncate font-mono text-[13px] text-base-content/85">' + escapeHtml(e.name) + '</span>' +
100
+ '<span class="text-xs text-base-content/40">' + fmtSize(e.size) + '</span>';
101
+ row.addEventListener('click', () => openDb(e.path));
102
+ }
103
+ fsListing.appendChild(row);
104
+ });
105
+ }
106
+
107
+ async function loadFs(p) {
108
+ try {
109
+ const data = await Api.get('/api/fs/list' + (p ? '?path=' + encodeURIComponent(p) : ''));
110
+ renderFs(data);
111
+ } catch (e) {
112
+ UI.showError(e.message);
113
+ }
114
+ }
115
+
116
+ function goUp() {
117
+ if (fsState.parent) loadFs(fsState.parent);
118
+ }
119
+
120
+ async function openDb(p) {
121
+ try {
122
+ const data = await Api.post('/api/databases/open', { path: p });
123
+ UI.showToast(data.message || 'Database opened.', 'success');
124
+ window.location.href = Api.basePath + '/' + encodeURIComponent(data.id);
125
+ } catch (e) {
126
+ UI.showError(e.message);
127
+ }
128
+ }
129
+
130
+ if (fsListing) {
131
+ loadFs('');
132
+ if (fsGoBtn) {
133
+ fsGoBtn.addEventListener('click', () => loadFs((fsPathInput.value || '').trim() || ''));
134
+ }
135
+ if (fsPathInput) {
136
+ fsPathInput.addEventListener('keydown', (e) => {
137
+ if (e.key === 'Enter') loadFs((fsPathInput.value || '').trim() || '');
138
+ });
139
+ }
140
+ if (fsUpBtn) fsUpBtn.addEventListener('click', goUp);
141
+ }
45
142
  })();
143
+
@@ -0,0 +1,377 @@
1
+ /* "Seed data" page.
2
+ *
3
+ * Lets you generate N realistic rows for a table. Each column gets a strategy
4
+ * (auto-detected from its name/type/foreign key, but overridable): skip,
5
+ * set NULL, fixed value, random from list, or any of the built-in fake-data
6
+ * generators (first/last name, email, phone, UUID, random int/date/…), plus
7
+ * "random from referenced table" for foreign-key columns.
8
+ *
9
+ * "Preview SQL" generates the rows and shows the INSERT statements without
10
+ * executing them; "Insert rows" writes them inside a single transaction.
11
+ */
12
+ (function () {
13
+ 'use strict';
14
+
15
+ const cfgEl = document.getElementById('seed-config');
16
+ if (!cfgEl) return;
17
+
18
+ const cfg = JSON.parse(cfgEl.textContent);
19
+ const t = encodeURIComponent(cfg.table);
20
+ const columns = cfg.columns || [];
21
+
22
+ const countInput = document.getElementById('seed-count');
23
+ const columnsBox = document.getElementById('seed-columns');
24
+ const feedback = document.getElementById('seed-feedback');
25
+ const sqlBox = document.getElementById('seed-sql');
26
+ const sqlCode = document.getElementById('seed-sql-code');
27
+ const copyBtn = document.getElementById('seed-copy');
28
+ const insertBtn = document.getElementById('seed-insert');
29
+ const previewBtn = document.getElementById('seed-preview');
30
+ const resetBtn = document.getElementById('seed-reset');
31
+ const applyAllBtn = document.getElementById('seed-apply-all');
32
+
33
+ function escapeHtml(s) {
34
+ return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
35
+ }
36
+
37
+ // Per-column live state: { strategy, ...optionValues }.
38
+ const state = {};
39
+
40
+ // ---- Option field definitions per strategy -----------------------------
41
+
42
+ function field(key, label, type, attrs) {
43
+ return Object.assign({ key, label, type }, attrs || {});
44
+ }
45
+ const FIELD_DEFS = {
46
+ fixed: [field('value', 'Value', 'text')],
47
+ list: [field('values', 'One value per line', 'textarea')],
48
+ int: [field('min', 'Min', 'number'), field('max', 'Max', 'number')],
49
+ decimal: [field('min', 'Min', 'number'), field('max', 'Max', 'number'), field('precision', 'Decimals', 'number', { step: 1 })],
50
+ date: [field('from', 'From', 'date'), field('to', 'To', 'date')],
51
+ datetime: [field('from', 'From', 'datetime-local'), field('to', 'To', 'datetime-local')],
52
+ time: [],
53
+ words: [field('minLen', 'Min words', 'number'), field('maxLen', 'Max words', 'number')],
54
+ sentence: [field('minLen', 'Min', 'number'), field('maxLen', 'Max', 'number')],
55
+ bytes: [field('minLen', 'Min bytes', 'number'), field('maxLen', 'Max bytes', 'number')],
56
+ json: [field('jsonKeys', 'JSON keys (comma-separated)', 'text')],
57
+ hash: [field('minLen', 'Min length', 'number'), field('maxLen', 'Max length', 'number')],
58
+ token: [field('minLen', 'Min length', 'number'), field('maxLen', 'Max length', 'number')],
59
+ };
60
+
61
+ function strategyNote(col, strategy) {
62
+ if (strategy === 'fk' && col.fk) return 'Values are sampled from ' + escapeHtml(col.fk.table) + (col.fk.to ? '.' + escapeHtml(col.fk.to) : '') + '.';
63
+ if (strategy === 'skip') return 'Column is omitted — the DB default (or NULL) applies.';
64
+ if (strategy === 'null') return 'Column is explicitly set to NULL.';
65
+ if (strategy === 'json') return 'Generates a valid JSON object — needed when the column has a JSON index or JSON default.';
66
+ if (strategy === 'hash') return 'Generates a random hexadecimal hash.';
67
+ if (strategy === 'token') return 'Generates a random short token / code.';
68
+ return '';
69
+ }
70
+
71
+ // ---- Card building ------------------------------------------------------
72
+
73
+ function buildOptionInputs(container, col, plan) {
74
+ container.innerHTML = '';
75
+ const fields = FIELD_DEFS[plan.strategy] || [];
76
+ if (!fields.length) return;
77
+ const grid = document.createElement('div');
78
+ grid.className = 'grid grid-cols-2 gap-2';
79
+ fields.forEach((f) => {
80
+ const wrap = document.createElement('label');
81
+ wrap.className = 'block';
82
+ const label = document.createElement('span');
83
+ label.className = 'field-label !mb-1 !text-[11px]';
84
+ label.textContent = f.label;
85
+ wrap.appendChild(label);
86
+ let el;
87
+ if (f.type === 'textarea') {
88
+ el = document.createElement('textarea');
89
+ el.rows = 4;
90
+ el.className = 'field-input col-span-2 w-full font-mono !text-[12px]';
91
+ el.placeholder = 'one value per line';
92
+ el.spellcheck = false;
93
+ if (Array.isArray(plan.values)) el.value = plan.values.join('\n');
94
+ } else {
95
+ el = document.createElement('input');
96
+ el.type = f.type;
97
+ el.className = 'field-input !py-1.5 !text-[13px]';
98
+ if (f.step != null) el.step = String(f.step);
99
+ const v = plan[f.key];
100
+ if (f.key === 'jsonKeys' && Array.isArray(v)) {
101
+ el.value = v.join(', ');
102
+ } else if (v !== undefined && v !== null) {
103
+ el.value = String(v);
104
+ }
105
+ }
106
+ el.dataset.optionKey = f.key;
107
+ el.addEventListener('input', () => {
108
+ const key = el.dataset.optionKey;
109
+ if (f.type === 'textarea') {
110
+ state[col.name][key] = el.value.split('\n').map((s) => s.trim()).filter(Boolean);
111
+ } else if (key === 'jsonKeys') {
112
+ state[col.name][key] = el.value.split(/[,;]/).map((s) => s.trim()).filter(Boolean);
113
+ } else if (key === 'value' || key === 'from' || key === 'to') {
114
+ state[col.name][key] = el.value;
115
+ } else {
116
+ const n = Number(el.value);
117
+ state[col.name][key] = el.value === '' || !Number.isFinite(n) ? undefined : n;
118
+ }
119
+ });
120
+ wrap.appendChild(el);
121
+ grid.appendChild(wrap);
122
+ });
123
+ container.appendChild(grid);
124
+ }
125
+
126
+ function buildCard(col) {
127
+ const card = document.createElement('div');
128
+ card.className = 'app-card p-4';
129
+ card.dataset.column = col.name;
130
+
131
+ const head = document.createElement('div');
132
+ head.className = 'flex flex-wrap items-start justify-between gap-2';
133
+
134
+ const title = document.createElement('div');
135
+ title.className = 'min-w-0';
136
+ const name = document.createElement('p');
137
+ name.className = 'flex flex-wrap items-center gap-1.5 font-mono text-sm font-semibold';
138
+ name.textContent = col.name;
139
+ if (col.pk) {
140
+ const chip = document.createElement('span');
141
+ chip.className = 'chip chip-pk';
142
+ chip.textContent = 'PK';
143
+ name.appendChild(chip);
144
+ }
145
+ if (col.notnull) {
146
+ const chip = document.createElement('span');
147
+ chip.className = 'chip chip-type';
148
+ chip.textContent = 'NOT NULL';
149
+ name.appendChild(chip);
150
+ }
151
+ if (col.hasDefault) {
152
+ const chip = document.createElement('span');
153
+ chip.className = 'chip chip-type';
154
+ chip.textContent = 'default';
155
+ name.appendChild(chip);
156
+ }
157
+ const type = document.createElement('p');
158
+ type.className = 'mt-0.5 text-xs text-base-content/45';
159
+ type.textContent = col.type || 'untyped';
160
+ title.appendChild(name);
161
+ title.appendChild(type);
162
+ head.appendChild(title);
163
+
164
+ const select = document.createElement('select');
165
+ select.className = 'field-select seed-strategy !w-auto !py-1 !text-[13px]';
166
+ select.dataset.column = col.name;
167
+ (col.strategies || []).forEach((s) => {
168
+ const opt = document.createElement('option');
169
+ opt.value = s.id;
170
+ opt.textContent = s.label;
171
+ select.appendChild(opt);
172
+ });
173
+ head.appendChild(select);
174
+ card.appendChild(head);
175
+
176
+ const options = document.createElement('div');
177
+ options.className = 'seed-options mt-2.5';
178
+ card.appendChild(options);
179
+
180
+ const note = document.createElement('p');
181
+ note.className = 'seed-note mt-2 hidden text-[11px] leading-relaxed text-base-content/45';
182
+ card.appendChild(note);
183
+
184
+ const warn = document.createElement('p');
185
+ warn.className = 'seed-warn mt-2 hidden text-[11px] font-medium text-warning';
186
+ card.appendChild(warn);
187
+
188
+ // Wire up strategy changes.
189
+ select.addEventListener('change', () => {
190
+ const st = select.value;
191
+ state[col.name] = Object.assign({}, state[col.name] || {}, { strategy: st });
192
+ buildOptionInputs(options, col, state[col.name]);
193
+ note.textContent = strategyNote(col, st);
194
+ note.classList.toggle('hidden', !note.textContent);
195
+ updateWarn(col, warn);
196
+ });
197
+
198
+ columnsBox.appendChild(card);
199
+ return { select, options, note, warn };
200
+ }
201
+
202
+ function updateWarn(col, warnEl) {
203
+ const st = state[col.name] ? state[col.name].strategy : null;
204
+ const risky = (st === 'skip' || st === 'null') && !col.pk && col.notnull && !col.hasDefault;
205
+ warnEl.textContent = risky
206
+ ? 'This column is NOT NULL with no default — "' + st + '" rows may fail to insert.'
207
+ : '';
208
+ warnEl.classList.toggle('hidden', !warnEl.textContent);
209
+ }
210
+
211
+ function resetCard(col) {
212
+ const plan = Object.assign({}, col.defaultPlan || { strategy: 'skip' });
213
+ state[col.name] = plan;
214
+ const select = columnsBox.querySelector('.seed-strategy[data-column="' + CSS.escape(col.name) + '"]');
215
+ if (select) {
216
+ select.value = plan.strategy;
217
+ const options = select.closest('.app-card').querySelector('.seed-options');
218
+ const note = select.closest('.app-card').querySelector('.seed-note');
219
+ const warn = select.closest('.app-card').querySelector('.seed-warn');
220
+ buildOptionInputs(options, col, plan);
221
+ note.textContent = strategyNote(col, plan.strategy);
222
+ note.classList.toggle('hidden', !note.textContent);
223
+ updateWarn(col, warn);
224
+ }
225
+ }
226
+
227
+ // ---- Apply strategy to all ---------------------------------------------
228
+
229
+ function unionStrategies() {
230
+ const map = new Map();
231
+ columns.forEach((col) => (col.strategies || []).forEach((s) => map.set(s.id, s.label)));
232
+ return [...map.entries()].map(([id, label]) => ({ id, label }));
233
+ }
234
+
235
+ function buildApplyAllDialog() {
236
+ const dialog = document.createElement('dialog');
237
+ dialog.className = 'modal';
238
+ dialog.id = 'seed-apply-all-dialog';
239
+ const inner = document.createElement('div');
240
+ inner.className = 'modal-box app-card';
241
+ inner.innerHTML =
242
+ '<h3 class="text-lg font-bold">Apply strategy to all columns</h3>' +
243
+ '<p class="mt-1 text-sm text-base-content/55">Columns that do not support the chosen strategy are left unchanged. Existing option values are preserved where possible.</p>' +
244
+ '<label class="field-label mt-4">Strategy</label>' +
245
+ '<select id="seed-apply-all-select" class="field-select w-full"></select>' +
246
+ '<div class="mt-6 flex justify-end gap-2">' +
247
+ '<button id="seed-apply-all-cancel" type="button" class="btn btn-ghost">Cancel</button>' +
248
+ '<button id="seed-apply-all-go" type="button" class="btn btn-primary">Apply</button>' +
249
+ '</div>';
250
+ dialog.appendChild(inner);
251
+ document.body.appendChild(dialog);
252
+
253
+ const select = inner.querySelector('#seed-apply-all-select');
254
+ unionStrategies().forEach((s) => {
255
+ const opt = document.createElement('option');
256
+ opt.value = s.id;
257
+ opt.textContent = s.label;
258
+ select.appendChild(opt);
259
+ });
260
+
261
+ const close = () => dialog.close();
262
+ inner.querySelector('#seed-apply-all-cancel').addEventListener('click', close);
263
+ inner.querySelector('#seed-apply-all-go').addEventListener('click', () => {
264
+ const target = select.value;
265
+ columns.forEach((col) => {
266
+ const supported = (col.strategies || []).some((s) => s.id === target);
267
+ if (!supported) return;
268
+ state[col.name] = Object.assign({}, state[col.name] || {}, { strategy: target });
269
+ const sel = columnsBox.querySelector('.seed-strategy[data-column="' + CSS.escape(col.name) + '"]');
270
+ if (sel) {
271
+ sel.value = target;
272
+ const options = sel.closest('.app-card').querySelector('.seed-options');
273
+ const note = sel.closest('.app-card').querySelector('.seed-note');
274
+ const warn = sel.closest('.app-card').querySelector('.seed-warn');
275
+ buildOptionInputs(options, col, state[col.name]);
276
+ note.textContent = strategyNote(col, target);
277
+ note.classList.toggle('hidden', !note.textContent);
278
+ updateWarn(col, warn);
279
+ }
280
+ });
281
+ close();
282
+ });
283
+ return dialog;
284
+ }
285
+
286
+ // ---- Feedback -----------------------------------------------------------
287
+
288
+ function showFeedback(warnings) {
289
+ feedback.classList.remove('hidden');
290
+ if (!warnings || !warnings.length) {
291
+ feedback.innerHTML = '';
292
+ feedback.classList.add('hidden');
293
+ return;
294
+ }
295
+ feedback.innerHTML =
296
+ '<div class="alert alert-warning shadow-soft">' +
297
+ '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>' +
298
+ '<div class="text-sm">' + warnings.map((w) => '<p>' + escapeHtml(w) + '</p>').join('') + '</div>' +
299
+ '</div>';
300
+ }
301
+
302
+ function showSql(sql) {
303
+ sqlCode.textContent = sql;
304
+ sqlBox.classList.remove('hidden');
305
+ sqlBox.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
306
+ }
307
+
308
+ // ---- Collect plan + submit ---------------------------------------------
309
+
310
+ function collectPlan() {
311
+ const plan = {};
312
+ columns.forEach((col) => {
313
+ const st = state[col.name];
314
+ if (!st || !st.strategy) return;
315
+ const out = { strategy: st.strategy };
316
+ Object.keys(st).forEach((k) => {
317
+ if (k === 'strategy' || st[k] === undefined) return;
318
+ out[k] = st[k];
319
+ });
320
+ plan[col.name] = out;
321
+ });
322
+ return plan;
323
+ }
324
+
325
+ function currentCount() {
326
+ const n = Number.parseInt(String(countInput ? countInput.value : '10'), 10);
327
+ return Math.max(1, Math.min(cfg.maxRows || 5000, Number.isFinite(n) ? n : 10));
328
+ }
329
+
330
+ async function submit(insert) {
331
+ const count = currentCount();
332
+ const plan = collectPlan();
333
+ const path = insert ? '/api/tables/' + t + '/seed' : '/api/tables/' + t + '/seed/generate';
334
+ try {
335
+ const data = await Api.post(path, { count, plan });
336
+ if (insert) {
337
+ UI.showToast(data.message, 'success');
338
+ showFeedback(data.warnings || []);
339
+ } else {
340
+ showSql(data.sql);
341
+ showFeedback(data.warnings || []);
342
+ }
343
+ } catch (e) {
344
+ UI.showError(e.message);
345
+ }
346
+ }
347
+
348
+ // ---- Init ---------------------------------------------------------------
349
+
350
+ function init() {
351
+ columns.forEach((col) => {
352
+ buildCard(col);
353
+ resetCard(col);
354
+ });
355
+ if (insertBtn) insertBtn.addEventListener('click', () => submit(true));
356
+ if (previewBtn) previewBtn.addEventListener('click', () => submit(false));
357
+ if (resetBtn) resetBtn.addEventListener('click', () => columns.forEach(resetCard));
358
+ if (applyAllBtn) applyAllBtn.addEventListener('click', () => {
359
+ const dialog = document.getElementById('seed-apply-all-dialog') || buildApplyAllDialog();
360
+ dialog.showModal();
361
+ });
362
+ if (copyBtn) {
363
+ copyBtn.addEventListener('click', () => {
364
+ navigator.clipboard.writeText(sqlCode.textContent)
365
+ .then(() => UI.showToast('Copied to clipboard.', 'success'))
366
+ .catch(() => UI.showError('Could not copy.'));
367
+ });
368
+ }
369
+ document.querySelectorAll('.seed-quick').forEach((btn) => {
370
+ btn.addEventListener('click', () => {
371
+ if (countInput) countInput.value = btn.dataset.n;
372
+ });
373
+ });
374
+ }
375
+
376
+ init();
377
+ })();
@@ -5,6 +5,7 @@ const generator_1 = require("../sql/generator");
5
5
  const classifier_1 = require("../sql/classifier");
6
6
  const csv_1 = require("../csv");
7
7
  const util_1 = require("../util");
8
+ const generator_2 = require("../data/generator");
8
9
  function ok(res, data, status = 200) {
9
10
  return res.status(status).json({ success: true, data });
10
11
  }
@@ -71,6 +72,24 @@ function pkWhere(info, encodedId) {
71
72
  }
72
73
  /** Hard cap on how many rows can be selected for a bulk operation at once. */
73
74
  const MAX_BULK_ROWS = 1000;
75
+ /**
76
+ * Parse and clamp the seed request body `{ count, plan }` into a safe row count
77
+ * and a per-column plan map (each plan sanitized against known keys). Per-column
78
+ * strategy validity is enforced later by `generateRows` against the table config.
79
+ */
80
+ function parseSeedRequest(body) {
81
+ const b = (body ?? {});
82
+ const count = Math.max(1, Math.min(generator_2.MAX_SEED_ROWS, Number.parseInt(String(b.count ?? '10'), 10) || 10));
83
+ const plan = {};
84
+ if (b.plan && typeof b.plan === 'object') {
85
+ for (const [k, v] of Object.entries(b.plan)) {
86
+ const p = (0, generator_2.sanitizeColumnPlan)(v);
87
+ if (p)
88
+ plan[k] = p;
89
+ }
90
+ }
91
+ return { count, plan };
92
+ }
74
93
  /**
75
94
  * Turn a list of encoded primary-key ids (from the grid's row checkboxes) into
76
95
  * the WHERE clauses needed to address each row. Returns null when the table has
@@ -525,6 +544,49 @@ function registerApi(router, ctx) {
525
544
  updated: r.data?.updated ?? 0,
526
545
  });
527
546
  }));
547
+ // ---- Data generator / seeder ------------------------------------------
548
+ // Per-column generator config (strategies + detected defaults) for the
549
+ // "Seed data" page.
550
+ router.get('/api/tables/:table/seed/config', wrap(async (req, res) => {
551
+ const info = await requireTable(db, req.params.table);
552
+ if (!info)
553
+ return fail(res, `Table "${req.params.table}" does not exist.`, 404);
554
+ ok(res, { table: req.params.table, columns: (0, generator_2.buildColumnConfigs)(info), maxRows: generator_2.MAX_SEED_ROWS });
555
+ }));
556
+ // Preview: generate the rows and return the INSERT statements without
557
+ // executing them (the "/generate" suffix keeps it available in read-only).
558
+ router.post('/api/tables/:table/seed/generate', wrap(async (req, res) => {
559
+ const info = await requireTable(db, req.params.table);
560
+ if (!info)
561
+ return fail(res, `Table "${req.params.table}" does not exist.`, 404);
562
+ const { count, plan } = parseSeedRequest(req.body);
563
+ const configs = (0, generator_2.buildColumnConfigs)(info);
564
+ const gen = await (0, generator_2.generateRows)(db, info, configs, count, plan);
565
+ ok(res, {
566
+ table: req.params.table,
567
+ count: gen.rows.length,
568
+ sql: (0, generator_2.buildSeedInsertSql)(req.params.table, gen.rows),
569
+ warnings: gen.warnings,
570
+ });
571
+ }));
572
+ // Insert the generated rows inside a single transaction.
573
+ router.post('/api/tables/:table/seed', wrap(async (req, res) => {
574
+ const info = await requireTable(db, req.params.table);
575
+ if (!info)
576
+ return fail(res, `Table "${req.params.table}" does not exist.`, 404);
577
+ const { count, plan } = parseSeedRequest(req.body);
578
+ const configs = (0, generator_2.buildColumnConfigs)(info);
579
+ const gen = await (0, generator_2.generateRows)(db, info, configs, count, plan);
580
+ const result = await db.insertRows(req.params.table, gen.rows);
581
+ if (!result.success)
582
+ return fail(res, result.error ?? 'Seed failed.', 400);
583
+ ok(res, {
584
+ message: `Inserted ${result.data?.inserted ?? 0} row(s).`,
585
+ inserted: result.data?.inserted ?? 0,
586
+ skipped: result.data?.skipped ?? 0,
587
+ warnings: gen.warnings,
588
+ }, 201);
589
+ }));
528
590
  // ---- Create table (run + generate) ------------------------------------
529
591
  router.post('/api/tables', wrap(async (req, res) => {
530
592
  const { name, columns } = req.body ?? {};