sqlite-hub 1.4.0 → 1.5.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 +9 -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/changelog.md +9 -1
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
- package/docs/todo.md +1 -13
- package/frontend/js/api.js +45 -0
- package/frontend/js/app.js +192 -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 +448 -0
- 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 +77 -27
- 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 +8 -0
- package/package.json +1 -1
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +285 -2
- package/server/routes/logs.js +127 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +45 -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 +773 -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: {} },
|
|
@@ -308,6 +338,18 @@ const state = {
|
|
|
308
338
|
loading: false,
|
|
309
339
|
error: null,
|
|
310
340
|
},
|
|
341
|
+
logs: {
|
|
342
|
+
items: [],
|
|
343
|
+
total: 0,
|
|
344
|
+
limit: LOG_PAGE_SIZE,
|
|
345
|
+
offset: 0,
|
|
346
|
+
hasMore: false,
|
|
347
|
+
loading: false,
|
|
348
|
+
loadingMore: false,
|
|
349
|
+
error: null,
|
|
350
|
+
filters: normalizeLogFilters(),
|
|
351
|
+
metadata: null,
|
|
352
|
+
},
|
|
311
353
|
dataBrowser: {
|
|
312
354
|
tables: [],
|
|
313
355
|
selectedTable: null,
|
|
@@ -332,6 +374,15 @@ const state = {
|
|
|
332
374
|
error: null,
|
|
333
375
|
saveError: null,
|
|
334
376
|
},
|
|
377
|
+
tableAdvisor: {
|
|
378
|
+
tables: [],
|
|
379
|
+
selectedTableName: null,
|
|
380
|
+
result: null,
|
|
381
|
+
loading: false,
|
|
382
|
+
analysisLoading: false,
|
|
383
|
+
error: null,
|
|
384
|
+
analysisError: null,
|
|
385
|
+
},
|
|
335
386
|
editor: {
|
|
336
387
|
sqlText: readStoredString(UI_PREFERENCE_STORAGE_KEYS.sqlEditorQueryDraft),
|
|
337
388
|
editorPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.sqlEditorEditorVisible, true),
|
|
@@ -622,6 +673,7 @@ function requiresActiveDatabase(routeName) {
|
|
|
622
673
|
return [
|
|
623
674
|
'overview',
|
|
624
675
|
'backups',
|
|
676
|
+
'logs',
|
|
625
677
|
'data',
|
|
626
678
|
'editor',
|
|
627
679
|
'editorResults',
|
|
@@ -629,6 +681,7 @@ function requiresActiveDatabase(routeName) {
|
|
|
629
681
|
'documents',
|
|
630
682
|
'structure',
|
|
631
683
|
'tableDesigner',
|
|
684
|
+
'tableAdvisor',
|
|
632
685
|
'mediaTaggingSetup',
|
|
633
686
|
'mediaTaggingQueue',
|
|
634
687
|
].includes(routeName);
|
|
@@ -1290,6 +1343,7 @@ function resolveQueryHistorySql(historyId) {
|
|
|
1290
1343
|
|
|
1291
1344
|
function clearRouteSlices() {
|
|
1292
1345
|
state.overview.error = null;
|
|
1346
|
+
state.logs.error = null;
|
|
1293
1347
|
state.dataBrowser.error = null;
|
|
1294
1348
|
state.dataBrowser.saveError = null;
|
|
1295
1349
|
state.charts.error = null;
|
|
@@ -1297,6 +1351,8 @@ function clearRouteSlices() {
|
|
|
1297
1351
|
state.charts.resultError = null;
|
|
1298
1352
|
state.tableDesigner.error = null;
|
|
1299
1353
|
state.tableDesigner.saveError = null;
|
|
1354
|
+
state.tableAdvisor.error = null;
|
|
1355
|
+
state.tableAdvisor.analysisError = null;
|
|
1300
1356
|
state.structure.error = null;
|
|
1301
1357
|
state.mediaTagging.error = null;
|
|
1302
1358
|
state.documents.error = null;
|
|
@@ -1310,6 +1366,15 @@ function setMissingDatabaseState() {
|
|
|
1310
1366
|
state.overview.data = null;
|
|
1311
1367
|
state.overview.error = error;
|
|
1312
1368
|
|
|
1369
|
+
state.logs.loading = false;
|
|
1370
|
+
state.logs.loadingMore = false;
|
|
1371
|
+
state.logs.items = [];
|
|
1372
|
+
state.logs.total = 0;
|
|
1373
|
+
state.logs.offset = 0;
|
|
1374
|
+
state.logs.hasMore = false;
|
|
1375
|
+
state.logs.error = error;
|
|
1376
|
+
state.logs.metadata = null;
|
|
1377
|
+
|
|
1313
1378
|
state.dataBrowser.loading = false;
|
|
1314
1379
|
state.dataBrowser.tableLoading = false;
|
|
1315
1380
|
state.dataBrowser.tables = [];
|
|
@@ -1350,6 +1415,14 @@ function setMissingDatabaseState() {
|
|
|
1350
1415
|
state.tableDesigner.error = error;
|
|
1351
1416
|
state.tableDesigner.saveError = null;
|
|
1352
1417
|
|
|
1418
|
+
state.tableAdvisor.loading = false;
|
|
1419
|
+
state.tableAdvisor.analysisLoading = false;
|
|
1420
|
+
state.tableAdvisor.tables = [];
|
|
1421
|
+
state.tableAdvisor.selectedTableName = null;
|
|
1422
|
+
state.tableAdvisor.result = null;
|
|
1423
|
+
state.tableAdvisor.error = error;
|
|
1424
|
+
state.tableAdvisor.analysisError = null;
|
|
1425
|
+
|
|
1353
1426
|
state.mediaTagging.loading = false;
|
|
1354
1427
|
state.mediaTagging.previewLoading = false;
|
|
1355
1428
|
state.mediaTagging.saving = false;
|
|
@@ -1495,6 +1568,65 @@ async function refreshSettingsState() {
|
|
|
1495
1568
|
}
|
|
1496
1569
|
}
|
|
1497
1570
|
|
|
1571
|
+
function buildLogRequestOptions({ append = false } = {}) {
|
|
1572
|
+
const filters = normalizeLogFilters(state.logs.filters);
|
|
1573
|
+
|
|
1574
|
+
return {
|
|
1575
|
+
...filters,
|
|
1576
|
+
search: filters.search,
|
|
1577
|
+
limit: state.logs.limit,
|
|
1578
|
+
offset: append ? state.logs.items.length : 0,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
async function loadLogs(options = {}) {
|
|
1583
|
+
const append = Boolean(options.append);
|
|
1584
|
+
|
|
1585
|
+
if (append && (state.logs.loading || state.logs.loadingMore || !state.logs.hasMore)) {
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (append) {
|
|
1590
|
+
state.logs.loadingMore = true;
|
|
1591
|
+
} else {
|
|
1592
|
+
state.logs.loading = true;
|
|
1593
|
+
state.logs.offset = 0;
|
|
1594
|
+
state.logs.error = null;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
emitChange();
|
|
1598
|
+
|
|
1599
|
+
try {
|
|
1600
|
+
const response = await api.getLogs(buildLogRequestOptions({ append }));
|
|
1601
|
+
const data = response.data ?? {};
|
|
1602
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
1603
|
+
|
|
1604
|
+
state.logs.items = append ? [...state.logs.items, ...items] : items;
|
|
1605
|
+
state.logs.total = Number(data.total ?? state.logs.items.length);
|
|
1606
|
+
state.logs.limit = Number(data.limit ?? state.logs.limit);
|
|
1607
|
+
state.logs.offset = Number(data.offset ?? 0);
|
|
1608
|
+
state.logs.hasMore = Boolean(data.hasMore);
|
|
1609
|
+
state.logs.metadata = response.metadata ?? data.filters ?? null;
|
|
1610
|
+
state.logs.filters = normalizeLogFilters({
|
|
1611
|
+
...state.logs.filters,
|
|
1612
|
+
...(data.filters ?? {}),
|
|
1613
|
+
...(response.metadata ?? {}),
|
|
1614
|
+
});
|
|
1615
|
+
state.logs.error = null;
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
state.logs.error = normalizeError(error);
|
|
1618
|
+
if (!append) {
|
|
1619
|
+
state.logs.items = [];
|
|
1620
|
+
state.logs.total = 0;
|
|
1621
|
+
state.logs.hasMore = false;
|
|
1622
|
+
}
|
|
1623
|
+
} finally {
|
|
1624
|
+
state.logs.loading = false;
|
|
1625
|
+
state.logs.loadingMore = false;
|
|
1626
|
+
emitChange();
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1498
1630
|
async function refreshBackupsState() {
|
|
1499
1631
|
if (!state.connections.active) {
|
|
1500
1632
|
state.backups.items = [];
|
|
@@ -1552,6 +1684,48 @@ export function setSettingsSection(section) {
|
|
|
1552
1684
|
emitChange();
|
|
1553
1685
|
}
|
|
1554
1686
|
|
|
1687
|
+
export async function setLogFilter(field, value) {
|
|
1688
|
+
if (!Object.prototype.hasOwnProperty.call(state.logs.filters, field)) {
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
state.logs.filters = normalizeLogFilters({
|
|
1693
|
+
...state.logs.filters,
|
|
1694
|
+
[field]: value,
|
|
1695
|
+
});
|
|
1696
|
+
state.logs.items = [];
|
|
1697
|
+
state.logs.total = 0;
|
|
1698
|
+
state.logs.hasMore = false;
|
|
1699
|
+
await loadLogs();
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
export function setLogSearchInput(value) {
|
|
1703
|
+
state.logs.filters = normalizeLogFilters({
|
|
1704
|
+
...state.logs.filters,
|
|
1705
|
+
searchInput: value,
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
export async function applyLogSearch(value = state.logs.filters.searchInput) {
|
|
1710
|
+
state.logs.filters = normalizeLogFilters({
|
|
1711
|
+
...state.logs.filters,
|
|
1712
|
+
searchInput: value,
|
|
1713
|
+
search: value,
|
|
1714
|
+
});
|
|
1715
|
+
state.logs.items = [];
|
|
1716
|
+
state.logs.total = 0;
|
|
1717
|
+
state.logs.hasMore = false;
|
|
1718
|
+
await loadLogs();
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
export async function refreshLogs() {
|
|
1722
|
+
await loadLogs();
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
export async function loadMoreLogs() {
|
|
1726
|
+
await loadLogs({ append: true });
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1555
1729
|
export async function checkSettingsAppVersion() {
|
|
1556
1730
|
state.settings.versionCheckLoading = true;
|
|
1557
1731
|
state.settings.versionCheckError = null;
|
|
@@ -2224,6 +2398,94 @@ async function loadData(version, route) {
|
|
|
2224
2398
|
}
|
|
2225
2399
|
}
|
|
2226
2400
|
|
|
2401
|
+
async function loadTableAdvisorResult(version, tableName) {
|
|
2402
|
+
if (!tableName) {
|
|
2403
|
+
state.tableAdvisor.result = null;
|
|
2404
|
+
state.tableAdvisor.analysisError = null;
|
|
2405
|
+
state.tableAdvisor.analysisLoading = false;
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
state.tableAdvisor.analysisLoading = true;
|
|
2410
|
+
state.tableAdvisor.analysisError = null;
|
|
2411
|
+
state.tableAdvisor.result = null;
|
|
2412
|
+
emitChange();
|
|
2413
|
+
|
|
2414
|
+
try {
|
|
2415
|
+
const response = await api.analyzeTableAdvisor(tableName);
|
|
2416
|
+
|
|
2417
|
+
if (version !== routeLoadVersion) {
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
state.tableAdvisor.result = response.data ?? null;
|
|
2422
|
+
state.tableAdvisor.analysisError = null;
|
|
2423
|
+
} catch (error) {
|
|
2424
|
+
if (version !== routeLoadVersion) {
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
state.tableAdvisor.result = null;
|
|
2429
|
+
state.tableAdvisor.analysisError = normalizeError(error);
|
|
2430
|
+
} finally {
|
|
2431
|
+
if (version === routeLoadVersion) {
|
|
2432
|
+
state.tableAdvisor.analysisLoading = false;
|
|
2433
|
+
emitChange();
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
async function loadTableAdvisor(version, route) {
|
|
2439
|
+
state.tableAdvisor.loading = true;
|
|
2440
|
+
state.tableAdvisor.error = null;
|
|
2441
|
+
state.tableAdvisor.analysisError = null;
|
|
2442
|
+
emitChange();
|
|
2443
|
+
|
|
2444
|
+
try {
|
|
2445
|
+
const response = await api.getDataTables();
|
|
2446
|
+
|
|
2447
|
+
if (version !== routeLoadVersion) {
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
const tables = response.data?.tables ?? [];
|
|
2452
|
+
const requestedTableName = route.params?.tableName ?? null;
|
|
2453
|
+
const selectedTableName =
|
|
2454
|
+
requestedTableName && tables.some(table => table.name === requestedTableName)
|
|
2455
|
+
? requestedTableName
|
|
2456
|
+
: (state.tableAdvisor.selectedTableName && tables.some(table => table.name === state.tableAdvisor.selectedTableName)
|
|
2457
|
+
? state.tableAdvisor.selectedTableName
|
|
2458
|
+
: (tables[0]?.name ?? null));
|
|
2459
|
+
|
|
2460
|
+
state.tableAdvisor.tables = tables;
|
|
2461
|
+
state.tableAdvisor.selectedTableName = selectedTableName;
|
|
2462
|
+
state.tableAdvisor.error = null;
|
|
2463
|
+
|
|
2464
|
+
if (!selectedTableName) {
|
|
2465
|
+
state.tableAdvisor.result = null;
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
state.tableAdvisor.loading = false;
|
|
2470
|
+
emitChange();
|
|
2471
|
+
await loadTableAdvisorResult(version, selectedTableName);
|
|
2472
|
+
} catch (error) {
|
|
2473
|
+
if (version !== routeLoadVersion) {
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
state.tableAdvisor.tables = [];
|
|
2478
|
+
state.tableAdvisor.selectedTableName = null;
|
|
2479
|
+
state.tableAdvisor.result = null;
|
|
2480
|
+
state.tableAdvisor.error = normalizeError(error);
|
|
2481
|
+
} finally {
|
|
2482
|
+
if (version === routeLoadVersion) {
|
|
2483
|
+
state.tableAdvisor.loading = false;
|
|
2484
|
+
emitChange();
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2227
2489
|
async function loadStructureDetail(version) {
|
|
2228
2490
|
const entry = getCurrentStructureEntry(state);
|
|
2229
2491
|
|
|
@@ -2671,6 +2933,9 @@ async function loadRouteData(route, options = {}) {
|
|
|
2671
2933
|
case 'backups':
|
|
2672
2934
|
await refreshBackupsState();
|
|
2673
2935
|
return;
|
|
2936
|
+
case 'logs':
|
|
2937
|
+
await loadLogs();
|
|
2938
|
+
return;
|
|
2674
2939
|
case 'data':
|
|
2675
2940
|
await loadData(version, route);
|
|
2676
2941
|
return;
|
|
@@ -2690,6 +2955,9 @@ async function loadRouteData(route, options = {}) {
|
|
|
2690
2955
|
case 'tableDesigner':
|
|
2691
2956
|
await loadTableDesigner(version, route);
|
|
2692
2957
|
return;
|
|
2958
|
+
case 'tableAdvisor':
|
|
2959
|
+
await loadTableAdvisor(version, route);
|
|
2960
|
+
return;
|
|
2693
2961
|
case 'mediaTaggingSetup':
|
|
2694
2962
|
case 'mediaTaggingQueue':
|
|
2695
2963
|
await loadMediaTagging(version);
|
|
@@ -2892,6 +3160,186 @@ export function openDataExportModal() {
|
|
|
2892
3160
|
emitChange();
|
|
2893
3161
|
}
|
|
2894
3162
|
|
|
3163
|
+
function getGenerateDataModalColumn(modal, columnName) {
|
|
3164
|
+
return (modal?.columns ?? []).find(column => column.name === columnName) ?? null;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
function resetGenerateDataPreview(modal) {
|
|
3168
|
+
if (!modal) {
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
modal.previewRows = [];
|
|
3173
|
+
modal.previewColumns = (modal.columns ?? []).map(column => column.name);
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
function buildGenerateDataPayload(modal) {
|
|
3177
|
+
return {
|
|
3178
|
+
rowCount: Number(modal.rowCount ?? 100),
|
|
3179
|
+
mappings: (modal.mappings ?? []).map(mapping => ({
|
|
3180
|
+
columnName: mapping.columnName,
|
|
3181
|
+
generator: mapping.generator,
|
|
3182
|
+
options: { ...(mapping.options ?? {}) },
|
|
3183
|
+
})),
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
export function openGenerateDataModal() {
|
|
3188
|
+
const table = state.dataBrowser.table;
|
|
3189
|
+
const tableName = table?.name ?? state.dataBrowser.selectedTable;
|
|
3190
|
+
|
|
3191
|
+
if (!tableName || !table) {
|
|
3192
|
+
pushToast('Select a table before generating rows.', 'alert');
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
const columns = (table.columnMeta ?? []).filter(column => column.visible && !column.generated);
|
|
3197
|
+
|
|
3198
|
+
state.modal = {
|
|
3199
|
+
kind: 'generate-data',
|
|
3200
|
+
tableName,
|
|
3201
|
+
columns,
|
|
3202
|
+
rowCount: 100,
|
|
3203
|
+
mappings: buildSyntheticDataMappings(columns, table.foreignKeys ?? []),
|
|
3204
|
+
previewColumns: columns.map(column => column.name),
|
|
3205
|
+
previewRows: [],
|
|
3206
|
+
previewLoading: false,
|
|
3207
|
+
previewRequestId: 0,
|
|
3208
|
+
error: null,
|
|
3209
|
+
submitting: false,
|
|
3210
|
+
};
|
|
3211
|
+
emitChange();
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
export function updateGenerateDataModal(field, value, options = {}) {
|
|
3215
|
+
if (state.modal?.kind !== 'generate-data') {
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
if (field === 'rowCount') {
|
|
3220
|
+
const nextValue = String(value ?? '');
|
|
3221
|
+
|
|
3222
|
+
if (state.modal.rowCount === nextValue) {
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
state.modal.rowCount = nextValue;
|
|
3227
|
+
state.modal.error = null;
|
|
3228
|
+
resetGenerateDataPreview(state.modal);
|
|
3229
|
+
if (options.notify !== false) {
|
|
3230
|
+
emitChange();
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
export function updateGenerateDataMapping(columnName, field, value, options = {}) {
|
|
3236
|
+
const modal = state.modal;
|
|
3237
|
+
|
|
3238
|
+
if (modal?.kind !== 'generate-data') {
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
const mapping = (modal.mappings ?? []).find(item => item.columnName === columnName);
|
|
3243
|
+
|
|
3244
|
+
if (!mapping) {
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
const column = getGenerateDataModalColumn(modal, columnName);
|
|
3249
|
+
|
|
3250
|
+
if (field === 'generator') {
|
|
3251
|
+
const generator = normalizeSyntheticGeneratorType(value, mapping.generator);
|
|
3252
|
+
|
|
3253
|
+
if (mapping.generator === generator) {
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
mapping.generator = generator;
|
|
3258
|
+
mapping.options = getDefaultSyntheticOptions(generator, column ?? {});
|
|
3259
|
+
} else {
|
|
3260
|
+
const nextValue = value;
|
|
3261
|
+
|
|
3262
|
+
if ((mapping.options ?? {})[field] === nextValue) {
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
mapping.options = {
|
|
3267
|
+
...(mapping.options ?? {}),
|
|
3268
|
+
[field]: nextValue,
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
modal.error = null;
|
|
3273
|
+
resetGenerateDataPreview(modal);
|
|
3274
|
+
if (options.notify !== false) {
|
|
3275
|
+
emitChange();
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
export async function previewGenerateDataRows() {
|
|
3280
|
+
const modal = state.modal;
|
|
3281
|
+
|
|
3282
|
+
if (modal?.kind !== 'generate-data') {
|
|
3283
|
+
return null;
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
const requestId = (modal.previewRequestId ?? 0) + 1;
|
|
3287
|
+
modal.previewRequestId = requestId;
|
|
3288
|
+
modal.previewLoading = true;
|
|
3289
|
+
modal.error = null;
|
|
3290
|
+
emitChange();
|
|
3291
|
+
|
|
3292
|
+
try {
|
|
3293
|
+
const response = await api.previewSyntheticDataRows(modal.tableName, buildGenerateDataPayload(modal));
|
|
3294
|
+
|
|
3295
|
+
if (state.modal?.kind !== 'generate-data' || state.modal.previewRequestId !== requestId) {
|
|
3296
|
+
return null;
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
state.modal.previewColumns = response.data?.columns ?? [];
|
|
3300
|
+
state.modal.previewRows = response.data?.rows ?? [];
|
|
3301
|
+
state.modal.error = null;
|
|
3302
|
+
return response.data;
|
|
3303
|
+
} catch (error) {
|
|
3304
|
+
if (state.modal?.kind === 'generate-data' && state.modal.previewRequestId === requestId) {
|
|
3305
|
+
state.modal.error = normalizeError(error);
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
return null;
|
|
3309
|
+
} finally {
|
|
3310
|
+
if (state.modal?.kind === 'generate-data' && state.modal.previewRequestId === requestId) {
|
|
3311
|
+
state.modal.previewLoading = false;
|
|
3312
|
+
emitChange();
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
export async function submitGenerateDataRows() {
|
|
3318
|
+
const modal = state.modal;
|
|
3319
|
+
|
|
3320
|
+
if (modal?.kind !== 'generate-data') {
|
|
3321
|
+
return null;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
startModalSubmission();
|
|
3325
|
+
|
|
3326
|
+
try {
|
|
3327
|
+
const response = await api.insertSyntheticDataRows(modal.tableName, buildGenerateDataPayload(modal));
|
|
3328
|
+
|
|
3329
|
+
if (state.modal?.kind === 'generate-data') {
|
|
3330
|
+
closeModalInternal();
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
pushToast(response.message || `Generated ${response.data?.insertedRowCount ?? modal.rowCount} rows for ${modal.tableName}.`, 'success');
|
|
3334
|
+
await loadDataTable(++routeLoadVersion);
|
|
3335
|
+
clearDataBrowserRowSelectionState();
|
|
3336
|
+
return response.data;
|
|
3337
|
+
} catch (error) {
|
|
3338
|
+
withModalError(error);
|
|
3339
|
+
return null;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
2895
3343
|
export function openCopyColumnModal({ scope = 'editor', columnName = '', mode = 'column' } = {}) {
|
|
2896
3344
|
const resultScope = normalizeCopyColumnScope(scope);
|
|
2897
3345
|
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
|
+
}
|