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.
Files changed (40) hide show
  1. package/README.md +9 -0
  2. package/bin/sqlite-hub.js +555 -122
  3. package/docs/API.md +30 -0
  4. package/docs/CLI.md +22 -0
  5. package/docs/CLI_API_PARITY.md +61 -0
  6. package/docs/changelog.md +9 -1
  7. package/docs/guidelines/AGENTS.md +32 -0
  8. package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
  9. package/docs/todo.md +1 -13
  10. package/frontend/js/api.js +45 -0
  11. package/frontend/js/app.js +192 -2
  12. package/frontend/js/components/connectionCard.js +3 -1
  13. package/frontend/js/components/modal.js +356 -15
  14. package/frontend/js/components/sidebar.js +41 -7
  15. package/frontend/js/components/topNav.js +2 -14
  16. package/frontend/js/router.js +10 -0
  17. package/frontend/js/store.js +448 -0
  18. package/frontend/js/utils/inputClear.js +36 -0
  19. package/frontend/js/utils/syntheticData.js +240 -0
  20. package/frontend/js/views/backups.js +52 -0
  21. package/frontend/js/views/data.js +16 -0
  22. package/frontend/js/views/logs.js +339 -0
  23. package/frontend/js/views/settings.js +77 -27
  24. package/frontend/js/views/structure.js +6 -0
  25. package/frontend/js/views/tableAdvisor.js +385 -0
  26. package/frontend/js/views/tableDesigner.js +6 -0
  27. package/frontend/styles/components.css +149 -0
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/views.css +8 -0
  30. package/package.json +1 -1
  31. package/server/routes/data.js +45 -0
  32. package/server/routes/externalApi.js +285 -2
  33. package/server/routes/logs.js +127 -0
  34. package/server/server.js +3 -0
  35. package/server/services/databaseCommandService.js +45 -0
  36. package/server/services/sqlite/dataBrowserService.js +36 -0
  37. package/server/services/sqlite/introspection.js +226 -22
  38. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  39. package/server/services/sqlite/tableAdvisor.js +790 -0
  40. package/server/services/storage/appStateStore.js +773 -169
