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.
- package/README.md +14 -1
- package/bin/sqlite-hub-mcp.js +8 -0
- package/bin/sqlite-hub.js +555 -122
- package/docs/API.md +30 -0
- package/docs/CLI.md +22 -0
- package/docs/CLI_API_PARITY.md +61 -0
- package/docs/MCP.md +85 -0
- package/docs/changelog.md +13 -1
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
- package/docs/todo.md +1 -14
- package/frontend/js/api.js +49 -0
- package/frontend/js/app.js +202 -2
- package/frontend/js/components/connectionCard.js +3 -1
- package/frontend/js/components/modal.js +356 -15
- package/frontend/js/components/sidebar.js +41 -7
- package/frontend/js/components/topNav.js +2 -14
- package/frontend/js/router.js +10 -0
- package/frontend/js/store.js +483 -7
- package/frontend/js/utils/inputClear.js +36 -0
- package/frontend/js/utils/syntheticData.js +240 -0
- package/frontend/js/views/backups.js +52 -0
- package/frontend/js/views/data.js +16 -0
- package/frontend/js/views/logs.js +339 -0
- package/frontend/js/views/settings.js +266 -30
- package/frontend/js/views/structure.js +6 -0
- package/frontend/js/views/tableAdvisor.js +385 -0
- package/frontend/js/views/tableDesigner.js +6 -0
- package/frontend/styles/components.css +149 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +31 -0
- package/package.json +4 -2
- package/server/mcp/stdioServer.js +272 -0
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +285 -2
- package/server/routes/logs.js +127 -0
- package/server/routes/settings.js +47 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +284 -2
- package/server/services/mcpStatusService.js +140 -0
- package/server/services/mcpToolService.js +274 -0
- package/server/services/sqlite/dataBrowserService.js +36 -0
- package/server/services/sqlite/introspection.js +226 -22
- package/server/services/sqlite/syntheticDataGenerator.js +728 -0
- package/server/services/sqlite/tableAdvisor.js +790 -0
- package/server/services/storage/appStateStore.js +821 -169
package/frontend/js/store.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
-
|
|
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
|
+
}
|