sqlite-hub 1.0.0 → 1.1.1

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 (47) 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 +229 -44
  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 +24 -26
  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 +700 -89
  13. package/frontend/js/components/tableDesignerEditor.js +26 -47
  14. package/frontend/js/components/tableDesignerSidebar.js +7 -31
  15. package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
  16. package/frontend/js/store.js +97 -8
  17. package/frontend/js/utils/exportFilenames.js +3 -1
  18. package/frontend/js/views/charts.js +326 -174
  19. package/frontend/js/views/connections.js +59 -64
  20. package/frontend/js/views/data.js +48 -54
  21. package/frontend/js/views/documents.js +62 -23
  22. package/frontend/js/views/editor.js +0 -2
  23. package/frontend/js/views/mediaTagging.js +6 -5
  24. package/frontend/js/views/settings.js +78 -0
  25. package/frontend/js/views/structure.js +48 -32
  26. package/frontend/js/views/tableDesigner.js +45 -1
  27. package/frontend/styles/components.css +259 -54
  28. package/frontend/styles/structure-graph.css +150 -37
  29. package/frontend/styles/tailwind.generated.css +1 -1
  30. package/frontend/styles/tokens.css +2 -0
  31. package/frontend/styles/views.css +75 -38
  32. package/package.json +3 -2
  33. package/server/routes/export.js +89 -22
  34. package/server/routes/externalApi.js +84 -2
  35. package/server/routes/settings.js +37 -28
  36. package/server/services/appInfoService.js +215 -0
  37. package/server/services/databaseCommandService.js +50 -6
  38. package/server/services/sqlite/dataBrowserService.js +11 -3
  39. package/server/services/sqlite/exportService.js +307 -22
  40. package/tests/api-token-auth.test.js +110 -1
  41. package/tests/cli-args.test.js +16 -3
  42. package/tests/database-command-service.test.js +40 -3
  43. package/tests/export-blob.test.js +54 -1
  44. package/tests/export-filenames.test.js +4 -0
  45. package/tests/settings-api-tokens-route.test.js +22 -1
  46. package/tests/settings-metadata.test.js +99 -1
  47. package/tests/settings-view.test.js +28 -0
@@ -62,6 +62,30 @@
62
62
  min-width: 0;
63
63
  }
64
64
 
65
+ .table-designer-workspace__toolbar {
66
+ align-items: center;
67
+ display: flex;
68
+ flex-wrap: wrap;
69
+ gap: var(--spacing-4);
70
+ justify-content: space-between;
71
+ }
72
+
73
+ .table-designer-workspace__toolbar-left,
74
+ .table-designer-workspace__toolbar-right {
75
+ align-items: center;
76
+ display: flex;
77
+ flex-wrap: wrap;
78
+ gap: var(--spacing-2);
79
+ }
80
+
81
+ .table-designer-workspace__toolbar-right {
82
+ margin-left: auto;
83
+ }
84
+
85
+ .table-designer-workspace__file-input {
86
+ display: none;
87
+ }
88
+
65
89
  .table-designer-workspace__top,
66
90
  .table-designer-workspace__bottom {
67
91
  min-height: 0;
@@ -152,18 +176,18 @@
152
176
  }
153
177
 
154
178
  .charts-view__sidebar {
155
- background: var(--color-surface-low);
156
- border-right: 1px solid var(--border-medium);
179
+ background: var(--color-surface-container-lowest);
180
+ border-left: 1px solid rgb(var(--rgb-outline) / 0.1);
157
181
  display: flex;
158
- flex: 0 0 clamp(20rem, 28vw, 23rem);
182
+ flex: 0 0 clamp(20rem, 26vw, 22.5rem);
159
183
  flex-direction: column;
160
184
  min-height: 0;
161
- width: clamp(20rem, 28vw, 23rem);
185
+ width: clamp(20rem, 26vw, 22.5rem);
162
186
  }
163
187
 
