sqlite-hub 1.0.0 → 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 (40) hide show
  1. package/README.md +47 -33
  2. package/bin/sqlite-hub.js +127 -47
  3. package/frontend/js/api.js +6 -0
  4. package/frontend/js/app.js +199 -34
  5. package/frontend/js/components/modal.js +17 -1
  6. package/frontend/js/components/queryChartRenderer.js +28 -3
  7. package/frontend/js/components/queryEditor.js +20 -24
  8. package/frontend/js/components/queryHistoryDetail.js +1 -1
  9. package/frontend/js/components/queryHistoryHeader.js +48 -0
  10. package/frontend/js/components/queryHistoryList.js +132 -0
  11. package/frontend/js/components/queryHistoryPanel.js +72 -136
  12. package/frontend/js/components/structureGraph.js +699 -89
  13. package/frontend/js/components/tableDesignerEditor.js +3 -5
  14. package/frontend/js/store.js +73 -7
  15. package/frontend/js/utils/exportFilenames.js +3 -1
  16. package/frontend/js/views/charts.js +320 -169
  17. package/frontend/js/views/connections.js +59 -64
  18. package/frontend/js/views/data.js +12 -20
  19. package/frontend/js/views/editor.js +0 -2
  20. package/frontend/js/views/settings.js +78 -0
  21. package/frontend/js/views/structure.js +27 -13
  22. package/frontend/styles/components.css +155 -0
  23. package/frontend/styles/structure-graph.css +140 -35
  24. package/frontend/styles/tailwind.generated.css +1 -1
  25. package/frontend/styles/views.css +12 -6
  26. package/package.json +3 -2
  27. package/server/routes/export.js +89 -22
  28. package/server/routes/externalApi.js +84 -2
  29. package/server/routes/settings.js +37 -28
  30. package/server/services/appInfoService.js +215 -0
  31. package/server/services/databaseCommandService.js +50 -6
  32. package/server/services/sqlite/exportService.js +307 -22
  33. package/tests/api-token-auth.test.js +110 -1
  34. package/tests/cli-args.test.js +16 -3
  35. package/tests/database-command-service.test.js +39 -2
  36. package/tests/export-blob.test.js +54 -1
  37. package/tests/export-filenames.test.js +4 -0
  38. package/tests/settings-api-tokens-route.test.js +22 -1
  39. package/tests/settings-metadata.test.js +99 -1
  40. package/tests/settings-view.test.js +28 -0
@@ -1,10 +1,29 @@
1
1
  const Database = require("better-sqlite3");
2
2
  const assert = require("node:assert/strict");
3
+ const parquet = require("parquetjs-lite");
3
4
  const test = require("node:test");
4
5
  const { ExportService } = require("../server/services/sqlite/exportService");
5
6
  const { SqlExecutor } = require("../server/services/sqlite/sqlExecutor");
6
7
 
