sqlite-hub 0.17.2 → 1.1.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 (85) hide show
  1. package/README.md +143 -47
  2. package/bin/sqlite-hub.js +206 -444
  3. package/examples/api/queries.js +31 -0
  4. package/examples/api/rows.js +27 -0
  5. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  6. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  7. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  8. package/frontend/assets/mockups/connections_1200.webp +0 -0
  9. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  10. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  11. package/frontend/assets/mockups/documents_1200.webp +0 -0
  12. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  13. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  14. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  15. package/frontend/assets/mockups/overview_1200.webp +0 -0
  16. package/frontend/assets/mockups/settings_1200.webp +0 -0
  17. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  18. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  19. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  20. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  21. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  22. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  23. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  24. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  25. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  26. package/frontend/js/api.js +25 -0
  27. package/frontend/js/app.js +263 -34
  28. package/frontend/js/components/formControls.js +38 -0
  29. package/frontend/js/components/modal.js +77 -21
  30. package/frontend/js/components/queryChartRenderer.js +28 -3
  31. package/frontend/js/components/queryEditor.js +20 -24
  32. package/frontend/js/components/queryHistoryDetail.js +1 -1
  33. package/frontend/js/components/queryHistoryHeader.js +48 -0
  34. package/frontend/js/components/queryHistoryList.js +132 -0
  35. package/frontend/js/components/queryHistoryPanel.js +72 -136
  36. package/frontend/js/components/structureGraph.js +699 -89
  37. package/frontend/js/components/tableDesignerEditor.js +3 -5
  38. package/frontend/js/store.js +188 -7
  39. package/frontend/js/utils/exportFilenames.js +3 -1
  40. package/frontend/js/views/charts.js +320 -169
  41. package/frontend/js/views/connections.js +59 -64
  42. package/frontend/js/views/data.js +12 -20
  43. package/frontend/js/views/editor.js +0 -2
  44. package/frontend/js/views/settings.js +219 -5
  45. package/frontend/js/views/structure.js +27 -13
  46. package/frontend/styles/components.css +155 -0
  47. package/frontend/styles/structure-graph.css +140 -35
  48. package/frontend/styles/tailwind.generated.css +2 -2
  49. package/frontend/styles/views.css +12 -6
  50. package/package.json +7 -6
  51. package/server/middleware/apiTokenAuth.js +30 -0
  52. package/server/routes/connections.js +17 -0
  53. package/server/routes/export.js +89 -22
  54. package/server/routes/externalApi.js +304 -0
  55. package/server/routes/settings.js +90 -21
  56. package/server/server.js +22 -1
  57. package/server/services/apiTokenService.js +101 -0
  58. package/server/services/appInfoService.js +215 -0
  59. package/server/services/databaseCommandService.js +443 -0
  60. package/server/services/nativeFileDialogService.js +93 -1
  61. package/server/services/sqlite/exportService.js +307 -22
  62. package/server/services/storage/appStateStore.js +113 -0
  63. package/server/utils/errors.js +7 -0
  64. package/tests/api-token-auth.test.js +236 -0
  65. package/tests/cli-args.test.js +16 -3
  66. package/tests/cli-service-delegation.test.js +43 -0
  67. package/tests/connections-file-dialog-route.test.js +43 -0
  68. package/tests/copy-column-modal.test.js +34 -0
  69. package/tests/database-command-service.test.js +139 -0
  70. package/tests/export-blob.test.js +54 -1
  71. package/tests/export-filenames.test.js +4 -0
  72. package/tests/form-controls.test.js +34 -0
  73. package/tests/native-file-dialog.test.js +27 -0
  74. package/tests/settings-api-tokens-route.test.js +97 -0
  75. package/tests/settings-metadata.test.js +99 -1
  76. package/tests/settings-view.test.js +75 -0
  77. package/frontend/assets/mockups/connections.png +0 -0
  78. package/frontend/assets/mockups/data.png +0 -0
  79. package/frontend/assets/mockups/data_row_editor.png +0 -0
  80. package/frontend/assets/mockups/home.png +0 -0
  81. package/frontend/assets/mockups/sql_editor.png +0 -0
  82. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  83. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  84. package/frontend/assets/mockups/structure.png +0 -0
  85. package/frontend/assets/mockups/structure_inspector.png +0 -0