@@ -48,6 +48,7 @@ import {
48
48
  initializeApp,
49
49
  insertMarkdownIntoLastOpenDocument,
50
50
  loadMoreQueryHistory,
51
+ loadMoreLogs,
51
52
  openModal,
52
53
  openOverviewInFinder,
53
54
  openQueryHistoryInEditor,
@@ -64,6 +65,7 @@ import {
64
65
  openRestoreBackupModal,
65
66
  openDeleteQueryChartModal,
66
67
  openDataExportModal,
68
+ openGenerateDataModal,
67
69
  openDeleteDocumentModal,
68
70
  openQueryExportModal,
69
71
  openDataRowByIdentity,
@@ -77,6 +79,7 @@ import {
77
79
  openDataRowUpdatePreview,
78
80
  openEditorRowUpdatePreview,
79
81
  refreshCurrentRoute,
82
+ refreshLogs,
80
83
  refreshBackups,
81
84
  refreshMediaTaggingPreview,
82
85
  removeConnection,
@@ -94,6 +97,9 @@ import {
94
97
  selectQueryHistoryItem,
95
98
  selectStructureEntry,
96
99
  setDocumentsSearchQuery,
100
+ setLogFilter,
101
+ setLogSearchInput,
102
+ applyLogSearch,
97
103
  setTableDesignerSearchQuery,
98
104
  setTableDesignerSqlPreviewVisibility,
99
105
  toggleTableDesignerTablesPanel,
@@ -126,7 +132,9 @@ import {
126
132
  submitBackupSafetyChoice,
127
133
  submitCreateBackupConfirmation,
128
134
  submitEditBackupConfirmation,
135
+ submitGenerateDataRows,
129
136
  submitRowUpdatePreviewConfirmation,
137
+ previewGenerateDataRows,
130
138
  setQueryHistoryPanelVisibility,
131
139
  sortDataTableByColumn,
132
140
  sortEditorResultsByColumn,
@@ -170,6 +178,8 @@ import {
170
178
  updateCurrentTableDesignerColumnField,
171
179
  updateCurrentTableDesignerConstraintField,
172
180
  updateCurrentTableDesignerField,
181
+ updateGenerateDataMapping,
182
+ updateGenerateDataModal,
173
183
  updateGenerateTypesModal,
174
184
  addCurrentTableDesignerColumn,
175
185
  applyCurrentMediaTaggingSelection,
@@ -182,10 +192,12 @@ import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
182
192
  import { renderDocumentsView } from './views/documents.js';
183
193
  import { renderEditorView } from './views/editor.js';
184
194
  import { renderLandingView } from './views/landing.js';
195
+ import { renderLogsView, renderLogTable } from './views/logs.js';
185
196
  import { renderMediaTaggingView } from './views/mediaTagging.js';
186
197
  import { renderOverviewView } from './views/overview.js';
187
198
  import { renderSettingsView } from './views/settings.js';
188
199
  import { renderStructureView } from './views/structure.js';
200
+ import { renderTableAdvisorView } from './views/tableAdvisor.js';
189
201
  import { renderTableDesignerView } from './views/tableDesigner.js';
190
202
  import { replaceChildrenFromRenderedMarkup, replaceElementFromRenderedMarkup } from './utils/dom.js';
191
203
  import {
@@ -200,6 +212,7 @@ import {
200
212
  detectFilePathValue,
201
213
  getPathTypeLabel,
202
214
  } from './utils/filePathPreview.js';
215
+ import { clearInputForEscape } from './utils/inputClear.js';
203
216
  import { formatSqlQuery } from './utils/sqlFormatter.js';
204
217
  import {
205
218
  buildDataRowEditorJsonObject,
@@ -263,6 +276,7 @@ const ROUTE_TITLE_SEGMENTS = {
263
276
  backups: 'Backups',
264
277
  overview: 'Overview',
265
278
  data: 'Data',
279
+ tableAdvisor: 'Table Advisor',
266
280
  structure: 'Structure',
267
281
  editor: 'SQL Editor',
268
282
  editorResults: 'SQL Editor',
@@ -272,6 +286,7 @@ const ROUTE_TITLE_SEGMENTS = {
272
286
  mediaTaggingSetup: 'Media Tagging',
273
287
  mediaTaggingQueue: 'Tagging Queue',
274
288
  settings: 'Settings',
289
+ logs: 'Logs',
275
290
  notFound: 'Not Found',
276
291
  };
277
292
 
@@ -973,6 +988,92 @@ function patchChartsDetailUi(state, { preserveCharts = false } = {}) {
973
988
  return true;
974
989
  }
975
990
 
991
+ function getLogsActiveDatabaseId(state) {
992
+ return String(state.connections?.active?.id ?? state.logs?.metadata?.activeDatabase?.id ?? '');
993
+ }
994
+
995
+ function syncLogsFilterControls(state) {
996
+ const filters = state.logs?.filters ?? {};
997
+
998
+ for (const button of shellRefs.view.querySelectorAll('[data-action="set-log-filter"][data-field][data-value]')) {
999
+ if (!(button instanceof HTMLElement)) {
1000
+ continue;
1001
+ }
1002
+
1003
+ const field = button.dataset.field;
1004
+ const value = button.dataset.value;
1005
+ const active = String(filters[field] ?? '') === String(value ?? '');
1006
+
1007
+ button.classList.toggle('is-active', active);
1008
+ button.setAttribute('aria-pressed', active ? 'true' : 'false');
1009
+ }
1010
+ }
1011
+
1012
+ function syncLogsMetaStrip(state) {
1013
+ const logs = state.logs ?? {};
1014
+ const values = {
1015
+ visible: formatNumber((logs.items ?? []).length),
1016
+ matched: formatNumber(logs.total ?? 0),
1017
+ scope: logs.metadata?.activeDatabase?.label ?? 'Active Database',
1018
+ };
1019
+
1020
+ for (const [key, value] of Object.entries(values)) {
1021
+ const node = shellRefs.view.querySelector(`[data-logs-meta="${key}"]`);
1022
+
1023
+ if (node instanceof HTMLElement) {
1024
+ node.textContent = value;
1025
+ node.title = value;
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ function patchLogsTableUi(state) {
1031
+ const logsView = shellRefs.view.querySelector('[data-logs-view]');
1032
+
1033
+ if (!(logsView instanceof HTMLElement)) {
1034
+ return false;
1035
+ }
1036
+
1037
+ const activeDatabaseId = getLogsActiveDatabaseId(state);
1038
+
1039
+ if (String(logsView.dataset.logsActiveDatabaseId ?? '') !== activeDatabaseId) {
1040
+ return false;
1041
+ }
1042
+
1043
+ const currentTable = logsView.querySelector('[data-logs-table]');
1044
+
1045
+ if (!(currentTable instanceof HTMLElement)) {
1046
+ return false;
1047
+ }
1048
+
1049
+ const currentScrollNode = currentTable.querySelector('[data-logs-table-scroll]');
1050
+ const scrollState =
1051
+ currentScrollNode instanceof HTMLElement
1052
+ ? {
1053
+ left: currentScrollNode.scrollLeft,
1054
+ top: currentScrollNode.scrollTop,
1055
+ }
1056
+ : null;
1057
+ const patched = replaceElementFromRenderedMarkup(currentTable, renderLogTable(state.logs ?? {}));
1058
+
1059
+ if (!patched) {
1060
+ return false;
1061
+ }
1062
+
1063
+ if (scrollState) {
1064
+ const nextScrollNode = logsView.querySelector('[data-logs-table-scroll]');
1065
+
1066
+ if (nextScrollNode instanceof HTMLElement) {
1067
+ nextScrollNode.scrollLeft = scrollState.left;
1068
+ nextScrollNode.scrollTop = scrollState.top;
1069
+ }
1070
+ }
1071
+
1072
+ syncLogsFilterControls(state);
1073
+ syncLogsMetaStrip(state);
1074
+ return true;
1075
+ }
1076
+
976
1077
  function readFileAsBase64(file) {
977
1078
  return new Promise((resolve, reject) => {
978
1079
  const reader = new FileReader();
@@ -1112,12 +1213,16 @@ function resolveView(state) {
1112
1213
  return renderStructureView(state);
1113
1214
  case 'tableDesigner':
1114
1215
  return renderTableDesignerView(state);
1216
+ case 'tableAdvisor':
1217
+ return renderTableAdvisorView(state);
1115
1218
  case 'mediaTaggingSetup':
1116
1219
  return renderMediaTaggingView(state, { subView: 'setup' });
1117
1220
  case 'mediaTaggingQueue':
1118
1221
  return renderMediaTaggingView(state, { subView: 'queue' });
1119
1222
  case 'settings':
1120
1223
  return renderSettingsView(state);
1224
+ case 'logs':
1225
+ return renderLogsView(state);
1121
1226
  default:
1122
1227
  return renderNotFoundView();
1123
1228
  }
@@ -1403,9 +1508,11 @@ function renderApp(state) {
1403
1508
  'editorResults',
1404
1509
  'data',
1405
1510
  'charts',
1511
+ 'logs',
1406
1512
  'documents',
1407
1513
  'structure',
1408
1514
  'tableDesigner',
1515
+ 'tableAdvisor',
1409
1516
  'mediaTaggingSetup',
1410
1517
  'mediaTaggingQueue',
1411
1518
  ].includes(state.route.name);
@@ -1457,8 +1564,8 @@ function renderApp(state) {
1457
1564
  pendingNewTableDesignerAutofocus = false;
1458
1565
  }
1459
1566
 
1460
- const canPatchChartsMain =
1461
- mainChanged && previousRouteName === 'charts' && state.route.name === 'charts';
1567
+ const canPatchChartsMain = mainChanged && previousRouteName === 'charts' && state.route.name === 'charts';
1568
+ const canPatchLogsMain = mainChanged && previousRouteName === 'logs' && state.route.name === 'logs';
1462
1569
  let mainPatched = false;
1463
1570
  let preservedChartsDom = false;
1464
1571
 
@@ -1484,6 +1591,10 @@ function renderApp(state) {
1484
1591
  }
1485
1592
  }
1486
1593
 
1594
+ if (!mainPatched && canPatchLogsMain) {
1595
+ mainPatched = patchLogsTableUi(state);
1596
+ }
1597
+
1487
1598
  if (mainChanged) {
1488
1599
  if (!mainPatched) {
1489
1600
  teardownStructureGraph();
@@ -2428,6 +2539,15 @@ async function handleAction(actionNode) {
2428
2539
  case 'refresh-view':
2429
2540
  await refreshCurrentRoute();
2430
2541
  return;
2542
+ case 'refresh-logs':
2543
+ await refreshLogs();
2544
+ return;
2545
+ case 'set-log-filter':
2546
+ await setLogFilter(actionNode.dataset.field, actionNode.dataset.value);
2547
+ return;
2548
+ case 'load-more-logs':
2549
+ await loadMoreLogs();
2550
+ return;
2431
2551
  case 'set-settings-section':
2432
2552
  setSettingsSection(actionNode.dataset.section);
2433
2553
  return;
@@ -2878,6 +2998,12 @@ async function handleAction(actionNode) {
2878
2998
  case 'open-data-export-modal':
2879
2999
  openDataExportModal();
2880
3000
  return;
3001
+ case 'open-generate-data-modal':
3002
+ openGenerateDataModal();
3003
+ return;
3004
+ case 'preview-generate-data':
3005
+ await previewGenerateDataRows();
3006
+ return;
2881
3007
  case 'export-data-format': {
2882
3008
  const format = actionNode.dataset.exportFormat;
2883
3009
  const filename = getExportFilenameFromAction(actionNode);
@@ -2948,6 +3074,30 @@ async function handleAction(actionNode) {
2948
3074
  }
2949
3075
  return;
2950
3076
  }
3077
+ case 'copy-table-advisor-sql': {
3078
+ const issueId = actionNode.dataset.issueId;
3079
+ const advisor = getState().tableAdvisor;
3080
+ const issue = (advisor.result?.issues ?? []).find(item => item.id === issueId);
3081
+ const sql = String(issue?.sql ?? '');
3082
+
3083
+ if (!sql.trim()) {
3084
+ showToast('No advisor SQL to copy.', 'alert');
3085
+ return;
3086
+ }
3087
+
3088
+ if (!navigator.clipboard?.writeText) {
3089
+ showToast('Clipboard API is not available.', 'alert');
3090
+ return;
3091
+ }
3092
+
3093
+ try {
3094
+ await navigator.clipboard.writeText(sql);
3095
+ showToast('Advisor SQL copied.', 'success');
3096
+ } catch (error) {
3097
+ showToast('Advisor SQL could not be copied.', 'alert');
3098
+ }
3099
+ return;
3100
+ }
2951
3101
  case 'toggle-table-designer-sql-preview':
2952
3102
  setTableDesignerSqlPreviewVisibility(
2953
3103
  actionNode.dataset.nextValue ? actionNode.dataset.nextValue === 'true' : undefined,
@@ -3236,6 +3386,11 @@ document.addEventListener('keydown', event => {
3236
3386
  return;
3237
3387
  }
3238
3388
 
3389
+ if (target instanceof HTMLInputElement && clearInputForEscape(target)) {
3390
+ event.preventDefault();
3391
+ return;
3392
+ }
3393
+
3239
3394
  if (state.modal) {
3240
3395
  event.preventDefault();
3241
3396
  closeModal();
@@ -3327,6 +3482,20 @@ document.addEventListener('input', event => {
3327
3482
  return;
3328
3483
  }
3329
3484
 
3485
+ if (bindNode.dataset.bind === 'generate-data-field') {
3486
+ updateGenerateDataModal(bindNode.dataset.field, bindNode.value, { notify: false });
3487
+ return;
3488
+ }
3489
+
3490
+ if (bindNode.dataset.bind === 'generate-data-mapping') {
3491
+ if (bindNode instanceof HTMLSelectElement) {
3492
+ return;
3493
+ }
3494
+
3495
+ updateGenerateDataMapping(bindNode.dataset.columnName, bindNode.dataset.field, bindNode.value, { notify: false });
3496
+ return;
3497
+ }
3498
+
3330
3499
  if (bindNode.dataset.bind === 'copy-column-format-field') {
3331
3500
  updateCopyColumnModalFormatField(
3332
3501
  bindNode.dataset.field,
@@ -3354,6 +3523,11 @@ document.addEventListener('input', event => {
3354
3523
  return;
3355
3524
  }
3356
3525
 
3526
+ if (bindNode.dataset.bind === 'logs-search') {
3527
+ setLogSearchInput(bindNode.value);
3528
+ return;
3529
+ }
3530
+
3357
3531
  if (bindNode.dataset.bind === 'data-search-query') {
3358
3532
  void setDataSearchQuery(bindNode.value);
3359
3533
  return;
@@ -3505,6 +3679,16 @@ document.addEventListener('change', event => {
3505
3679
  return;
3506
3680
  }
3507
3681
 
3682
+ if (bindNode.dataset.bind === 'generate-data-field') {
3683
+ updateGenerateDataModal(bindNode.dataset.field, bindNode.value);
3684
+ return;
3685
+ }
3686
+
3687
+ if (bindNode.dataset.bind === 'generate-data-mapping') {
3688
+ updateGenerateDataMapping(bindNode.dataset.columnName, bindNode.dataset.field, bindNode.value);
3689
+ return;
3690
+ }
3691
+
3508
3692
  if (bindNode.dataset.bind === 'data-search-column') {
3509
3693
  void setDataSearchColumn(bindNode.value);
3510
3694
  return;
@@ -3649,6 +3833,9 @@ document.addEventListener('submit', async event => {
3649
3833
  const formData = submitter ? new FormData(form, submitter) : new FormData(form);
3650
3834
 
3651
3835
  switch (form.dataset.form) {
3836
+ case 'logs-search':
3837
+ await applyLogSearch(String(formData.get('search') ?? ''));
3838
+ return;
3652
3839
  case 'create-api-token':
3653
3840
  await createSettingsApiToken(String(formData.get('name') ?? ''));
3654
3841
  return;
@@ -3780,6 +3967,9 @@ document.addEventListener('submit', async event => {
3780
3967
  case 'apply-row-update-preview':
3781
3968
  await submitRowUpdatePreviewConfirmation();
3782
3969
  return;
3970
+ case 'generate-data':
3971
+ await submitGenerateDataRows();
3972
+ return;
3783
3973
  case 'create-media-tagging-tag-table':
3784
3974
  await submitCreateMediaTaggingTagTable();
3785
3975
  return;
@@ -56,7 +56,9 @@ export function renderConnectionCard(connection, activeConnectionId) {
56
56
  '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Opened</div><div class="text-xs font-bold text-on-surface">',
57
57
  escapeHtml(formatDateTime(connection.lastOpenedAt)),
58
58
  "</div></div>",
59
- '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div><div class="text-xs font-bold text-on-surface">',
59
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div><div class="text-xs font-bold ',
60
+ connection.readOnly ? "text-primary-container" : "text-on-surface",
61
+ '">',
60
62
  connection.readOnly ? "Read only" : "Read / Write",
61
63
  "</div></div></div></div>",
62
64
  '<div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">',