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
package/bin/sqlite-hub.js CHANGED
@@ -3,9 +3,11 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { spawn } = require('node:child_process');
6
+ const { DatabaseCommandService, getQueryTitle } = require('../server/services/databaseCommandService');
7
+ const { buildAppInfo } = require('../server/services/appInfoService');
6
8
 
7
9
  const DEFAULT_PORT = 4173;
8
- const EXPORT_FORMATS = new Set(['csv', 'tsv', 'md']);
10
+ const EXPORT_FORMATS = new Set(['csv', 'tsv', 'md', 'json']);
9
11
 
10
12
  function printHelp() {
11
13
  console.log(`SQLite Hub CLI
@@ -14,8 +16,10 @@ Usage:
14
16
  sqlite-hub [--port:4173]
15
17
  sqlite-hub --database
16
18
  sqlite-hub --database:"name" --tables
19
+ sqlite-hub --database:"name" --query:"SELECT * FROM table_name"
20
+ sqlite-hub --database:"name" --query:"SELECT * FROM table_name" --store:"Query Name"
17
21
  sqlite-hub --database:"name" --execute:"Saved Query"
18
- sqlite-hub --database:"name" --query:"Saved Query"
22
+ sqlite-hub --database:"name" --saved-query:"Saved Query"
19
23
  sqlite-hub --database:"name" --notes:"Saved Query"
20
24
  sqlite-hub --database:"name" --export:"Saved Query" --format:csv
21
25
  sqlite-hub --database:"name" --documents
@@ -27,7 +31,7 @@ Usage:
27
31
  Options:
28
32
  --help, -h Show this help text.
29
33
  --version, -v Show the version number.
30
- --config Show CLI port, URL, app version, and SQLite version.
34
+ --info Show port, URL, app version, SQLite version, and update status.
31
35
  --open Start/open SQLite Hub in the browser.
32
36
  --port:PORT Start the server on a custom port.
33
37
  --database, -d List all imported databases.
@@ -37,20 +41,18 @@ Options:
37
41
  --lastopened Print the selected database last-opened timestamp.
38
42
  --tables List tables in the selected database.
39
43
  --queries List saved SQL Editor queries for the selected database.
44
+ --query:"sql" Execute raw SQL and record it in query history.
45
+ --store:"name" Save a raw --query history item with this name.
40
46
  --execute:"query" Execute a saved SQL Editor query by name.
41
- --query:"query" Print a saved SQL Editor query by name.
47
+ --saved-query:"query" Print a saved SQL Editor query by name.
42
48
  --notes:"query" Print notes for a saved SQL Editor query by name.
43
49
  --export:"query" Export a saved query when --table is not set.
44
- --format:csv|tsv|md Export format for query exports. Defaults to csv.
50
+ --format:csv|tsv|md|json Export format for query exports. Defaults to csv.
45
51
  --documents List Markdown documents for the selected database.
46
52
  --documents:"name" Print a document's Markdown content.
47
53
  --documents:"name" --export Export a document as a Markdown file.
48
54
  --table:"table" Print table metadata.
49
55
  --table:"table" --export:"pk" Export one row as JSON by primary key or rowid.
50
-
51
- Legacy aliases still work:
52
- --database-path:name, --database-size:name, --database-lastopened:name
53
- --database-tables:name, --database:name --sqleditor, --database:name --sqleditor:"query"
54
56
  `);
55
57
  }
56
58
 