@@ -351,11 +351,9 @@ export function renderTableDesignerEditor(state) {
351
351
  value="${escapeHtml(draft.tableName)}"
352
352
  />
353
353
  ${
354
- draft.mode === "edit"
355
- ? `<div class="status-badge status-badge--muted">FROM ${escapeHtml(
356
- draft.originalTableName
357
- )}</div>`
358
- : `<div class="status-badge status-badge--primary">CREATE</div>`
354
+ draft.mode === "create"
355
+ ? `<div class="status-badge status-badge--primary">CREATE</div>`
356
+ : ""
359
357
  }
360
358
  </div>
361
359
  <div class="table-designer-main__subtitle">
@@ -49,6 +49,7 @@ const UI_PREFERENCE_STORAGE_KEYS = {
49
49
  dataTablesVisible: 'sqlite_hub_data_tables_visible',
50
50
  structureTablesVisible: 'sqlite_hub_structure_tables_visible',
51
51
  chartsHistoryVisible: 'sqlite_hub_charts_history_visible',
52
+ chartsQueryVisible: 'sqlite_hub_charts_query_visible',
52
53
  chartsResultsVisible: 'sqlite_hub_charts_results_visible',
53
54
  tableDesignerSqlPreviewVisible: 'sqlite_hub_table_designer_sql_preview_visible',
54
55
  documentsEditorVisible: 'sqlite_hub_documents_editor_visible',
@@ -63,6 +64,8 @@ const TEXT_EXPORT_FORMAT_LABELS = {
63
64
  csv: 'CSV',
64
65
  tsv: 'TSV',
65
66
  md: 'Markdown',
67
+ json: 'JSON',
68
+ parquet: 'Parquet',
66
69
  };
67
70
  const MISSING_DATABASE_ERROR = {
68
71
  code: 'ACTIVE_DATABASE_REQUIRED',
@@ -253,10 +256,18 @@ const state = {
253
256
  },
254
257
  settings: {
255
258
  data: { ...DEFAULT_SETTINGS },
259
+ section: 'information',
260
+ apiTokens: [],
261
+ tokenDatabase: null,
262
+ createdApiToken: null,
263
+ tokenSaving: false,
256
264
  loading: false,
257
265
  error: null,
258
266
  appVersion: null,
259
267
  sqliteVersion: null,
268
+ versionCheck: null,
269
+ versionCheckLoading: false,
270
+ versionCheckError: null,
260
271
  },
261
272
  overview: {
262
273
  data: null,
@@ -326,10 +337,13 @@ const state = {
326
337
  loading: false,
327
338
  error: null,
328
339
  historyTab: readStoredChartsHistoryTab(),
340
+ historySearchInput: '',
341
+ historySearch: '',
329
342
  historyPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true),
343
+ detailPanelVisible: false,
330
344
  selectedHistoryId: null,
331
345
  chartHeightPreset: 'medium',
332
- sqlExpanded: false,
346
+ queryVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true),
333
347
  resultsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true),
334
348
  detail: null,
335
349
  detailLoading: false,
@@ -924,10 +938,13 @@ function resetChartsState() {
924
938
  state.charts.loading = false;
925
939
  state.charts.error = null;
926
940
  state.charts.historyTab = readStoredChartsHistoryTab(state.charts.historyTab);
941
+ state.charts.historySearchInput = '';
942
+ state.charts.historySearch = '';
927
943
  state.charts.historyPanelVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true);
944
+ state.charts.detailPanelVisible = false;
928
945
  state.charts.selectedHistoryId = null;
929
946
  state.charts.chartHeightPreset = 'medium';
930
- state.charts.sqlExpanded = false;
947
+ state.charts.queryVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true);
931
948
  state.charts.resultsVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true);
