sqlite-hub 1.4.0 → 2.0.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 (46) hide show
  1. package/README.md +14 -1
  2. package/bin/sqlite-hub-mcp.js +8 -0
  3. package/bin/sqlite-hub.js +555 -122
  4. package/docs/API.md +30 -0
  5. package/docs/CLI.md +22 -0
  6. package/docs/CLI_API_PARITY.md +61 -0
  7. package/docs/MCP.md +85 -0
  8. package/docs/changelog.md +13 -1
  9. package/docs/guidelines/AGENTS.md +32 -0
  10. package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
  11. package/docs/todo.md +1 -14
  12. package/frontend/js/api.js +49 -0
  13. package/frontend/js/app.js +202 -2
  14. package/frontend/js/components/connectionCard.js +3 -1
  15. package/frontend/js/components/modal.js +356 -15
  16. package/frontend/js/components/sidebar.js +41 -7
  17. package/frontend/js/components/topNav.js +2 -14
  18. package/frontend/js/router.js +10 -0
  19. package/frontend/js/store.js +483 -7
  20. package/frontend/js/utils/inputClear.js +36 -0
  21. package/frontend/js/utils/syntheticData.js +240 -0
  22. package/frontend/js/views/backups.js +52 -0
  23. package/frontend/js/views/data.js +16 -0
  24. package/frontend/js/views/logs.js +339 -0
  25. package/frontend/js/views/settings.js +266 -30
  26. package/frontend/js/views/structure.js +6 -0
  27. package/frontend/js/views/tableAdvisor.js +385 -0
  28. package/frontend/js/views/tableDesigner.js +6 -0
  29. package/frontend/styles/components.css +149 -0
  30. package/frontend/styles/tailwind.generated.css +1 -1
  31. package/frontend/styles/views.css +31 -0
  32. package/package.json +4 -2
  33. package/server/mcp/stdioServer.js +272 -0
  34. package/server/routes/data.js +45 -0
  35. package/server/routes/externalApi.js +285 -2
  36. package/server/routes/logs.js +127 -0
  37. package/server/routes/settings.js +47 -0
  38. package/server/server.js +3 -0
  39. package/server/services/databaseCommandService.js +284 -2
  40. package/server/services/mcpStatusService.js +140 -0
  41. package/server/services/mcpToolService.js +274 -0
  42. package/server/services/sqlite/dataBrowserService.js +36 -0
  43. package/server/services/sqlite/introspection.js +226 -22
  44. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  45. package/server/services/sqlite/tableAdvisor.js +790 -0
  46. package/server/services/storage/appStateStore.js +821 -169
@@ -23,6 +23,11 @@ import { MEDIA_TAGGING_DEFAULT_MAPPING_TABLE, MEDIA_TAGGING_DEFAULT_TAG_TABLE }
23
23
  import { buildTextExportFilename } from './utils/exportFilenames.js';
24
24
  import { toggleMarkdownTodoLine } from './utils/markdownDocuments.js';
25
25
  import { buildRiskySqlBackupContext, detectRiskySqlOperations } from './utils/riskySql.js';
26
+ import {
27
+ buildSyntheticDataMappings,
28
+ getDefaultSyntheticOptions,
29
+ normalizeSyntheticGeneratorType,
30
+ } from './utils/syntheticData.js';
26
31
 
27
32
  const listeners = new Set();
28
33
  const DEFAULT_SETTINGS = {
@@ -59,6 +64,13 @@ const UI_PREFERENCE_STORAGE_KEYS = {
59
64
  };
60
65
  const QUERY_HISTORY_PAGE_SIZE = 30;
61
66
  const QUERY_HISTORY_RUN_LIMIT = 8;
67
+ const LOG_PAGE_SIZE = 80;
68
+ const LOG_KINDS = new Set(['all', 'query', 'access']);
69
+ const LOG_RANGES = new Set(['1h', '24h', '7d', '30d', 'all']);
70
+ const LOG_ACTORS = new Set(['all', 'user', 'cli', 'api', 'mcp']);
71
+ const LOG_STATUSES = new Set(['all', 'success', 'error']);
72
+ const LOG_QUERY_TYPES = new Set(['all', 'select', 'insert', 'update', 'delete', 'pragma', 'create', 'alter', 'drop', 'other']);
73
+ const LOG_DESTRUCTIVE_FILTERS = new Set(['all', 'yes', 'no']);
62
74
  const CHART_HEIGHT_PRESETS = new Set(['small', 'medium', 'large']);
63
75
  const TYPE_GENERATION_TARGETS = new Set(['typescript', 'rust', 'kotlin', 'swift']);
64
76
  const TYPE_GENERATION_NAMING = new Set(['preserve', 'camel', 'pascal', 'snake']);
@@ -258,6 +270,24 @@ function storeQueryHistoryTab(tab) {
258
270
  }
259
271
  }