7
- test("query and table exports include complete BLOB data", () => {
8
+ async function readParquetRows(buffer) {
9
+ const reader = await parquet.ParquetReader.openBuffer(buffer);
10
+ const cursor = reader.getCursor();
11
+ const rows = [];
12
+
13
+ try {
14
+ let row;
15
+
16
+ while ((row = await cursor.next())) {
17
+ rows.push(row);
18
+ }
19
+ } finally {
20
+ await reader.close();
21
+ }
22
+
23
+ return rows;
24
+ }
25
+
26
+ test("query and table exports include complete BLOB data", async () => {
8
27
  const db = new Database(":memory:");
9
28
  const blob = Buffer.from(Array.from({ length: 100 }, (_, index) => index));
10
29
 
@@ -33,13 +52,47 @@ test("query and table exports include complete BLOB data", () => {
33
52
  "SELECT payload FROM files ORDER BY id",
34
53
  { format: "csv" }
35
54
  );
55
+ const queryJsonExport = exportService.exportQuery(
56
+ "SELECT payload FROM files ORDER BY id",
57
+ { format: "json" }
58
+ );
36
59
  const tableExport = exportService.exportTable("files", { format: "tsv" });
60
+ const jsonExport = exportService.exportTable("files", { format: "json" });
61
+ const queryParquetExport = await exportService.exportQueryDownload(
62
+ "SELECT id, payload FROM files ORDER BY id",
63
+ { format: "parquet" }
64
+ );
65
+ const tableParquetExport = await exportService.exportTableDownload("files", { format: "parquet" });
37
66
 
38
67
  for (const content of [queryExport.content, tableExport.content]) {
39
68
  assert.equal(content.includes(expectedBase64), true);
40
69
  assert.match(content, /""encoding"":""base64""/);
41
70
  assert.doesNotMatch(content, /base64Preview|hexPreview/);
42
71
  }
72
+
73
+ assert.equal(jsonExport.mimeType, "application/json; charset=utf-8");
74
+ assert.equal(jsonExport.filename, "files.json");
75
+
76
+ const [queryJsonRow] = JSON.parse(queryJsonExport.content);
77
+ const [jsonRow] = JSON.parse(jsonExport.content);
78
+ assert.equal(queryJsonExport.mimeType, "application/json; charset=utf-8");
79
+ assert.equal(queryJsonRow.payload.encoding, "base64");
80
+ assert.equal(queryJsonRow.payload.data, expectedBase64);
81
+ assert.equal(jsonRow.payload.encoding, "base64");
82
+ assert.equal(jsonRow.payload.data, expectedBase64);
83
+ assert.equal(jsonRow.payload.sizeBytes, blob.length);
84
+ assert.equal(jsonExport.content.includes("base64Preview"), false);
85
+ assert.equal(jsonExport.content.includes("hexPreview"), false);
86
+
87
+ assert.equal(queryParquetExport.mimeType, "application/vnd.apache.parquet");
88
+ assert.equal(queryParquetExport.filename.endsWith(".parquet"), true);
89
+ assert.equal(Buffer.isBuffer(queryParquetExport.content), true);
90
+ assert.equal(tableParquetExport.filename, "files.parquet");
91
+
92
+ const [queryParquetRow] = await readParquetRows(queryParquetExport.content);
93
+ const [tableParquetRow] = await readParquetRows(tableParquetExport.content);
94
+ assert.equal(queryParquetRow.payload.equals(blob), true);
95
+ assert.equal(tableParquetRow.payload.equals(blob), true);
43
96
  } finally {
44
97
  db.close();
45
98
  }
@@ -24,6 +24,8 @@ test("text export filenames use the selected format extension", async () => {
24
24
 
25
25
  assert.equal(buildTextExportFilename("query-results.csv", { format: "tsv" }), "query-results.tsv");
26
26
  assert.equal(buildTextExportFilename("white_house_live_streams", { format: "md" }), "white_house_live_streams.md");
27
+ assert.equal(buildTextExportFilename("companies.tsv", { format: "json" }), "companies.json");
28
+ assert.equal(buildTextExportFilename("companies.json", { format: "parquet" }), "companies.parquet");
27
29
  });
28
30
 
29
31
  test("text export filenames sanitize unsafe names and fallback when empty", async () => {
@@ -31,4 +33,6 @@ test("text export filenames sanitize unsafe names and fallback when empty", asyn
31
33
 
32
34
  assert.equal(buildTextExportFilename("", { format: "csv", fallback: "table" }), "table.csv");
33
35
  assert.equal(buildTextExportFilename("../bad:name?.csv", { format: "csv", fallback: "table" }), "bad name.csv");
36
+ assert.equal(buildTextExportFilename("bad:name?.json", { format: "json", fallback: "table" }), "bad name.json");
37
+ assert.equal(buildTextExportFilename("bad:name?.parquet", { format: "parquet", fallback: "table" }), "bad name.parquet");
34
38
  });
@@ -35,7 +35,20 @@ test("settings routes create and delete tokens for the active database", async (
35
35
  app.use(express.json());
36
36
  app.use(
37
37
  "/api/settings",
38
- createSettingsRouter({ appStateStore: store, connectionManager, tokenService })
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
+ })
39
52
  );
40
53
  app.use(errorMiddleware);
41
54
 
@@ -73,4 +86,12 @@ test("settings routes create and delete tokens for the active database", async (
73
86
  assert.equal(deleteResponse.status, 200);
74
87
  assert.deepEqual(deleted.data, { id: created.data.id, deleted: true });
75
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);
76
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,11 +29,39 @@ 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\./);
32
36
  assert.match(rendered.main, /data-section="information"/);
33
37
  assert.match(rendered.main, /data-section="api-tokens"/);
34
38
  assert.doesNotMatch(rendered.main, /data-form="create-api-token"/);
35
39
  });
36
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
+
37
65
  test("settings view scopes API token controls to the active database", async () => {
38
66
  const { renderSettingsView } = await loadSettingsViewModule();
39
67
  const rendered = renderSettingsView({