932
949
  state.charts.detail = null;
933
950
  state.charts.detailLoading = false;
@@ -1014,7 +1031,7 @@ function normalizeChartsHeightPreset(value) {
1014
1031
  }
1015
1032
 
1016
1033
  function normalizeChartsHistoryTab(value) {
1017
- return ['recent', 'saved'].includes(value) ? value : 'recent';
1034
+ return ['recent', 'saved', 'unsaved'].includes(value) ? value : 'recent';
1018
1035
  }
1019
1036
 
1020
1037
  function mergeQueryHistoryItemWithChartSummary(updatedItem, fallbackItem = null) {
@@ -1310,6 +1327,9 @@ async function refreshSettingsState() {
1310
1327
  };
1311
1328
  state.settings.appVersion = response.metadata?.appVersion ?? null;
1312
1329
  state.settings.sqliteVersion = response.metadata?.sqliteVersion ?? null;
1330
+ state.settings.apiTokens = response.metadata?.apiTokens ?? [];
1331
+ state.settings.tokenDatabase = response.metadata?.activeDatabase ?? null;
1332
+ state.settings.createdApiToken = null;
1313
1333
  } catch (error) {
1314
1334
  state.settings.error = normalizeError(error);
1315
1335
  } finally {
@@ -1318,6 +1338,130 @@ async function refreshSettingsState() {
1318
1338
  }
1319
1339
  }
1320
1340
 
