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
@@ -3,6 +3,8 @@ const test = require("node:test");
3
3
  const {
4
4
  NativeFileDialogService,
5
5
  buildDialogAttempts,
6
+ buildOpenDialogAttempts,
7
+ normalizeOpenedDatabasePath,
6
8
  normalizeSelectedDatabasePath,
7
9
  } = require("../server/services/nativeFileDialogService");
8
10
 
@@ -24,6 +26,21 @@ test("native database dialog builds platform-specific save commands", () => {
24
26
  assert.deepEqual(linuxAttempts.map((attempt) => attempt.command), ["zenity", "kdialog"]);
25
27
  });
26
28
 
29
+ test("native database dialog builds platform-specific open commands", () => {
30
+ const macAttempt = buildOpenDialogAttempts({ platform: "darwin", homeDirectory: "/Users/test" })[0];
31
+ const windowsAttempt = buildOpenDialogAttempts({ platform: "win32", homeDirectory: "C:\\Users\\test" })[0];
32
+ const linuxAttempts = buildOpenDialogAttempts({ platform: "linux", homeDirectory: "/home/test" });
33
+
34
+ assert.match(macAttempt.args.join(" "), /choose file with prompt/);
35
+ assert.match(windowsAttempt.args.at(-1), /OpenFileDialog/);
36
+ assert.deepEqual(linuxAttempts.map((attempt) => attempt.command), ["zenity", "kdialog"]);
37
+ });
38
+
39
+ test("open database dialog preserves the selected filename", () => {
40
+ assert.equal(normalizeOpenedDatabasePath("/tmp/catalog\n"), "/tmp/catalog");
41
+ assert.equal(normalizeOpenedDatabasePath(""), null);
42
+ });
43
+
27
44
  test("native database dialog returns null when the user cancels", async () => {
28
45
  const service = new NativeFileDialogService({
29
46
  platform: "darwin",
@@ -76,3 +93,13 @@ test("native database dialog falls back from zenity to kdialog", async () => {
76
93
  assert.equal(await service.chooseCreateDatabasePath(), "/home/test/catalog.sqlite3");
77
94
  assert.deepEqual(commands, ["zenity", "kdialog"]);
78
95
  });
96
+
97
+ test("native open database dialog returns the selected existing path", async () => {
98
+ const service = new NativeFileDialogService({
99
+ platform: "linux",
100
+ homeDirectory: "/home/test",
101
+ executeFile: async () => ({ stdout: "/home/test/catalog.db\n" }),
102
+ });
103
+
104
+ assert.equal(await service.chooseOpenDatabasePath(), "/home/test/catalog.db");
105
+ });
@@ -0,0 +1,97 @@
1
+ const assert = require("node:assert/strict");
2
+ const express = require("express");
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const test = require("node:test");
7
+
8
+ const { createSettingsRouter } = require("../server/routes/settings");
9
+ const { ApiTokenService } = require("../server/services/apiTokenService");
10
+ const { AppStateStore } = require("../server/services/storage/appStateStore");
11
+ const { errorMiddleware } = require("../server/utils/errors");
12
+
13
+ test("settings routes create and delete tokens for the active database", async (t) => {
14
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-settings-token-"));
15
+ const store = new AppStateStore(path.join(directory, "state.db"));
16
+ const connection = {
17
+ id: "db-active",
18
+ label: "Active Database",
19
+ path: path.join(directory, "active.db"),
20
+ lastOpenedAt: new Date().toISOString(),
21
+ lastModifiedAt: new Date().toISOString(),
22
+ sizeBytes: 0,
23
+ readOnly: false,
24
+ logoPath: null,
25
+ };
26
+
27
+ store.upsertRecentConnection(connection);
28
+
29
+ const tokenService = new ApiTokenService({ appStateStore: store });
30
+ const connectionManager = {
31
+ getActiveConnection: () => connection,
32
+ };
33
+ const app = express();
34
+
35
+ app.use(express.json());
36
+ app.use(
37
+ "/api/settings",
38
+ createSettingsRouter({
39
+ appStateStore: store,
40
+ connectionManager,
41
+ tokenService,
42
+ versionCheckService: async () => ({
43
+ packageName: "sqlite-hub",
44
+ currentVersion: "1.0.1",
45
+ latestVersion: "1.1.0",
46
+ updateAvailable: true,
47
+ checkedAt: "2026-06-19T20:00:00.000Z",
48
+ source: "npm",
49
+ releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.1.0",
50
+ }),
51
+ })
52
+ );
53
+ app.use(errorMiddleware);
54
+
55
+ const server = await new Promise((resolve) => {
56
+ const listener = app.listen(0, "127.0.0.1", () => resolve(listener));
57
+ });
58
+
59
+ t.after(async () => {
60
+ await new Promise((resolve, reject) => {
61
+ server.close((error) => (error ? reject(error) : resolve()));
62
+ });
63
+ store.db.close();
64
+ fs.rmSync(directory, { recursive: true, force: true });
65
+ });
66
+
67
+ const baseUrl = `http://127.0.0.1:${server.address().port}/api/settings`;
68
+ const createResponse = await fetch(`${baseUrl}/api-tokens`, {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify({ name: "Settings token" }),
72
+ });
73
+ const created = await createResponse.json();
74
+
75
+ assert.equal(createResponse.status, 201);
76
+ assert.equal(created.data.databaseKey, connection.id);
77
+ assert.match(created.data.token, /^shub_/);
78
+ assert.equal(created.metadata.apiTokens.length, 1);
79
+ assert.equal(created.metadata.apiTokens[0].token, undefined);
80
+
81
+ const deleteResponse = await fetch(`${baseUrl}/api-tokens/${created.data.id}`, {
82
+ method: "DELETE",
83
+ });
84
+ const deleted = await deleteResponse.json();
85
+
86
+ assert.equal(deleteResponse.status, 200);
87
+ assert.deepEqual(deleted.data, { id: created.data.id, deleted: true });
88
+ assert.equal(deleted.metadata.apiTokens.length, 0);
89
+
90
+ const versionResponse = await fetch(`${baseUrl}/version-check`);
91
+ const versionCheck = await versionResponse.json();
92
+
93
+ assert.equal(versionResponse.status, 200);
94
+ assert.equal(versionCheck.data.latestVersion, "1.1.0");
95
+ assert.equal(versionCheck.data.updateAvailable, true);
96
+ assert.equal(versionCheck.metadata.appVersion, require("../package.json").version);
97
+ });
@@ -2,7 +2,14 @@ const assert = require("node:assert/strict");
2
2
  const test = require("node:test");
3
3
 
4
4
  const { version } = require("../package.json");
5
- const { readSettingsMetadata, readSqliteVersion } = require("../server/routes/settings");
5
+ const {
6
+ checkLatestAppVersion,
7
+ compareSemver,
8
+ isNewerVersion,
9
+ readSettingsMetadata,
10
+ readSqliteVersion,
11
+ } = require("../server/routes/settings");
12
+ const { buildAppInfo } = require("../server/services/appInfoService");
6
13
 
7
14
  test("settings metadata exposes app and SQLite versions", () => {
8
15
  const metadata = readSettingsMetadata();
@@ -14,3 +21,94 @@ test("settings metadata exposes app and SQLite versions", () => {
14
21
  test("SQLite runtime version is readable", () => {
15
22
  assert.match(readSqliteVersion(), /^\d+\.\d+\.\d+$/);
16
23
  });
24
+
25
+ test("settings version comparison detects newer releases", () => {
26
+ assert.equal(compareSemver("1.2.0", "1.1.9"), 1);
27
+ assert.equal(compareSemver("1.0.1", "1.0.1"), 0);
28
+ assert.equal(compareSemver("1.0.0", "1.0.1"), -1);
29
+ assert.equal(isNewerVersion("1.0.2", "1.0.1"), true);
30
+ assert.equal(isNewerVersion("1.0.1", "1.0.1"), false);
31
+ });
32
+
33
+ test("settings version check reads the npm latest version", async () => {
34
+ const result = await checkLatestAppVersion({
35
+ currentVersion: "1.0.1",
36
+ packageName: "sqlite-hub",
37
+ fetchImpl: async (url, options) => {
38
+ assert.equal(url, "https://registry.npmjs.org/sqlite-hub/latest");
39
+ assert.equal(options.headers.Accept, "application/json");
40
+
41
+ return {
42
+ ok: true,
43
+ json: async () => ({ version: "1.1.0" }),
44
+ };
45
+ },
46
+ });
47
+
48
+ assert.equal(result.packageName, "sqlite-hub");
49
+ assert.equal(result.currentVersion, "1.0.1");
50
+ assert.equal(result.latestVersion, "1.1.0");
51
+ assert.equal(result.updateAvailable, true);
52
+ assert.equal(result.source, "npm");
53
+ assert.equal(result.releaseUrl, "https://www.npmjs.com/package/sqlite-hub/v/1.1.0");
54
+ assert.match(result.checkedAt, /^\d{4}-\d{2}-\d{2}T/);
55
+ });
56
+
57
+ test("app info includes update status for CLI and API consumers", async () => {
58
+ const info = await buildAppInfo({
59
+ port: 4173,
60
+ url: "http://127.0.0.1:4173",
61
+ currentVersion: "1.0.1",
62
+ sqliteVersion: "3.50.0",
63
+ versionCheckService: async () => ({
64
+ packageName: "sqlite-hub",
65
+ currentVersion: "1.0.1",
66
+ latestVersion: "1.1.0",
67
+ updateAvailable: true,
68
+ checkedAt: "2026-06-20T10:00:00.000Z",
69
+ source: "npm",
70
+ releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.1.0",
71
+ }),
72
+ });
73
+
74
+ assert.equal(info.packageName, "sqlite-hub");
75
+ assert.equal(info.appVersion, "1.0.1");
76
+ assert.equal(info.sqliteVersion, "3.50.0");
77
+ assert.equal(info.port, 4173);
78
+ assert.equal(info.url, "http://127.0.0.1:4173");
79
+ assert.equal(info.versionCheck.status, "update_available");
80
+ assert.equal(info.versionCheck.updateAvailable, true);
81
+ });
82
+
83
+ test("app info reports unknown version status when registry lookup fails", async () => {
84
+ const info = await buildAppInfo({
85
+ currentVersion: "1.0.1",
86
+ sqliteVersion: "3.50.0",
87
+ versionCheckService: async () => {
88
+ throw new Error("offline");
89
+ },
90
+ });
91
+
92
+ assert.equal(info.versionCheck.status, "unknown");
93
+ assert.equal(info.versionCheck.updateAvailable, null);
94
+ assert.equal(info.versionCheck.error.message, "offline");
95
+ });
96
+
97
+ test("app info marks local builds newer than npm latest as ahead", async () => {
98
+ const info = await buildAppInfo({
99
+ currentVersion: "1.1.0",
100
+ sqliteVersion: "3.50.0",
101
+ versionCheckService: async () => ({
102
+ packageName: "sqlite-hub",
103
+ currentVersion: "1.1.0",
104
+ latestVersion: "1.0.0",
105
+ updateAvailable: false,
106
+ checkedAt: "2026-06-20T10:00:00.000Z",
107
+ source: "npm",
108
+ releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.0.0",
109
+ }),
110
+ });
111
+
112
+ assert.equal(info.versionCheck.status, "ahead");
113
+ assert.equal(info.versionCheck.updateAvailable, false);
114
+ });
@@ -29,4 +29,79 @@ test("settings view shows SQLite runtime version and custom port command", async
29
29
  assert.match(rendered.main, /SQLite_Runtime/);
30
30
  assert.match(rendered.main, /3\.53\.1/);
31
31
  assert.match(rendered.main, /sqlite-hub --port:PORT/);
32
+ assert.match(rendered.main, /Version_Check/);
33
+ assert.match(rendered.main, /data-action="check-app-version"/);
34
+ assert.match(rendered.main, /Check Updates/);
35
+ assert.match(rendered.main, /No version check has been run yet\./);
36
+ assert.match(rendered.main, /data-section="information"/);
37
+ assert.match(rendered.main, /data-section="api-tokens"/);
38
+ assert.doesNotMatch(rendered.main, /data-form="create-api-token"/);
39
+ });
40
+
41
+ test("settings view shows available app updates", async () => {
42
+ const { renderSettingsView } = await loadSettingsViewModule();
43
+ const rendered = renderSettingsView({
44
+ settings: {
45
+ loading: false,
46
+ error: null,
47
+ appVersion: "1.0.1",
48
+ sqliteVersion: "3.53.1",
49
+ versionCheckLoading: false,
50
+ versionCheckError: null,
51
+ versionCheck: {
52
+ currentVersion: "1.0.1",
53
+ latestVersion: "1.1.0",
54
+ updateAvailable: true,
55
+ releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.1.0",
56
+ },
57
+ },
58
+ });
59
+
60
+ assert.match(rendered.main, /New Version Available/);
61
+ assert.match(rendered.main, /Current v1\.0\.1 · Latest v1\.1\.0/);
62
+ assert.match(rendered.main, /Open Release/);
63
+ });
64
+
65
+ test("settings view scopes API token controls to the active database", async () => {
66
+ const { renderSettingsView } = await loadSettingsViewModule();
67
+ const rendered = renderSettingsView({
68
+ settings: {
69
+ loading: false,
70
+ error: null,
71
+ appVersion: "1.2.3",
72
+ sqliteVersion: "3.53.1",
73
+ section: "api-tokens",
74
+ tokenDatabase: { id: "db-one", label: "Database One" },
75
+ apiTokens: [
76
+ {
77
+ id: "token-one",
78
+ name: "Automation",
79
+ tokenPrefix: "shub_example",
80
+ createdAt: "2026-06-15T10:00:00.000Z",
81
+ },
82
+ ],
83
+ createdApiToken: null,
84
+ tokenSaving: false,
85
+ },
86
+ });
87
+
88
+ assert.match(rendered.main, /API Tokens/);
89
+ assert.match(rendered.main, /Security \/\/ Database API access/);
90
+ assert.match(rendered.main, /Database One/);
91
+ assert.match(rendered.main, /Database ID/);
92
+ assert.match(rendered.main, /data-database-id/);
93
+ assert.match(rendered.main, /data-action="copy-database-id"/);
94
+ assert.match(rendered.main, /Copy ID/);
95
+ assert.match(rendered.main, /sm:grid-cols-\[minmax\(0,1fr\)_9rem\]/);
96
+ assert.equal((rendered.main.match(/w-full self-center justify-center/g) ?? []).length, 2);
97
+ assert.match(rendered.main, /shub_example\.\.\./);
98
+ assert.match(rendered.main, /data-api-token-name/);
99
+ assert.match(rendered.main, /data-form="create-api-token"/);
100
+ assert.match(rendered.main, /name="name"/);
101
+ assert.match(rendered.main, /type="submit"/);
102
+ assert.match(rendered.main, /data-action="open-delete-api-token-modal"/);
103
+ assert.match(rendered.main, /bg-surface-container-lowest/);
104
+ assert.doesNotMatch(rendered.main, /sqlite-hub --port:PORT/);
105
+ assert.doesNotMatch(rendered.main, /Open Github/);
106
+ assert.doesNotMatch(rendered.main, /token_hash/);
32
107
  });
Binary file
Binary file