260
272
 
273
+ function normalizeSetValue(value, allowedValues, fallback) {
274
+ const normalized = String(value ?? fallback).trim().toLowerCase();
275
+ return allowedValues.has(normalized) ? normalized : fallback;
276
+ }
277
+
278
+ function normalizeLogFilters(filters = {}) {
279
+ return {
280
+ kind: normalizeSetValue(filters.kind, LOG_KINDS, 'all'),
281
+ range: normalizeSetValue(filters.range, LOG_RANGES, 'all'),
282
+ actor: normalizeSetValue(filters.actor, LOG_ACTORS, 'all'),
283
+ status: normalizeSetValue(filters.status, LOG_STATUSES, 'all'),
284
+ queryType: normalizeSetValue(filters.queryType, LOG_QUERY_TYPES, 'all'),
285
+ destructive: normalizeSetValue(filters.destructive, LOG_DESTRUCTIVE_FILTERS, 'all'),
286
+ searchInput: String(filters.searchInput ?? filters.search ?? ''),
287
+ search: String(filters.search ?? filters.searchInput ?? '').trim(),
288
+ };
289
+ }
290
+
261
291
  const state = {
262
292
  ready: false,
263
293
  route: { name: 'landing', path: '/', params: {} },
@@ -302,12 +332,27 @@ const state = {
302
332
  versionCheck: null,
303
333
  versionCheckLoading: false,
304
334
  versionCheckError: null,
335
+ mcpStatus: null,
336
+ mcpStatusLoading: false,
337
+ mcpStatusError: null,
305
338
  },
306
339
  overview: {
307
340
  data: null,
308
341
  loading: false,
309
342
  error: null,
310
343
  },
344
+ logs: {
345
+ items: [],
346
+ total: 0,
347
+ limit: LOG_PAGE_SIZE,
348
+ offset: 0,
349
+ hasMore: false,
350
+ loading: false,
351
+ loadingMore: false,
352
+ error: null,
353
+ filters: normalizeLogFilters(),
354
+ metadata: null,
355
+ },
311
356
  dataBrowser: {
312
357
  tables: [],
313
358
  selectedTable: null,
@@ -332,6 +377,15 @@ const state = {
332
377
  error: null,
333
378
  saveError: null,
334
379
  },
380
+ tableAdvisor: {
381
+ tables: [],
382
+ selectedTableName: null,
383
+ result: null,
384
+ loading: false,
385
+ analysisLoading: false,
386
+ error: null,
387
+ analysisError: null,
388
+ },
335
389
  editor: {
336
390
  sqlText: readStoredString(UI_PREFERENCE_STORAGE_KEYS.sqlEditorQueryDraft),
337
391
  editorPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.sqlEditorEditorVisible, true),
@@ -622,6 +676,7 @@ function requiresActiveDatabase(routeName) {
622
676
  return [
623
677
  'overview',
624
678
  'backups',
679
+ 'logs',
625
680
  'data',
626
681
  'editor',
627
682
  'editorResults',
@@ -629,6 +684,7 @@ function requiresActiveDatabase(routeName) {
629
684
  'documents',
630
685
  'structure',
631
686
  'tableDesigner',
687
+ 'tableAdvisor',
632
688
  'mediaTaggingSetup',
633
689
  'mediaTaggingQueue',
634
690
  ].includes(routeName);
@@ -1290,6 +1346,7 @@ function resolveQueryHistorySql(historyId) {
1290
1346
 
1291
1347
  function clearRouteSlices() {
1292
1348
  state.overview.error = null;
1349
+ state.logs.error = null;
1293
1350
  state.dataBrowser.error = null;
1294
1351
  state.dataBrowser.saveError = null;
1295
1352
  state.charts.error = null;
@@ -1297,6 +1354,8 @@ function clearRouteSlices() {
1297
1354
  state.charts.resultError = null;
1298
1355
  state.tableDesigner.error = null;
1299
1356
  state.tableDesigner.saveError = null;
1357
+ state.tableAdvisor.error = null;
1358
+ state.tableAdvisor.analysisError = null;
1300
1359
  state.structure.error = null;
1301
1360
  state.mediaTagging.error = null;
1302
1361
  state.documents.error = null;
@@ -1310,6 +1369,15 @@ function setMissingDatabaseState() {
1310
1369
  state.overview.data = null;
1311
1370
  state.overview.error = error;
1312
1371
 
1372
+ state.logs.loading = false;
1373
+ state.logs.loadingMore = false;
1374
+ state.logs.items = [];
1375
+ state.logs.total = 0;
1376
+ state.logs.offset = 0;
1377
+ state.logs.hasMore = false;
1378
+ state.logs.error = error;
1379
+ state.logs.metadata = null;
1380
+
1313
1381
  state.dataBrowser.loading = false;
1314
1382
  state.dataBrowser.tableLoading = false;
1315
1383
  state.dataBrowser.tables = [];
@@ -1350,6 +1418,14 @@ function setMissingDatabaseState() {
1350
1418
  state.tableDesigner.error = error;
1351
1419
  state.tableDesigner.saveError = null;
1352
1420
 
1421
+ state.tableAdvisor.loading = false;
1422
+ state.tableAdvisor.analysisLoading = false;
1423
+ state.tableAdvisor.tables = [];
1424
+ state.tableAdvisor.selectedTableName = null;
1425
+ state.tableAdvisor.result = null;
1426
+ state.tableAdvisor.error = error;
1427
+ state.tableAdvisor.analysisError = null;
1428
+
1353
1429
  state.mediaTagging.loading = false;
1354
1430
  state.mediaTagging.previewLoading = false;
1355
1431
  state.mediaTagging.saving = false;
@@ -1495,6 +1571,81 @@ async function refreshSettingsState() {
1495
1571
  }
1496
1572
  }
1497
1573
 
1574
+ async function refreshSettingsMcpStatus() {
1575
+ state.settings.mcpStatusLoading = true;
1576
+ state.settings.mcpStatusError = null;
1577
+ emitChange();
1578
+
1579
+ try {
1580
+ const response = await api.getSettingsMcpStatus();
1581
+ state.settings.mcpStatus = response.data ?? null;
1582
+ } catch (error) {
1583
+ state.settings.mcpStatusError = normalizeError(error);
1584
+ } finally {
1585
+ state.settings.mcpStatusLoading = false;
1586
+ emitChange();
1587
+ }
1588
+ }
1589
+
1590
+ function buildLogRequestOptions({ append = false } = {}) {
1591
+ const filters = normalizeLogFilters(state.logs.filters);
1592
+
1593
+ return {
1594
+ ...filters,
1595
+ search: filters.search,
1596
+ limit: state.logs.limit,
1597
+ offset: append ? state.logs.items.length : 0,
1598
+ };
1599
+ }
1600
+
1601
+ async function loadLogs(options = {}) {
1602
+ const append = Boolean(options.append);
1603
+
1604
+ if (append && (state.logs.loading || state.logs.loadingMore || !state.logs.hasMore)) {
1605
+ return;
1606
+ }
1607
+
1608
+ if (append) {
1609
+ state.logs.loadingMore = true;
1610
+ } else {
1611
+ state.logs.loading = true;
1612
+ state.logs.offset = 0;
1613
+ state.logs.error = null;
1614
+ }
1615
+
1616
+ emitChange();
1617
+
1618
+ try {
1619
+ const response = await api.getLogs(buildLogRequestOptions({ append }));
1620
+ const data = response.data ?? {};
1621
+ const items = Array.isArray(data.items) ? data.items : [];
1622
+
1623
+ state.logs.items = append ? [...state.logs.items, ...items] : items;
1624
+ state.logs.total = Number(data.total ?? state.logs.items.length);
1625
+ state.logs.limit = Number(data.limit ?? state.logs.limit);
1626
+ state.logs.offset = Number(data.offset ?? 0);
1627
+ state.logs.hasMore = Boolean(data.hasMore);
1628
+ state.logs.metadata = response.metadata ?? data.filters ?? null;
1629
+ state.logs.filters = normalizeLogFilters({
1630
+ ...state.logs.filters,
1631
+ ...(data.filters ?? {}),
1632
+ ...(response.metadata ?? {}),
1633
+ });
1634
+ state.logs.error = null;
1635
+ } catch (error) {
1636
+ state.logs.error = normalizeError(error);
1637
+ if (!append) {
1638
+ state.logs.items = [];
1639
+ state.logs.total = 0;
1640
+ state.logs.hasMore = false;
1641
+ }
1642
+ } finally {
1643
+ state.logs.loading = false;
1644
+ state.logs.loadingMore = false;
1645
+ emitChange();
1646
+ }
1647
+ }
1648
+
1498
1649
  async function refreshBackupsState() {
1499
1650
  if (!state.connections.active) {
1500
1651
  state.backups.items = [];
@@ -1542,7 +1693,8 @@ export async function createSettingsApiToken(name) {
1542
1693
  }
1543
1694
 
1544
1695
  export function setSettingsSection(section) {
1545
- const normalizedSection = section === 'api-tokens' ? 'api-tokens' : 'information';
1696
+ const normalizedSection =
1697
+ section === 'api-tokens' ? 'api-tokens' : section === 'mcp' ? 'mcp' : 'information';
1546
1698
 
1547
1699
  if (state.settings.section === normalizedSection) {
1548
1700
  return;
@@ -1552,7 +1704,51 @@ export function setSettingsSection(section) {
1552
1704
  emitChange();
1553
1705
  }
1554
1706
 
1555
- export async function checkSettingsAppVersion() {
1707
+ export async function setLogFilter(field, value) {
1708
+ if (!Object.prototype.hasOwnProperty.call(state.logs.filters, field)) {
1709
+ return;
1710
+ }
1711
+
1712
+ state.logs.filters = normalizeLogFilters({
1713
+ ...state.logs.filters,
1714
+ [field]: value,
1715
+ });
1716
+ state.logs.items = [];
1717
+ state.logs.total = 0;
1718
+ state.logs.hasMore = false;
1719
+ await loadLogs();
1720
+ }
1721
+
1722
+ export function setLogSearchInput(value) {
1723
+ state.logs.filters = normalizeLogFilters({
1724
+ ...state.logs.filters,
1725
+ searchInput: value,
1726
+ });
1727
+ }
1728
+
1729
+ export async function applyLogSearch(value = state.logs.filters.searchInput) {
1730
+ state.logs.filters = normalizeLogFilters({
1731
+ ...state.logs.filters,
1732
+ searchInput: value,
1733
+ search: value,
1734
+ });
1735
+ state.logs.items = [];
1736
+ state.logs.total = 0;
1737
+ state.logs.hasMore = false;
1738
+ await loadLogs();
1739
+ }
1740
+
1741
+ export async function refreshLogs() {
1742
+ await loadLogs();
1743
+ }
1744
+
1745
+ export async function loadMoreLogs() {
1746
+ await loadLogs({ append: true });
1747
+ }
1748
+
1749
+ export async function checkSettingsAppVersion(options = {}) {
1750
+ const silent = Boolean(options.silent);
1751
+
1556
1752
  state.settings.versionCheckLoading = true;
1557
1753
  state.settings.versionCheckError = null;
1558
1754
  emitChange();
@@ -1562,14 +1758,18 @@ export async function checkSettingsAppVersion() {
1562
1758
  const result = response.data ?? null;
1563
1759
 
1564
1760
  state.settings.versionCheck = result;
1565
- pushToast(
1566
- result?.updateAvailable ? `SQLite Hub v${result.latestVersion} is available.` : 'SQLite Hub is up to date.',
1567
- 'success',
1568
- );
1761
+ if (!silent) {
1762
+ pushToast(
1763
+ result?.updateAvailable ? `SQLite Hub v${result.latestVersion} is available.` : 'SQLite Hub is up to date.',
1764
+ 'success',
1765
+ );
1766
+ }
1569
1767
  return result;
1570
1768
  } catch (error) {
1571
1769
  state.settings.versionCheckError = normalizeError(error);
1572
- pushToast('Version check failed.', 'alert');
1770
+ if (!silent) {
1771
+ pushToast('Version check failed.', 'alert');
1772
+ }
1573
1773
  return null;
1574
1774
  } finally {
1575
1775
  state.settings.versionCheckLoading = false;
@@ -2224,6 +2424,94 @@ async function loadData(version, route) {
2224
2424
  }
2225
2425
  }
2226
2426
 
2427
+ async function loadTableAdvisorResult(version, tableName) {
2428
+ if (!tableName) {
2429
+ state.tableAdvisor.result = null;
2430
+ state.tableAdvisor.analysisError = null;
2431
+ state.tableAdvisor.analysisLoading = false;
2432
+ return;
2433
+ }
2434
+
2435
+ state.tableAdvisor.analysisLoading = true;
2436
+ state.tableAdvisor.analysisError = null;
2437
+ state.tableAdvisor.result = null;
2438
+ emitChange();
2439
+
2440
+ try {
2441
+ const response = await api.analyzeTableAdvisor(tableName);
2442
+
2443
+ if (version !== routeLoadVersion) {
2444
+ return;
2445
+ }
2446
+
2447
+ state.tableAdvisor.result = response.data ?? null;
2448
+ state.tableAdvisor.analysisError = null;
2449
+ } catch (error) {
2450
+ if (version !== routeLoadVersion) {
2451
+ return;
2452
+ }
2453
+
2454
+ state.tableAdvisor.result = null;
2455
+ state.tableAdvisor.analysisError = normalizeError(error);
2456
+ } finally {
2457
+ if (version === routeLoadVersion) {
2458
+ state.tableAdvisor.analysisLoading = false;
2459
+ emitChange();
2460
+ }
2461
+ }
2462
+ }
2463
+
2464
+ async function loadTableAdvisor(version, route) {
2465
+ state.tableAdvisor.loading = true;
2466
+ state.tableAdvisor.error = null;
2467
+ state.tableAdvisor.analysisError = null;
2468
+ emitChange();
2469
+
2470
+ try {
2471
+ const response = await api.getDataTables();
2472
+
2473
+ if (version !== routeLoadVersion) {
2474
+ return;
2475
+ }
2476
+
2477
+ const tables = response.data?.tables ?? [];
2478
+ const requestedTableName = route.params?.tableName ?? null;
2479
+ const selectedTableName =
2480
+ requestedTableName && tables.some(table => table.name === requestedTableName)
2481
+ ? requestedTableName
2482
+ : (state.tableAdvisor.selectedTableName && tables.some(table => table.name === state.tableAdvisor.selectedTableName)
2483
+ ? state.tableAdvisor.selectedTableName
2484
+ : (tables[0]?.name ?? null));
2485
+
2486
+ state.tableAdvisor.tables = tables;
2487
+ state.tableAdvisor.selectedTableName = selectedTableName;
2488
+ state.tableAdvisor.error = null;
2489
+
2490
+ if (!selectedTableName) {
2491
+ state.tableAdvisor.result = null;
2492
+ return;
2493
+ }
2494
+
2495
+ state.tableAdvisor.loading = false;
2496
+ emitChange();
2497
+ await loadTableAdvisorResult(version, selectedTableName);
2498
+ } catch (error) {
2499
+ if (version !== routeLoadVersion) {
2500
+ return;
2501
+ }
2502
+
2503
+ state.tableAdvisor.tables = [];
2504
+ state.tableAdvisor.selectedTableName = null;
2505
+ state.tableAdvisor.result = null;
2506
+ state.tableAdvisor.error = normalizeError(error);
2507
+ } finally {
2508
+ if (version === routeLoadVersion) {
2509
+ state.tableAdvisor.loading = false;
2510
+ emitChange();
2511
+ }
2512
+ }
2513
+ }
2514
+
2227
2515
  async function loadStructureDetail(version) {
2228
2516
  const entry = getCurrentStructureEntry(state);
2229
2517
 
@@ -2671,6 +2959,9 @@ async function loadRouteData(route, options = {}) {
2671
2959
  case 'backups':
2672
2960
  await refreshBackupsState();
2673
2961
  return;
2962
+ case 'logs':
2963
+ await loadLogs();
2964
+ return;
2674
2965
  case 'data':
2675
2966
  await loadData(version, route);
2676
2967
  return;
@@ -2690,12 +2981,17 @@ async function loadRouteData(route, options = {}) {
2690
2981
  case 'tableDesigner':
2691
2982
  await loadTableDesigner(version, route);
2692
2983
  return;
2984
+ case 'tableAdvisor':
2985
+ await loadTableAdvisor(version, route);
2986
+ return;
2693
2987
  case 'mediaTaggingSetup':
2694
2988
  case 'mediaTaggingQueue':
2695
2989
  await loadMediaTagging(version);
2696
2990
  return;
2697
2991
  case 'settings':
2698
2992
  await refreshSettingsState();
2993
+ await refreshSettingsMcpStatus();
2994
+ await checkSettingsAppVersion({ silent: true });
2699
2995
  return;
2700
2996
  default:
2701
2997
  }
@@ -2892,6 +3188,186 @@ export function openDataExportModal() {
2892
3188
  emitChange();
2893
3189
  }
2894
3190
 
3191
+ function getGenerateDataModalColumn(modal, columnName) {
3192
+ return (modal?.columns ?? []).find(column => column.name === columnName) ?? null;
3193
+ }
3194
+
3195
+ function resetGenerateDataPreview(modal) {
3196
+ if (!modal) {
3197
+ return;
3198
+ }
3199
+
3200
+ modal.previewRows = [];
3201
+ modal.previewColumns = (modal.columns ?? []).map(column => column.name);
3202
+ }
3203
+
3204
+ function buildGenerateDataPayload(modal) {
3205
+ return {
3206
+ rowCount: Number(modal.rowCount ?? 100),
3207
+ mappings: (modal.mappings ?? []).map(mapping => ({
3208
+ columnName: mapping.columnName,
3209
+ generator: mapping.generator,
3210
+ options: { ...(mapping.options ?? {}) },
3211
+ })),
3212
+ };
3213
+ }
3214
+
3215
+ export function openGenerateDataModal() {
3216
+ const table = state.dataBrowser.table;
3217
+ const tableName = table?.name ?? state.dataBrowser.selectedTable;
3218
+
3219
+ if (!tableName || !table) {
3220
+ pushToast('Select a table before generating rows.', 'alert');
3221
+ return;
3222
+ }
3223
+
3224
+ const columns = (table.columnMeta ?? []).filter(column => column.visible && !column.generated);
3225
+
3226
+ state.modal = {
3227
+ kind: 'generate-data',
3228
+ tableName,
3229
+ columns,
3230
+ rowCount: 100,
3231
+ mappings: buildSyntheticDataMappings(columns, table.foreignKeys ?? []),
3232
+ previewColumns: columns.map(column => column.name),
3233
+ previewRows: [],
3234
+ previewLoading: false,
3235
+ previewRequestId: 0,
3236
+ error: null,
3237
+ submitting: false,
3238
+ };
3239
+ emitChange();
3240
+ }
3241
+
3242
+ export function updateGenerateDataModal(field, value, options = {}) {
3243
+ if (state.modal?.kind !== 'generate-data') {
3244
+ return;
3245
+ }
3246
+
3247
+ if (field === 'rowCount') {
3248
+ const nextValue = String(value ?? '');
3249
+
3250
+ if (state.modal.rowCount === nextValue) {
3251
+ return;
3252
+ }
3253
+
3254
+ state.modal.rowCount = nextValue;
3255
+ state.modal.error = null;
3256
+ resetGenerateDataPreview(state.modal);
3257
+ if (options.notify !== false) {
3258
+ emitChange();
3259
+ }
3260
+ }
3261
+ }
3262
+
3263
+ export function updateGenerateDataMapping(columnName, field, value, options = {}) {
3264
+ const modal = state.modal;
3265
+
3266
+ if (modal?.kind !== 'generate-data') {
3267
+ return;
3268
+ }
3269
+
3270
+ const mapping = (modal.mappings ?? []).find(item => item.columnName === columnName);
3271
+
3272
+ if (!mapping) {
3273
+ return;
3274
+ }
3275
+
3276
+ const column = getGenerateDataModalColumn(modal, columnName);
3277
+
3278
+ if (field === 'generator') {
3279
+ const generator = normalizeSyntheticGeneratorType(value, mapping.generator);
3280
+
3281
+ if (mapping.generator === generator) {
3282
+ return;
3283
+ }
3284
+
3285
+ mapping.generator = generator;
3286
+ mapping.options = getDefaultSyntheticOptions(generator, column ?? {});
3287
+ } else {
3288
+ const nextValue = value;
3289
+
3290
+ if ((mapping.options ?? {})[field] === nextValue) {
3291
+ return;
3292
+ }
3293
+
3294
+ mapping.options = {
3295
+ ...(mapping.options ?? {}),
3296
+ [field]: nextValue,
3297
+ };
3298
+ }
3299
+
3300
+ modal.error = null;
3301
+ resetGenerateDataPreview(modal);
3302
+ if (options.notify !== false) {
3303
+ emitChange();
3304
+ }
3305
+ }
3306
+
3307
+ export async function previewGenerateDataRows() {
3308
+ const modal = state.modal;
3309
+
3310
+ if (modal?.kind !== 'generate-data') {
3311
+ return null;
3312
+ }
3313
+
3314
+ const requestId = (modal.previewRequestId ?? 0) + 1;
3315
+ modal.previewRequestId = requestId;
3316
+ modal.previewLoading = true;
3317
+ modal.error = null;
3318
+ emitChange();
3319
+
3320
+ try {
3321
+ const response = await api.previewSyntheticDataRows(modal.tableName, buildGenerateDataPayload(modal));
3322
+
3323
+ if (state.modal?.kind !== 'generate-data' || state.modal.previewRequestId !== requestId) {
3324
+ return null;
3325
+ }
3326
+
3327
+ state.modal.previewColumns = response.data?.columns ?? [];
3328
+ state.modal.previewRows = response.data?.rows ?? [];
3329
+ state.modal.error = null;
3330
+ return response.data;
3331
+ } catch (error) {
3332
+ if (state.modal?.kind === 'generate-data' && state.modal.previewRequestId === requestId) {
3333
+ state.modal.error = normalizeError(error);
3334
+ }
3335
+
3336
+ return null;
3337
+ } finally {
3338
+ if (state.modal?.kind === 'generate-data' && state.modal.previewRequestId === requestId) {
3339
+ state.modal.previewLoading = false;
3340
+ emitChange();
3341
+ }
3342
+ }
3343
+ }
3344
+
3345
+ export async function submitGenerateDataRows() {
3346
+ const modal = state.modal;
3347
+
3348
+ if (modal?.kind !== 'generate-data') {
3349
+ return null;
3350
+ }
3351
+
3352
+ startModalSubmission();
3353
+
3354
+ try {
3355
+ const response = await api.insertSyntheticDataRows(modal.tableName, buildGenerateDataPayload(modal));
3356
+
3357
+ if (state.modal?.kind === 'generate-data') {
3358
+ closeModalInternal();
3359
+ }
3360
+
3361
+ pushToast(response.message || `Generated ${response.data?.insertedRowCount ?? modal.rowCount} rows for ${modal.tableName}.`, 'success');
3362
+ await loadDataTable(++routeLoadVersion);
3363
+ clearDataBrowserRowSelectionState();
3364
+ return response.data;
3365
+ } catch (error) {
3366
+ withModalError(error);
3367
+ return null;
3368
+ }
3369
+ }
3370
+
2895
3371
  export function openCopyColumnModal({ scope = 'editor', columnName = '', mode = 'column' } = {}) {
2896
3372
  const resultScope = normalizeCopyColumnScope(scope);
2897
3373
  const normalizedColumnName = String(columnName ?? '');
@@ -0,0 +1,36 @@
1
+ const ESCAPE_CLEARABLE_INPUT_TYPES = new Set([
2
+ '',
3
+ 'email',
4
+ 'number',
5
+ 'password',
6
+ 'search',
7
+ 'tel',
8
+ 'text',
9
+ 'url',
10
+ ]);
11
+
12
+ export function isEscapeClearableInput(input) {
13
+ const type = String(input?.type ?? 'text').trim().toLowerCase();
14
+
15
+ return (
16
+ Boolean(input) &&
17
+ typeof input.value === 'string' &&
18
+ !input.disabled &&
19
+ !input.readOnly &&
20
+ ESCAPE_CLEARABLE_INPUT_TYPES.has(type)
21
+ );
22
+ }
23
+
24
+ export function clearInputForEscape(input, createInputEvent = () => new Event('input', { bubbles: true })) {
25
+ if (!isEscapeClearableInput(input) || input.value === '') {
26
+ return false;
27
+ }
28
+
29
+ input.value = '';
30
+
31
+ if (typeof input.dispatchEvent === 'function') {
32
+ input.dispatchEvent(createInputEvent());
33
+ }
34
+
35
+ return true;
36
+ }