1341
+ export async function createSettingsApiToken(name) {
1342
+ state.settings.tokenSaving = true;
1343
+ state.settings.error = null;
1344
+ emitChange();
1345
+
1346
+ try {
1347
+ const response = await api.createApiToken(name);
1348
+ state.settings.apiTokens = response.metadata?.apiTokens ?? state.settings.apiTokens;
1349
+ state.settings.tokenDatabase = response.metadata?.activeDatabase ?? state.settings.tokenDatabase;
1350
+ state.settings.createdApiToken = response.data ?? null;
1351
+ pushToast('API token created. Store it now; it will not be shown again.', 'success');
1352
+ return response.data ?? null;
1353
+ } catch (error) {
1354
+ state.settings.error = normalizeError(error);
1355
+ return null;
1356
+ } finally {
1357
+ state.settings.tokenSaving = false;
1358
+ emitChange();
1359
+ }
1360
+ }
1361
+
1362
+ export function setSettingsSection(section) {
1363
+ const normalizedSection = section === 'api-tokens' ? 'api-tokens' : 'information';
1364
+
1365
+ if (state.settings.section === normalizedSection) {
1366
+ return;
1367
+ }
1368
+
1369
+ state.settings.section = normalizedSection;
1370
+ emitChange();
1371
+ }
1372
+
1373
+ export async function checkSettingsAppVersion() {
1374
+ state.settings.versionCheckLoading = true;
1375
+ state.settings.versionCheckError = null;
1376
+ emitChange();
1377
+
1378
+ try {
1379
+ const response = await api.checkAppVersion();
1380
+ const result = response.data ?? null;
1381
+
1382
+ state.settings.versionCheck = result;
1383
+ pushToast(
1384
+ result?.updateAvailable
1385
+ ? `SQLite Hub v${result.latestVersion} is available.`
1386
+ : 'SQLite Hub is up to date.',
1387
+ 'success',
1388
+ );
1389
+ return result;
1390
+ } catch (error) {
1391
+ state.settings.versionCheckError = normalizeError(error);
1392
+ pushToast('Version check failed.', 'alert');
1393
+ return null;
1394
+ } finally {
1395
+ state.settings.versionCheckLoading = false;
1396
+ emitChange();
1397
+ }
1398
+ }
1399
+
1400
+ export async function deleteSettingsApiToken(tokenId) {
1401
+ state.settings.tokenSaving = true;
1402
+ state.settings.error = null;
1403
+ emitChange();
1404
+
1405
+ try {
1406
+ const response = await api.deleteApiToken(tokenId);
1407
+ state.settings.apiTokens = response.metadata?.apiTokens ?? [];
1408
+ state.settings.tokenDatabase = response.metadata?.activeDatabase ?? state.settings.tokenDatabase;
1409
+
1410
+ if (state.settings.createdApiToken?.id === tokenId) {
1411
+ state.settings.createdApiToken = null;
1412
+ }
1413
+
1414
+ pushToast('API token deleted.', 'success');
1415
+ return true;
1416
+ } catch (error) {
1417
+ if (state.modal?.kind === 'delete-api-token') {
1418
+ withModalError(error);
1419
+ } else {
1420
+ state.settings.error = normalizeError(error);
1421
+ }
1422
+ return false;
1423
+ } finally {
1424
+ state.settings.tokenSaving = false;
1425
+ emitChange();
1426
+ }
1427
+ }
1428
+
1429
+ export function openDeleteSettingsApiTokenModal(tokenId) {
1430
+ const token = state.settings.apiTokens.find(candidate => candidate.id === tokenId);
1431
+
1432
+ if (!token) {
1433
+ pushToast('The selected API token could not be loaded.', 'alert');
1434
+ return;
1435
+ }
1436
+
1437
+ state.modal = {
1438
+ kind: 'delete-api-token',
1439
+ tokenId: token.id,
1440
+ tokenName: token.name,
1441
+ tokenPrefix: token.tokenPrefix,
1442
+ databaseLabel: state.settings.tokenDatabase?.label ?? 'active database',
1443
+ error: null,
1444
+ submitting: false,
1445
+ };
1446
+ emitChange();
1447
+ }
1448
+
1449
+ export async function submitDeleteSettingsApiTokenConfirmation() {
1450
+ if (state.modal?.kind !== 'delete-api-token') {
1451
+ return false;
1452
+ }
1453
+
1454
+ const tokenId = state.modal.tokenId;
1455
+ startModalSubmission();
1456
+ const deleted = await deleteSettingsApiToken(tokenId);
1457
+
1458
+ if (deleted) {
1459
+ closeModalInternal();
1460
+ }
1461
+
1462
+ return deleted;
1463
+ }
1464
+
1321
1465
  async function loadQueryHistoryDetail(historyId, options = {}) {
1322
1466
  const normalizedId = String(historyId ?? '').trim();
1323
1467
  const numericId = Number(normalizedId);
@@ -1452,7 +1596,8 @@ async function loadChartsDetail(historyId) {
1452
1596
 
1453
1597
  if (!Number.isInteger(numericId) || numericId < 1) {
1454
1598
  state.charts.selectedHistoryId = null;
1455
- state.charts.sqlExpanded = false;
1599
+ state.charts.detailPanelVisible = false;
1600
+ state.charts.queryVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true);
1456
1601
  state.charts.resultsVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true);
1457
1602
  state.charts.detail = null;
1458
1603
  state.charts.detailLoading = false;
@@ -1465,7 +1610,7 @@ async function loadChartsDetail(historyId) {
1465
1610
  }
1466
1611
 
1467
1612
  state.charts.selectedHistoryId = numericId;
1468
- state.charts.sqlExpanded = false;
1613
+ state.charts.queryVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true);
1469
1614
  state.charts.resultsVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true);
1470
1615
  state.charts.detail = null;
1471
1616
  state.charts.detailLoading = true;
@@ -3343,7 +3488,8 @@ export async function openEditorRowUpdatePreview(rowIndex, values) {
3343
3488
  }
3344
3489
 
3345
3490
  export function toggleChartsSqlPanel() {
3346
- state.charts.sqlExpanded = !state.charts.sqlExpanded;
3491
+ state.charts.queryVisible = !state.charts.queryVisible;
3492
+ storeBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, state.charts.queryVisible);
3347
3493
  emitChange();