@@ -142,7 +144,7 @@ function normalizeExportFormat(format) {
142
144
  const normalized = String(format ?? 'csv').toLowerCase();
143
145
 
144
146
  if (!EXPORT_FORMATS.has(normalized)) {
145
- throw new Error(`Unsupported export format: ${format}. Use csv, tsv, or md.`);
147
+ throw new Error(`Unsupported export format: ${format}. Use csv, tsv, md, or json.`);
146
148
  }
147
149
 
148
150
  return normalized;
@@ -152,7 +154,7 @@ function parseCliArguments(argv) {
152
154
  const options = {
153
155
  help: false,
154
156
  version: false,
155
- config: false,
157
+ info: false,
156
158
  open: false,
157
159
  port: undefined,
158
160
  databaseList: false,
@@ -163,6 +165,8 @@ function parseCliArguments(argv) {
163
165
  tables: false,
164
166
  queries: false,
165
167
  executeQuery: null,
168
+ rawQuery: null,
169
+ storeName: null,
166
170
  showQuery: null,
167
171
  showNotes: null,
168
172
  exportTarget: null,
@@ -187,8 +191,8 @@ function parseCliArguments(argv) {
187
191
  continue;
188
192
  }
189
193
 
190
- if (flag === '--config') {
191
- options.config = true;
194
+ if (flag === '--info' || flag === '--config') {
195
+ options.info = true;
192
196
  continue;
193
197
  }
194
198
 
@@ -295,6 +299,20 @@ function parseCliArguments(argv) {
295
299
  }
296
300
 
297
301
  if (flag === '--query') {
302
+ const parsed = takeFlagValue(flag, value, argv, index);
303
+ options.rawQuery = parsed.value;
304
+ index = parsed.nextIndex;
305
+ continue;
306
+ }
307
+
308
+ if (flag === '--store') {
309
+ const parsed = takeFlagValue(flag, value, argv, index);
310
+ options.storeName = parsed.value;
311
+ index = parsed.nextIndex;
312
+ continue;
313
+ }
314
+
315
+ if (flag === '--saved-query') {
298
316
  const parsed = takeFlagValue(flag, value, argv, index);
299
317
  options.showQuery = parsed.value;
300
318
  index = parsed.nextIndex;
@@ -375,18 +393,20 @@ function parseCliArguments(argv) {
375
393
  function hasDatabaseOperation(options) {
376
394
  return Boolean(
377
395
  options.pathInfo ||
378
- options.sizeInfo ||
379
- options.lastOpenedInfo ||
380
- options.tables ||
381
- options.queries ||
382
- options.executeQuery ||
383
- options.showQuery ||
384
- options.showNotes ||
385
- options.documents ||
386
- options.documentName ||
387
- options.documentExport ||
388
- options.exportTarget ||
389
- options.tableName,
396
+ options.sizeInfo ||
397
+ options.lastOpenedInfo ||
398
+ options.tables ||
399
+ options.queries ||
400
+ options.executeQuery ||
401
+ options.rawQuery ||
402
+ options.storeName ||
403
+ options.showQuery ||
404
+ options.showNotes ||
405
+ options.documents ||
406
+ options.documentName ||
407
+ options.documentExport ||
408
+ options.exportTarget ||
409
+ options.tableName,
390
410
  );
391
411
  }
392
412
 
@@ -421,14 +441,6 @@ function openInDefaultBrowser(url) {
421
441
  child.unref();
422
442
  }
423
443
 
424
- function findDatabaseByName(connections, name) {
425
- const normalizedName = String(name ?? '').toLowerCase();
426
-
427
- return connections.find(
428
- conn => conn.label.toLowerCase() === normalizedName || conn.id.toLowerCase() === normalizedName,
429
- );
430
- }
431
-
432
444
  function formatSize(bytes) {
433
445
  if (!bytes) return 'N/A';
434
446
  if (bytes < 1024) return `${bytes} B`;
@@ -436,20 +448,6 @@ function formatSize(bytes) {
436
448
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
437
449
  }
438
450
 
439
- function sanitizeFilenameBase(value, fallback = 'export') {
440
- const sanitized = String(value ?? '')
441
- .replace(/[<>:"/\\|?*\u0000-\u001f]/g, ' ')
442
- .replace(/\s+/g, ' ')
443
- .trim()
444
- .replace(/[. ]+$/g, '');
445
-
446
- if (!sanitized) {
447
- return fallback;
448
- }
449
-
450
- return sanitized.slice(0, 120);
451
- }
452
-
453
451
  function createAppStateStore() {
454
452
  const { resolveAppStatePaths } = require('../server/utils/appPaths');
455
453
  const { AppStateStore } = require('../server/services/storage/appStateStore');
@@ -459,39 +457,6 @@ function createAppStateStore() {
459
457
  return new AppStateStore(appStateDbPath);
460
458
  }
461
459
 
462
- function createReadOnlyRuntime(conn, appStateStore) {
463
- const { ConnectionManager } = require('../server/services/sqlite/connectionManager');
464
- const { DataBrowserService } = require('../server/services/sqlite/dataBrowserService');
465
- const { ExportService } = require('../server/services/sqlite/exportService');
466
- const { SqlExecutor } = require('../server/services/sqlite/sqlExecutor');
467
-
468
- const connectionManager = new ConnectionManager({ appStateStore });
469
-
470
- connectionManager.openConnection({
471
- filePath: conn.path,
472
- label: conn.label,
473
- id: conn.id,
474
- logoPath: conn.logoPath ?? null,
475
- makeActive: false,
476
- readOnly: true,
477
- });
478
-
479
- const sqlExecutor = new SqlExecutor({ connectionManager, appStateStore });
480
- const exportService = new ExportService({ appStateStore, connectionManager, sqlExecutor });
481
- const dataBrowserService = new DataBrowserService({ connectionManager });
482
-
483
- return {
484
- connectionManager,
485
- db: connectionManager.getActiveDatabase(),
486
- dataBrowserService,
487
- exportService,
488
- sqlExecutor,
489
- close() {
490
- connectionManager.closeCurrent();
491
- },
492
- };
493
- }
494
-
495
460
  function printDatabaseList(connections) {
496
461
  if (connections.length === 0) {
497
462
  console.log('No databases imported yet.');
@@ -538,72 +503,8 @@ function printTables(conn, tables) {
538
503
  console.log('');
539
504
  }
540
505
 
541
- function getQueryTitle(item) {
542
- return item?.title || item?.displayTitle || item?.previewSql || item?.rawSql || '(untitled query)';
543
- }
544
-
545
- function findQueryByName(appStateStore, conn, queryName) {
546
- const normalizedQueryName = String(queryName ?? '').trim().toLowerCase();
547
-
548
- if (!normalizedQueryName) {
549
- throw new Error('Query name is required.');
550
- }
551
-
552
- const collection = appStateStore.buildQueryHistoryCollection({
553
- databaseKey: conn.id,
554
- search: queryName,
555
- onlySaved: false,
556
- limit: 100,
557
- });
558
-
559
- return (
560
- collection.items.find(item => {
561
- const candidates = [item.id, item.title, item.displayTitle].filter(Boolean);
562
-
563
- return candidates.some(candidate => String(candidate).toLowerCase() === normalizedQueryName);
564
- }) ||
565
- collection.items.find(item => String(item.rawSql ?? '').toLowerCase().includes(normalizedQueryName)) ||
566
- null
567
- );
568
- }
569
-
570
- function getSavedQueries(appStateStore, conn, limit = 100) {
571
- return appStateStore.buildQueryHistoryCollection({
572
- databaseKey: conn.id,
573
- onlySaved: true,
574
- limit,
575
- });
576
- }
577
-
578
- function printAvailableQueries(appStateStore, conn) {
579
- const allQueries = getSavedQueries(appStateStore, conn);
580
-
581
- console.error('\nAvailable saved queries:');
582
-
583
- if (allQueries.items.length > 0) {
584
- allQueries.items.forEach(query => {
585
- console.error(` - ${getQueryTitle(query)}`);
586
- });
587
- return;
588
- }
589
-
590
- console.error(' (none)');
591
- }
592
-
593
- function requireMatchingQuery(appStateStore, conn, queryName) {
594
- const matchingQuery = findQueryByName(appStateStore, conn, queryName);
595
-
596
- if (!matchingQuery) {
597
- console.error(`Saved query not found: ${queryName}`);
598
- printAvailableQueries(appStateStore, conn);
599
- process.exit(1);
600
- }
601
-
602
- return matchingQuery;
603
- }
604
-
605
- function listSavedQueries(appStateStore, conn) {
606
- const savedQueries = getSavedQueries(appStateStore, conn);
506
+ function listSavedQueries(databaseService, conn) {
507
+ const savedQueries = databaseService.listSavedQueries(conn.id);
607
508
 
608
509
  if (savedQueries.items.length === 0) {
609
510
  console.log(`No saved queries found for ${conn.label}.`);
@@ -664,28 +565,40 @@ function printExecutionResult(result) {
664
565
  });
665
566
  }
666
567
 
667
- function executeSavedQuery({ appStateStore, conn, sqlExecutor, queryName }) {
668
- const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
568
+ function executeSavedQuery({ databaseService, conn, queryName }) {
569
+ const { query: matchingQuery, result } = databaseService.executeSavedQuery(conn.id, queryName);
669
570
 
670
571
  console.log(`\nExecuting: ${getQueryTitle(matchingQuery)}`);
671
572
  console.log(`SQL: ${matchingQuery.previewSql || matchingQuery.rawSql}`);
672
573
  console.log('─'.repeat(60));
673
574
 
674
- const result = sqlExecutor.execute(matchingQuery.rawSql, {
675
- persistHistory: false,
676
- });
575
+ printExecutionResult(result);
576
+ }
577
+
578
+ function executeRawQuery({ databaseService, conn, sql, storeName = null }) {
579
+ const { result, storedQuery } = databaseService.executeRawQuery(conn.id, sql, { storeName });
677
580
 
581
+ console.log(`\nExecuting raw SQL against: ${conn.label}`);
582
+ console.log('─'.repeat(60));
678
583
  printExecutionResult(result);
584
+
585
+ if (result.historyId) {
586
+ console.log(`\nHistory ID: ${result.historyId}`);
587
+ }
588
+
589
+ if (storedQuery) {
590
+ console.log(`Stored query: ${getQueryTitle(storedQuery)}`);
591
+ }
679
592
  }
680
593
 
681
- function showSavedQuery({ appStateStore, conn, queryName }) {
682
- const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
594
+ function showSavedQuery({ databaseService, conn, queryName }) {
595
+ const matchingQuery = databaseService.getSavedQuery(conn.id, queryName);
683
596
 
684
597
  console.log(matchingQuery.rawSql);
685
598
  }
686
599
 
687
- function showSavedQueryNotes({ appStateStore, conn, queryName }) {
688
- const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
600
+ function showSavedQueryNotes({ databaseService, conn, queryName }) {
601
+ const matchingQuery = databaseService.getSavedQuery(conn.id, queryName);
689
602
  const notes = String(matchingQuery.notes ?? '').trim();
690
603
 
691
604
  if (notes) {
@@ -696,9 +609,8 @@ function showSavedQueryNotes({ appStateStore, conn, queryName }) {
696
609
  console.log(`No notes saved for: ${getQueryTitle(matchingQuery)}`);
697
610
  }
698
611
 
699
- function exportSavedQuery({ appStateStore, conn, exportService, queryName, format }) {
700
- const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
701
- const result = exportService.exportQuery(matchingQuery.rawSql, { format });
612
+ function exportSavedQuery({ databaseService, conn, queryName, format }) {
613
+ const { query: matchingQuery, result } = databaseService.exportSavedQuery(conn.id, queryName, format);
702
614
  const outputPath = path.resolve(process.cwd(), result.filename);
703
615
 
704
616
  fs.writeFileSync(outputPath, result.content, 'utf8');
@@ -709,66 +621,8 @@ function exportSavedQuery({ appStateStore, conn, exportService, queryName, forma
709
621
  console.log(`File: ${outputPath}`);
710
622
  }
711
623
 
712
- function getDocumentTitle(document) {
713
- return document?.filename || document?.title || document?.id || '(untitled document)';
714
- }
715
-
716
- function printAvailableDocuments(appStateStore, conn) {
717
- const documents = appStateStore.listDatabaseDocuments(conn.id);
718
-
719
- console.error('\nAvailable documents:');
720
-
721
- if (documents.length > 0) {
722
- documents.forEach(document => {
723
- console.error(` - ${getDocumentTitle(document)}`);
724
- });
725
- return;
726
- }
727
-
728
- console.error(' (none)');
729
- }
730
-
731
- function findDocumentByName(appStateStore, conn, documentName) {
732
- const normalizedDocumentName = String(documentName ?? '').trim().toLowerCase();
733
-
734
- if (!normalizedDocumentName) {
735
- throw new Error('Document name is required.');
736
- }
737
-
738
- const documents = appStateStore.listDatabaseDocuments(conn.id);
739
- const exactMatch = documents.find(document => {
740
- const candidates = [document.id, document.filename, document.title].filter(Boolean);
741
-
742
- return candidates.some(candidate => String(candidate).toLowerCase() === normalizedDocumentName);
743
- });
744
-
745
- if (exactMatch) {
746
- return appStateStore.getDatabaseDocument(conn.id, exactMatch.id);
747
- }
748
-
749
- const partialMatch = documents.find(document => {
750
- const candidates = [document.filename, document.title].filter(Boolean);
751
-
752
- return candidates.some(candidate => String(candidate).toLowerCase().includes(normalizedDocumentName));
753
- });
754
-
755
- return partialMatch ? appStateStore.getDatabaseDocument(conn.id, partialMatch.id) : null;
756
- }
757
-
758
- function requireMatchingDocument(appStateStore, conn, documentName) {
759
- const matchingDocument = findDocumentByName(appStateStore, conn, documentName);
760
-
761
- if (!matchingDocument) {
762
- console.error(`Document not found: ${documentName}`);
763
- printAvailableDocuments(appStateStore, conn);
764
- process.exit(1);
765
- }
766
-
767
- return matchingDocument;
768
- }
769
-
770
- function listDocuments(appStateStore, conn) {
771
- const documents = appStateStore.listDatabaseDocuments(conn.id);
624
+ function listDocuments(databaseService, conn) {
625
+ const documents = databaseService.listDocuments(conn.id);
772
626
 
773
627
  if (documents.length === 0) {
774
628
  console.log(`No documents found for ${conn.label}.`);
@@ -787,156 +641,27 @@ function listDocuments(appStateStore, conn) {
787
641
  console.log('');
788
642
  }
789
643
 
790
- function showDocumentMarkdown({ appStateStore, conn, documentName }) {
791
- const matchingDocument = requireMatchingDocument(appStateStore, conn, documentName);
644
+ function showDocumentMarkdown({ databaseService, conn, documentName }) {
645
+ const matchingDocument = databaseService.getDocument(conn.id, documentName);
792
646
 
793
647
  console.log(matchingDocument.content ?? '');
794
648
  }
795
649
 
796
- function normalizeMarkdownExportFilename(filename, fallback = 'document.md') {
797
- let normalizedFilename = String(filename ?? '')
798
- .replace(/[\u0000-\u001f\u007f]/g, ' ')
799
- .replace(/[<>:"/\\|?*]+/g, ' ')
800
- .replace(/\s+/g, ' ')
801
- .trim()
802
- .replace(/^\.+/, '')
803
- .replace(/[. ]+$/g, '');
804
-
805
- if (!normalizedFilename) {
806
- normalizedFilename = fallback;
807
- }
808
-
809
- if (!/\.md$/i.test(normalizedFilename)) {
810
- normalizedFilename = `${normalizedFilename}.md`;
811
- }
812
-
813
- if (normalizedFilename.length > 160) {
814
- normalizedFilename = `${normalizedFilename.slice(0, 157)}.md`;
815
- }
816
-
817
- return normalizedFilename;
818
- }
819
-
820
- function exportDocumentMarkdown({ appStateStore, conn, documentName }) {
821
- const matchingDocument = requireMatchingDocument(appStateStore, conn, documentName);
822
- const filename = normalizeMarkdownExportFilename(matchingDocument.filename, `${matchingDocument.title || 'document'}.md`);
823
- const outputPath = path.resolve(process.cwd(), filename);
650
+ function exportDocumentMarkdown({ databaseService, conn, documentName }) {
651
+ const result = databaseService.exportDocument(conn.id, documentName);
652
+ const outputPath = path.resolve(process.cwd(), result.filename);
824
653
 
825
- fs.writeFileSync(outputPath, matchingDocument.content ?? '', 'utf8');
654
+ fs.writeFileSync(outputPath, result.content, 'utf8');
826
655
 
827
- console.log(`Exported document: ${matchingDocument.filename}`);
828
- console.log(`Characters: ${matchingDocument.contentLength}`);
656
+ console.log(`Exported document: ${result.document.filename}`);
657
+ console.log(`Characters: ${result.document.contentLength}`);
829
658
  console.log(`File: ${outputPath}`);
830
659
  }
660
+ function exportTableRowAsJson({ databaseService, conn, tableName, exportTarget }) {
661
+ const result = databaseService.getTableRow(conn.id, tableName, exportTarget);
662
+ const outputPath = path.resolve(process.cwd(), result.filename);
831
663
 
832
- function coerceIdentityValue(column, value) {
833
- const text = String(value ?? '');
834
- const affinity = String(column?.affinity ?? '').toUpperCase();
835
-
836
- if ((affinity === 'INTEGER' || affinity === 'REAL' || affinity === 'NUMERIC') && text.trim() !== '') {
837
- const numberValue = Number(text);
838
-
839
- if (Number.isFinite(numberValue)) {
840
- return numberValue;
841
- }
842
- }
843
-
844
- return value;
845
- }
846
-
847
- function parseCompositePrimaryKeyValue(rawValue) {
848
- try {
849
- const parsed = JSON.parse(rawValue);
850
-
851
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
852
- return parsed;
853
- }
854
- } catch (error) {
855
- // Fall through to the clearer error below.
856
- }
857
-
858
- throw new Error('Composite primary key export requires a JSON object, for example --export:\'{"id":1,"locale":"en"}\'.');
859
- }
860
-
861
- function buildIdentityFromExportTarget(tableDetail, exportTarget) {
862
- if (tableDetail.identityStrategy?.type === 'rowid') {
863
- const numberValue = Number(exportTarget);
864
- const rowid = Number.isInteger(numberValue) ? numberValue : exportTarget;
865
-
866
- return {
867
- kind: 'rowid',
868
- values: {
869
- rowid,
870
- },
871
- };
872
- }
873
-
874
- if (tableDetail.identityStrategy?.type === 'primaryKey') {
875
- const columns = tableDetail.identityStrategy.columns ?? [];
876
-
877
- if (columns.length === 1) {
878
- const columnName = columns[0];
879
- const column = tableDetail.columns.find(candidate => candidate.name === columnName);
880
-
881
- return {
882
- kind: 'primaryKey',
883
- columns,
884
- values: {
885
- [columnName]: coerceIdentityValue(column, exportTarget),
886
- },
887
- };
888
- }
889
-
890
- const parsed = parseCompositePrimaryKeyValue(exportTarget);
891
-
892
- return {
893
- kind: 'primaryKey',
894
- columns,
895
- values: Object.fromEntries(
896
- columns.map(columnName => {
897
- if (!Object.prototype.hasOwnProperty.call(parsed, columnName)) {
898
- throw new Error(`Missing primary key value for ${columnName}.`);
899
- }
900
-
901
- const column = tableDetail.columns.find(candidate => candidate.name === columnName);
902
-
903
- return [columnName, coerceIdentityValue(column, parsed[columnName])];
904
- }),
905
- ),
906
- };
907
- }
908
-
909
- throw new Error(`Table ${tableDetail.name} has no stable row identity.`);
910
- }
911
-
912
- function buildDataRowEditorJsonObject({ row, columns = [] } = {}) {
913
- const names = columns
914
- .map(column => String(typeof column === 'object' ? column?.name : column ?? '').trim())
915
- .filter(name => name && name !== '__identity');
916
- const sourceNames = names.length
917
- ? names
918
- : Object.keys(row ?? {}).filter(name => name !== '__identity');
919
-
920
- return Object.fromEntries(
921
- sourceNames
922
- .map(name => [name, Object.prototype.hasOwnProperty.call(row ?? {}, name) ? row[name] : undefined])
923
- .filter(([, value]) => value !== undefined),
924
- );
925
- }
926
-
927
- function exportTableRowAsJson({ dataBrowserService, db, tableName, exportTarget }) {
928
- const { getTableDetail } = require('../server/services/sqlite/introspection');
929
- const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
930
- const identity = buildIdentityFromExportTarget(tableDetail, exportTarget);
931
- const { row } = dataBrowserService.getTableRow(tableName, { identity });
932
- const rowObject = buildDataRowEditorJsonObject({
933
- row,
934
- columns: tableDetail.columns.filter(column => column.visible),
935
- });
936
- const filenameBase = sanitizeFilenameBase(`${tableName}-${exportTarget}`, `${tableName}-row`);
937
- const outputPath = path.resolve(process.cwd(), `${filenameBase}.json`);
938
-
939
- fs.writeFileSync(outputPath, `${JSON.stringify(rowObject, null, 2)}\n`, 'utf8');
664
+ fs.writeFileSync(outputPath, `${JSON.stringify(result.data, null, 2)}\n`, 'utf8');
940
665
 
941
666
  console.log(`Exported row: ${tableName}`);
942
667
  console.log(`Key: ${exportTarget}`);
@@ -997,28 +722,51 @@ function printTableInfo(tableDetail) {
997
722
  console.log(`Indexes: ${tableDetail.indexes.length}`);
998
723
  tableDetail.indexes.forEach(index => {
999
724
  const unique = index.unique ? ' UNIQUE' : '';
1000
- const columns = index.columns.map(column => column.name).filter(Boolean).join(', ') || 'expression';
725
+ const columns =
726
+ index.columns
727
+ .map(column => column.name)
728
+ .filter(Boolean)
729
+ .join(', ') || 'expression';
1001
730
  console.log(` - ${index.name}${unique}: ${columns}`);
1002
731
  });
1003
732
  }
1004
733
  }
1005
734
 
1006
- function printConfig(port) {
1007
- const Database = require('better-sqlite3');
1008
- const { version } = require('../package.json');
1009
- const db = new Database(':memory:');
735
+ function formatVersionStatus(versionCheck) {
736
+ if (!versionCheck || versionCheck.status === 'unknown') {
737
+ return `unknown${versionCheck?.error?.message ? ` (${versionCheck.error.message})` : ''}`;
738
+ }
739
+
740
+ if (versionCheck.updateAvailable) {
741
+ return `update available (${versionCheck.currentVersion} -> ${versionCheck.latestVersion})`;
742
+ }
1010
743
 
1011
- try {
1012
- const sqliteVersion = db.prepare('SELECT sqlite_version() AS version').get().version;
1013
- const url = `http://127.0.0.1:${port}`;
1014
-
1015
- console.log('SQLite Hub config');
1016
- console.log(`Port: ${port}`);
1017
- console.log(`URL: ${url}`);
1018
- console.log(`App version: ${version}`);
1019
- console.log(`SQLite version: ${sqliteVersion}`);
1020
- } finally {
1021
- db.close();
744
+ if (versionCheck.status === 'ahead') {
745
+ return `ahead of npm latest (${versionCheck.currentVersion} > ${versionCheck.latestVersion})`;
746
+ }
747
+
748
+ return `current (${versionCheck.currentVersion})`;
749
+ }
750
+
751
+ async function printInfo(port, options = {}) {
752
+ const infoService = options.appInfoService ?? buildAppInfo;
753
+ const url = `http://127.0.0.1:${port}`;
754
+ const info = await infoService({ port, url });
755
+
756
+ console.log('SQLite Hub info');
757
+ console.log(`Port: ${info.port}`);
758
+ console.log(`URL: ${info.url}`);
759
+ console.log(`Package: ${info.packageName}`);
760
+ console.log(`App version: ${info.appVersion}`);
761
+ console.log(`SQLite version: ${info.sqliteVersion}`);
762
+ console.log(`Version status: ${formatVersionStatus(info.versionCheck)}`);
763
+
764
+ if (info.versionCheck?.latestVersion) {
765
+ console.log(`Latest version: ${info.versionCheck.latestVersion}`);
766
+ }
767
+
768
+ if (info.versionCheck?.releaseUrl) {
769
+ console.log(`Release URL: ${info.versionCheck.releaseUrl}`);
1022
770
  }
1023
771
  }
1024
772
 
@@ -1049,8 +797,8 @@ function requireDatabaseName(options) {
1049
797
  return options.databaseName;
1050
798
  }
1051
799
 
1052
- async function main() {
1053
- const options = parseCliArguments(process.argv.slice(2));
800
+ async function main(argv = process.argv.slice(2), dependencies = {}) {
801
+ const options = parseCliArguments(argv);
1054
802
  const port = options.port ?? DEFAULT_PORT;
1055
803
 
1056
804
  if (options.help) {
@@ -1064,8 +812,8 @@ async function main() {
1064
812
  return;
1065
813
  }
1066
814
 
1067
- if (options.config) {
1068
- printConfig(port);
815
+ if (options.info) {
816
+ await printInfo(port, dependencies);
1069
817
  return;
1070
818
  }
1071
819
 
@@ -1074,8 +822,16 @@ async function main() {
1074
822
  return;
1075
823
  }
1076
824
 
1077
- const appStateStore = createAppStateStore();
1078
- const connections = appStateStore.getRecentConnections();
825
+ if (options.storeName && !options.rawQuery) {
826
+ throw new Error('--store requires --query:"sql".');
827
+ }
828
+
829
+ const databaseService =
830
+ dependencies.databaseService ??
831
+ new DatabaseCommandService({
832
+ appStateStore: dependencies.appStateStore ?? createAppStateStore(),
833
+ });
834
+ const connections = databaseService.listDatabases();
1079
835
 
1080
836
  if (options.databaseList && !options.databaseName && !hasDatabaseOperation(options)) {
1081
837
  printDatabaseList(connections);
@@ -1084,24 +840,19 @@ async function main() {
1084
840
 
1085
841
  if (options.databaseName || hasDatabaseOperation(options)) {
1086
842
  const dbName = requireDatabaseName(options);
1087
- const conn = findDatabaseByName(connections, dbName);
1088
-
1089
- if (!conn) {
1090
- console.error(`Database not found: ${dbName}`);
1091
- process.exit(1);
1092
- }
843
+ const conn = databaseService.getDatabase(dbName);
1093
844
 
1094
845
  if (options.documents) {
1095
846
  if (options.documentName) {
1096
847
  if (options.documentExport) {
1097
848
  exportDocumentMarkdown({
1098
- appStateStore,
849
+ databaseService,
1099
850
  conn,
1100
851
  documentName: options.documentName,
1101
852
  });
1102
853
  } else {
1103
854
  showDocumentMarkdown({
1104
- appStateStore,
855
+ databaseService,
1105
856
  conn,
1106
857
  documentName: options.documentName,
1107
858
  });
@@ -1109,73 +860,82 @@ async function main() {
1109
860
  return;
1110
861
  }
1111
862
 
1112
- listDocuments(appStateStore, conn);
863
+ listDocuments(databaseService, conn);
1113
864
  return;
1114
865
  }
1115
866
 
1116
- if (options.tableName || options.tables || options.queries || options.executeQuery || options.showQuery || options.showNotes || options.exportTarget) {
1117
- const runtime = createReadOnlyRuntime(conn, appStateStore);
1118
-
1119
- try {
1120
- if (options.tableName) {
1121
- const { getTableDetail } = require('../server/services/sqlite/introspection');
1122
-
1123
- if (options.exportTarget) {
1124
- exportTableRowAsJson({
1125
- dataBrowserService: runtime.dataBrowserService,
1126
- db: runtime.db,
1127
- tableName: options.tableName,
1128
- exportTarget: options.exportTarget,
1129
- });
1130
- } else {
1131
- printTableInfo(getTableDetail(runtime.db, options.tableName));
1132
- }
1133
-
1134
- return;
1135
- }
1136
-
867
+ if (
868
+ options.tableName ||
869
+ options.tables ||
870
+ options.queries ||
871
+ options.executeQuery ||
872
+ options.rawQuery ||
873
+ options.showQuery ||
874
+ options.showNotes ||
875
+ options.exportTarget
876
+ ) {
877
+ if (options.tableName) {
1137
878
  if (options.exportTarget) {
1138
- exportSavedQuery({
1139
- appStateStore,
879
+ exportTableRowAsJson({
880
+ databaseService,
1140
881
  conn,
1141
- exportService: runtime.exportService,
1142
- queryName: options.exportTarget,
1143
- format: options.exportFormat,
882
+ tableName: options.tableName,
883
+ exportTarget: options.exportTarget,
1144
884
  });
1145
- return;
885
+ } else {
886
+ printTableInfo(databaseService.getTable(conn.id, options.tableName));
1146
887
  }
1147
888
 
1148
- if (options.executeQuery) {
1149
- executeSavedQuery({
1150
- appStateStore,
1151
- conn,
1152
- sqlExecutor: runtime.sqlExecutor,
1153
- queryName: options.executeQuery,
1154
- });
1155
- return;
1156
- }
889
+ return;
890
+ }
1157
891
 
1158
- if (options.showQuery) {
1159
- showSavedQuery({ appStateStore, conn, queryName: options.showQuery });
1160
- return;
1161
- }
892
+ if (options.exportTarget) {
893
+ exportSavedQuery({
894
+ databaseService,
895
+ conn,
896
+ queryName: options.exportTarget,
897
+ format: options.exportFormat,
898
+ });
899
+ return;
900
+ }
1162
901
 
1163
- if (options.showNotes) {
1164
- showSavedQueryNotes({ appStateStore, conn, queryName: options.showNotes });
1165
- return;
1166
- }
902
+ if (options.executeQuery) {
903
+ executeSavedQuery({
904
+ databaseService,
905
+ conn,
906
+ queryName: options.executeQuery,
907
+ });
908
+ return;
909
+ }
1167
910
 
1168
- if (options.queries) {
1169
- listSavedQueries(appStateStore, conn);
1170
- return;
1171
- }
911
+ if (options.rawQuery) {
912
+ executeRawQuery({
913
+ databaseService,
914
+ conn,
915
+ sql: options.rawQuery,
916
+ storeName: options.storeName,
917
+ });
918
+ return;
919
+ }
1172
920
 
1173
- if (options.tables) {
1174
- printTables(conn, runtime.dataBrowserService.listTables());
1175
- return;
1176
- }
1177
- } finally {
1178
- runtime.close();
921
+ if (options.showQuery) {
922
+ showSavedQuery({ databaseService, conn, queryName: options.showQuery });
923
+ return;
924
+ }
925
+
926
+ if (options.showNotes) {
927
+ showSavedQueryNotes({ databaseService, conn, queryName: options.showNotes });
928
+ return;
929
+ }
930
+
931
+ if (options.queries) {
932
+ listSavedQueries(databaseService, conn);
933
+ return;
934
+ }
935
+
936
+ if (options.tables) {
937
+ printTables(conn, databaseService.listTables(conn.id));
938
+ return;
1179
939
  }
1180
940
  }
1181
941
 
@@ -1215,7 +975,9 @@ if (require.main === module) {
1215
975
 
1216
976
  module.exports = {
1217
977
  main,
978
+ formatVersionStatus,
1218
979
  normalizeExportFormat,
1219
980
  openInDefaultBrowser,
1220
981
  parseCliArguments,
982
+ printInfo,
1221
983
  };