164
188
  .charts-view__sidebar-header {
165
- border-bottom: 1px solid var(--border-medium);
166
- padding: var(--spacing-5);
189
+ border-bottom: 1px solid rgb(var(--rgb-outline) / 0.1);
190
+ padding: var(--spacing-4);
167
191
  }
168
192
 
169
193
  .structure-view {
@@ -177,23 +201,10 @@
177
201
  .structure-view__sidebar {
178
202
  background: var(--color-surface-low);
179
203
  border-right: 1px solid var(--border-medium);
180
- display: flex;
181
- flex: 0 0 clamp(16rem, 22vw, 18.5rem);
182
- flex-direction: column;
183
- min-height: 0;
184
- width: clamp(16rem, 22vw, 18.5rem);
185
204
  }
186
205
 
187
206
  .structure-view__sidebar-header {
188
- border-bottom: 1px solid var(--border-medium);
189
- padding: var(--spacing-5);
190
- }
191
-
192
- .structure-view__sidebar-body {
193
- flex: 1;
194
- min-height: 0;
195
- overflow-y: auto;
196
- padding: var(--spacing-4);
207
+ align-items: flex-start;
197
208
  }
198
209
 
199
210
  .structure-view__detail {
@@ -245,30 +256,30 @@
245
256
  .documents-view__sidebar {
246
257
  background: var(--color-surface-low);
247
258
  border-right: 1px solid var(--border-medium);
248
- display: flex;
249
- flex: 0 0 clamp(17rem, 23vw, 21rem);
250
- flex-direction: column;
251
- min-height: 100%;
259
+ max-width: var(--subnavi-width);
252
260
  overflow: hidden;
253
- width: clamp(17rem, 23vw, 21rem);
254
261
  }
255
262
 
256
263
  .documents-view__sidebar-header {
257
264
  align-items: center;
258
- border-bottom: 1px solid var(--border-medium);
259
- display: flex;
260
- justify-content: space-between;
261
- padding: var(--spacing-5);
262
265
  }
263
266
 
264
- .documents-view__sidebar-body {
265
- display: flex;
266
- flex: 1;
267
- flex-direction: column;
268
- gap: var(--spacing-2);
267
+ .data-view-grid {
268
+ display: grid;
269
+ grid-template-columns: 1fr;
270
+ height: 100%;
269
271
  min-height: 0;
270
- overflow: hidden;
271
- padding: var(--spacing-3);
272
+ }
273
+
274
+ .data-view__sidebar {
275
+ background: var(--color-surface-low);
276
+ border-right: 1px solid var(--border-medium);
277
+ }
278
+
279
+ @media (min-width: 768px) {
280
+ .data-view-grid--with-subnavi {
281
+ grid-template-columns: var(--subnavi-width) minmax(0, 1fr);
282
+ }
272
283
  }
273
284
 
274
285
  .documents-list-item {
@@ -351,11 +362,20 @@
351
362
  padding: var(--spacing-4);
352
363
  }
353
364
 
354
- .documents-filename-field {
365
+ .documents-titlebar {
366
+ align-items: center;
355
367
  display: flex;
356
- flex-direction: column;
368
+ flex-wrap: wrap;
369
+ gap: var(--spacing-2);
370
+ max-width: min(100%, 44rem);
357
371
  padding: var(--spacing-4);
358
372
  padding-bottom: 0;
373
+ }
374
+
375
+ .documents-filename-field {
376
+ display: flex;
377
+ flex: 1 1 22rem;
378
+ flex-direction: column;
359
379
  max-width: 32rem;
360
380
  min-width: min(100%, 14rem);
361
381
  }
@@ -386,6 +406,10 @@
386
406
  opacity: 1;
387
407
  }
388
408
 
409
+ .documents-create-button {
410
+ flex: 0 0 auto;
411
+ }
412
+
389
413
  .documents-toolbar__actions {
390
414
  display: flex;
391
415
  flex-wrap: wrap;
@@ -624,6 +648,7 @@
624
648
  border-bottom: 1px solid var(--border-medium);
625
649
  border-right: 0;
626
650
  flex: 0 0 auto;
651
+ max-width: none;
627
652
  max-height: 18rem;
628
653
  min-height: 18rem;
629
654
  width: 100%;
@@ -644,6 +669,11 @@
644
669
  margin-top: 1.5rem;
645
670
  }
646
671
 
672
+ .structure-sidebar__section {
673
+ max-width: 100%;
674
+ width: min(var(--subnavi-item-width), 100%);
675
+ }
676
+
647
677
  .charts-view__detail {
648
678
  display: flex;
649
679
  flex: 1;
@@ -652,6 +682,10 @@
652
682
  min-width: 0;
653
683
  }
654
684
 
685
+ .charts-detail-scroll {
686
+ scrollbar-gutter: stable;
687
+ }
688
+
655
689
  .charts-detail-shell {
656
690
  min-height: 100%;
657
691
  padding: var(--spacing-6);
@@ -1628,6 +1662,8 @@
1628
1662
  }
1629
1663
 
1630
1664
  .charts-view__sidebar {
1665
+ border-left: 0;
1666
+ border-top: 1px solid var(--border-medium);
1631
1667
  flex-basis: 18rem;
1632
1668
  width: 100%;
1633
1669
  }
@@ -1638,6 +1674,7 @@
1638
1674
 
1639
1675
  .structure-view__sidebar {
1640
1676
  flex-basis: 18rem;
1677
+ max-width: none;
1641
1678
  width: 100%;
1642
1679
  }
1643
1680
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-hub",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "main": "server/server.js",
6
6
  "bin": {
@@ -27,7 +27,8 @@
27
27
  "express-rate-limit": "^8.5.2",
28
28
  "helmet": "^8.2.0",
29
29
  "marked": "^18.0.5",
30
- "material-symbols": "^0.45.1"
30
+ "material-symbols": "^0.45.1",
31
+ "parquetjs-lite": "^0.8.7"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@tailwindcss/cli": "^4.3.1",
@@ -1,9 +1,19 @@
1
1
  const express = require("express");
2
- const { route, successResponse } = require("../utils/errors");
2
+ const { route, successResponse, ValidationError } = require("../utils/errors");
3
3
 
4
4
  function createExportRouter({ exportService }) {
5
5
  const router = express.Router();
6
6
 
7
+ function normalizeRequestFormat(format = "csv") {
8
+ return String(format ?? "csv").toLowerCase();
9
+ }
10
+
11
+ function assertJsonExportFormat(format) {
12
+ if (normalizeRequestFormat(format) === "parquet") {
13
+ throw new ValidationError("Parquet exports are binary and must use the download endpoint.");
14
+ }
15
+ }
16
+
7
17
  function sendExport(res, result) {
8
18
  res.setHeader("Content-Type", result.mimeType || "text/plain; charset=utf-8");
9
19
  res.setHeader(
@@ -13,13 +23,13 @@ function createExportRouter({ exportService }) {
13
23
  res.send(result.content ?? result.csv ?? "");
14
24
  }
15
25
 
16
- function sendQueryExport(res, sql, format) {
17
- const result = exportService.exportQuery(sql, { format });
26
+ async function sendQueryExport(res, sql, format) {
27
+ const result = await exportService.exportQueryDownload(sql, { format });
18
28
  sendExport(res, result);
19
29
  }
20
30
 
21
- function exportTableFromBody(body, format) {
22
- return exportService.exportTable(body?.tableName, {
31
+ async function exportTableFromBody(body, format) {
32
+ return exportService.exportTableDownload(body?.tableName, {
23
33
  sortColumn: body?.sortColumn,
24
34
  sortDirection: body?.sortDirection,
25
35
  filterColumn: body?.filterColumn,
@@ -29,13 +39,14 @@ function createExportRouter({ exportService }) {
29
39
  });
30
40
  }
31
41
 
32
- function sendTableExport(res, body, format) {
33
- sendExport(res, exportTableFromBody(body, format));
42
+ async function sendTableExport(res, body, format) {
43
+ sendExport(res, await exportTableFromBody(body, format));
34
44
  }
35
45
 
36
46
  router.post(
37
47
  "/query",
38
48
  route((req, res) => {
49
+ assertJsonExportFormat(req.body?.format);
39
50
  const result = exportService.exportQuery(req.body?.sql, {
40
51
  format: req.body?.format || "csv",
41
52
  });
@@ -57,29 +68,51 @@ function createExportRouter({ exportService }) {
57
68
 
58
69
  router.post(
59
70
  "/query.csv",
60
- route((req, res) => {
61
- sendQueryExport(res, req.body?.sql, "csv");
71
+ route(async (req, res) => {
72
+ await sendQueryExport(res, req.body?.sql, "csv");
62
73
  })
63
74
  );
64
75
 
65
76
  router.post(
66
77
  "/query.tsv",
67
- route((req, res) => {
68
- sendQueryExport(res, req.body?.sql, "tsv");
78
+ route(async (req, res) => {
79
+ await sendQueryExport(res, req.body?.sql, "tsv");
69
80
  })
70
81
  );
71
82
 
72
83
  router.post(
73
84
  "/query.md",
74
- route((req, res) => {
75
- sendQueryExport(res, req.body?.sql, "md");
85
+ route(async (req, res) => {
86
+ await sendQueryExport(res, req.body?.sql, "md");
87
+ })
88
+ );
89
+
90
+ router.post(
91
+ "/query.json",
92
+ route(async (req, res) => {
93
+ await sendQueryExport(res, req.body?.sql, "json");
94
+ })
95
+ );
96
+
97
+ router.post(
98
+ "/query.parquet",
99
+ route(async (req, res) => {
100
+ await sendQueryExport(res, req.body?.sql, "parquet");
76
101
  })
77
102
  );
78
103
 
79
104
  router.post(
80
105
  "/table",
81
106
  route((req, res) => {
82
- const result = exportTableFromBody(req.body, req.body?.format || "csv");
107
+ assertJsonExportFormat(req.body?.format);
108
+ const result = exportService.exportTable(req.body?.tableName, {
109
+ sortColumn: req.body?.sortColumn,
110
+ sortDirection: req.body?.sortDirection,
111
+ filterColumn: req.body?.filterColumn,
112
+ filterOperator: req.body?.filterOperator,
113
+ filterValue: req.body?.filterValue,
114
+ format: req.body?.format || "csv",
115
+ });
83
116
 
84
117
  res.json(
85
118
  successResponse({
@@ -98,29 +131,63 @@ function createExportRouter({ exportService }) {
98
131
 
99
132
  router.post(
100
133
  "/table.csv",
101
- route((req, res) => {
102
- sendTableExport(res, req.body, "csv");
134
+ route(async (req, res) => {
135
+ await sendTableExport(res, req.body, "csv");
103
136
  })
104
137
  );
105
138
 
106
139
  router.post(
107
140
  "/table.tsv",
108
- route((req, res) => {
109
- sendTableExport(res, req.body, "tsv");
141
+ route(async (req, res) => {
142
+ await sendTableExport(res, req.body, "tsv");
110
143
  })
111
144
  );
112
145
 
113
146
  router.post(
114
147
  "/table.md",
115
- route((req, res) => {
116
- sendTableExport(res, req.body, "md");
148
+ route(async (req, res) => {
149
+ await sendTableExport(res, req.body, "md");
150
+ })
151
+ );
152
+
153
+ router.post(
154
+ "/table.json",
155
+ route(async (req, res) => {
156
+ await sendTableExport(res, req.body, "json");
157
+ })
158
+ );
159
+
160
+ router.post(
161
+ "/table.parquet",
162
+ route(async (req, res) => {
163
+ await sendTableExport(res, req.body, "parquet");
117
164
  })
118
165
  );
119
166
 
120
167
  router.get(
121
168
  "/table/:tableName.csv",
122
- route((req, res) => {
123
- const result = exportService.exportTable(req.params.tableName);
169
+ route(async (req, res) => {
170
+ const result = await exportService.exportTableDownload(req.params.tableName);
171
+ sendExport(res, result);
172
+ })
173
+ );
174
+
175
+ router.get(
176
+ "/table/:tableName.json",
177
+ route(async (req, res) => {
178
+ const result = await exportService.exportTableDownload(req.params.tableName, {
179
+ format: "json",
180
+ });
181
+ sendExport(res, result);
182
+ })
183
+ );
184
+
185
+ router.get(
186
+ "/table/:tableName.parquet",
187
+ route(async (req, res) => {
188
+ const result = await exportService.exportTableDownload(req.params.tableName, {
189
+ format: "parquet",
190
+ });
124
191
  sendExport(res, result);
125
192
  })
126
193
  );
@@ -1,10 +1,92 @@
1
1
  const express = require("express");
2
2
  const { createApiTokenAuth } = require("../middleware/apiTokenAuth");
3
- const { route, successResponse } = require("../utils/errors");
3
+ const { readBearerToken } = require("../middleware/apiTokenAuth");
4
+ const { AuthenticationError, ValidationError, route, successResponse } = require("../utils/errors");
5
+ const { buildAppInfo } = require("../services/appInfoService");
4
6
 
5
- function createExternalApiRouter({ databaseService, tokenService }) {
7
+ function buildRequestBaseUrl(req) {
8
+ const host = req.get("host") ?? `127.0.0.1:${req.socket.localPort ?? ""}`;
9
+ return `${req.protocol}://${host}`;
10
+ }
11
+
12
+ function readDatabaseId(req) {
13
+ return String(req.body?.databaseId ?? req.query.databaseId ?? "").trim();
14
+ }
15
+
16
+ function readSqlText(req) {
17
+ return String(req.body?.sql ?? req.body?.query ?? req.query.sql ?? req.query.query ?? "");
18
+ }
19
+
20
+ function readStoreName(req) {
21
+ return String(
22
+ req.body?.store ??
23
+ req.body?.storeName ??
24
+ req.body?.name ??
25
+ req.query.store ??
26
+ req.query.storeName ??
27
+ req.query.name ??
28
+ ""
29
+ ).trim();
30
+ }
31
+
32
+ function authenticateDatabaseRequest(req, tokenService, databaseId) {
33
+ const token = readBearerToken(req.get("authorization"));
34
+
35
+ if (!token) {
36
+ throw new AuthenticationError("Bearer API token is required.", {
37
+ code: "API_TOKEN_REQUIRED",
38
+ });
39
+ }
40
+
41
+ return tokenService.authenticate(databaseId, token);
42
+ }
43
+
44
+ function createExternalApiRouter({ databaseService, tokenService, appInfoService = buildAppInfo }) {
6
45
  const router = express.Router();
7
46
 
47
+ router.get(
48
+ "/info",
49
+ route(async (req, res) => {
50
+ const port = Number(req.socket.localPort);
51
+ const data = await appInfoService({
52
+ port: Number.isInteger(port) ? port : null,
53
+ url: buildRequestBaseUrl(req),
54
+ });
55
+
56
+ res.json(successResponse({ data }));
57
+ })
58
+ );
59
+
60
+ router.post(
61
+ "/query",
62
+ route((req, res) => {
63
+ const databaseId = readDatabaseId(req);
64
+ const sql = readSqlText(req);
65
+ const storeName = readStoreName(req);
66
+
67
+ if (!databaseId) {
68
+ throw new ValidationError("databaseId is required.");
69
+ }
70
+
71
+ authenticateDatabaseRequest(req, tokenService, databaseId);
72
+
73
+ const { result } = databaseService.executeRawQuery(databaseId, sql, { storeName });
74
+
75
+ res.json(
76
+ successResponse({
77
+ message: "SQL executed successfully.",
78
+ data: result,
79
+ metadata: {
80
+ databaseId,
81
+ stored: Boolean(result.storedQuery),
82
+ },
83
+ timingMs: result.timingMs,
84
+ readOnly: false,
85
+ })
86
+ );
87
+ })
88
+ );
89
+
8
90
  router.use(
9
91
  "/databases/:databaseId",
10
92
  createApiTokenAuth({ tokenService })
@@ -1,31 +1,12 @@
1
1
  const express = require("express");
2
- const Database = require("better-sqlite3");
3
- const fs = require("node:fs");
4
- const path = require("node:path");
5
- const { DatabaseRequiredError, route, successResponse } = require("../utils/errors");
6
-
7
- function readAppVersion() {
8
- const packageJsonPath = path.resolve(__dirname, "..", "..", "package.json");
9
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
10
- return packageJson.version ?? "0.0.0";
11
- }
12
-
13
- function readSqliteVersion() {
14
- const db = new Database(":memory:");
15
-
16
- try {
17
- return db.prepare("SELECT sqlite_version() AS version").get().version ?? "unknown";
18
- } finally {
19
- db.close();
20
- }
21
- }
22
-
23
- function readSettingsMetadata() {
24
- return {
25
- appVersion: readAppVersion(),
26
- sqliteVersion: readSqliteVersion(),
27
- };
28
- }
2
+ const { AppError, DatabaseRequiredError, route, successResponse } = require("../utils/errors");
3
+ const {
4
+ checkLatestAppVersion,
5
+ compareSemver,
6
+ isNewerVersion,
7
+ readSettingsMetadata,
8
+ readSqliteVersion,
9
+ } = require("../services/appInfoService");
29
10
 
30
11
  function getActiveTokenContext({ connectionManager, tokenService }) {
31
12
  const activeDatabase = connectionManager?.getActiveConnection?.() ?? null;
@@ -53,9 +34,10 @@ function buildSettingsMetadata(context) {
53
34
  };
54
35
  }
55
36
 
56
- function createSettingsRouter({ appStateStore, connectionManager, tokenService }) {
37
+ function createSettingsRouter({ appStateStore, connectionManager, tokenService, versionCheckService }) {
57
38
  const router = express.Router();
58
39
  const context = { connectionManager, tokenService };
40
+ const checkVersion = versionCheckService ?? checkLatestAppVersion;
59
41
 
60
42
  router.get(
61
43
  "/",
@@ -83,6 +65,30 @@ function createSettingsRouter({ appStateStore, connectionManager, tokenService }
83
65
  })
84
66
  );
85
67
 
68
+ router.get(
69
+ "/version-check",
70
+ route(async (req, res) => {
71
+ try {
72
+ const result = await checkVersion();
73
+
74
+ res.json(
75
+ successResponse({
76
+ data: result,
77
+ metadata: readSettingsMetadata(),
78
+ })
79
+ );
80
+ } catch (error) {
81
+ throw new AppError("Version check failed. Check your internet connection and try again.", 502, {
82
+ code: "VERSION_CHECK_FAILED",
83
+ details: {
84
+ source: "npm",
85
+ message: error.message,
86
+ },
87
+ });
88
+ }
89
+ })
90
+ );
91
+
86
92
  router.post(
87
93
  "/api-tokens",
88
94
  route((req, res) => {
@@ -121,6 +127,9 @@ function createSettingsRouter({ appStateStore, connectionManager, tokenService }
121
127
  module.exports = {
122
128
  createSettingsRouter,
123
129
  buildSettingsMetadata,
130
+ checkLatestAppVersion,
131
+ compareSemver,
132
+ isNewerVersion,
124
133
  readSettingsMetadata,
125
134
  readSqliteVersion,
126
135
  };