3348
3494
  }
3349
3495
 
@@ -3365,6 +3511,17 @@ export function setChartsHistoryPanelVisibility(visible) {
3365
3511
  emitChange();
3366
3512
  }
3367
3513
 
3514
+ export function setChartsDetailPanelVisibility(visible) {
3515
+ const nextValue = typeof visible === 'boolean' ? visible : !Boolean(state.charts.detailPanelVisible);
3516
+
3517
+ if (state.charts.detailPanelVisible === nextValue) {
3518
+ return;
3519
+ }
3520
+
3521
+ state.charts.detailPanelVisible = nextValue;
3522
+ emitChange();
3523
+ }
3524
+
3368
3525
  export function setChartsHeightPreset(preset) {
3369
3526
  const nextPreset = normalizeChartsHeightPreset(preset);
3370
3527
 
@@ -3392,6 +3549,18 @@ export function setChartsHistoryTab(tab) {
3392
3549
  emitChange();
3393
3550
  }
3394
3551
 
3552
+ export function setChartsHistorySearchInput(query) {
3553
+ const nextQuery = String(query ?? '');
3554
+
3555
+ if (state.charts.historySearchInput === nextQuery && state.charts.historySearch === nextQuery.trim()) {
3556
+ return;
3557
+ }
3558
+
3559
+ state.charts.historySearchInput = nextQuery;
3560
+ state.charts.historySearch = nextQuery.trim();
3561
+ emitChange();
3562
+ }
3563
+
3395
3564
  export function updateCurrentQueryChartDraftField(field, value) {
3396
3565
  if (state.modal?.kind !== 'chart-editor' || !state.modal.draft) {
3397
3566
  return;
@@ -3490,6 +3659,16 @@ export async function submitCreateConnection(payload) {
3490
3659
  }
3491
3660
  }
3492
3661
 
3662
+ export async function chooseOpenDatabasePath() {
3663
+ try {
3664
+ const response = await api.chooseOpenDatabasePath();
3665
+ return response.data?.path ?? null;
3666
+ } catch (error) {
3667
+ pushToast(normalizeError(error).message || 'Native file dialog could not be opened.', 'alert');
3668
+ return null;
3669
+ }
3670
+ }
3671
+
3493
3672
  export async function chooseCreateDatabasePath() {
3494
3673
  try {
3495
3674
  const response = await api.chooseCreateDatabasePath();
@@ -3820,7 +3999,9 @@ export function openQueryHistoryInEditor(historyId, options = {}) {
3820
3999
  clearQueryHistoryDetailState();
3821
4000
  state.editor.sqlText = options.append ? [state.editor.sqlText.trim(), rawSql].filter(Boolean).join('\n\n') : rawSql;
3822
4001
  storeString(UI_PREFERENCE_STORAGE_KEYS.sqlEditorQueryDraft, state.editor.sqlText);
3823
- emitChange();
4002
+ if (options.notify !== false) {
4003
+ emitChange();
4004
+ }
3824
4005
  return true;
3825
4006
  }
3826
4007
 
@@ -2,6 +2,8 @@ export const TEXT_EXPORT_EXTENSIONS = {
2
2
  csv: "csv",
3
3
  tsv: "tsv",
4
4
  md: "md",
5
+ json: "json",
6
+ parquet: "parquet",
5
7
  };
6
8
 
7
9
  export function normalizeTextExportFormat(format = "csv") {
@@ -10,7 +12,7 @@ export function normalizeTextExportFormat(format = "csv") {
10
12
  }
11
13
 
12
14
  function stripKnownTextExportExtension(value = "") {
13
- return String(value).replace(/\.(csv|tsv|md)$/i, "");
15
+ return String(value).replace(/\.(csv|tsv|md|json|parquet)$/i, "");
14
16
  }
15
17
 
16
18
  export function sanitizeExportFilenameBase(value, fallback = "export") {