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.
- package/README.md +47 -33
- package/bin/sqlite-hub.js +127 -47
- package/frontend/js/api.js +6 -0
- package/frontend/js/app.js +229 -44
- package/frontend/js/components/modal.js +17 -1
- package/frontend/js/components/queryChartRenderer.js +28 -3
- package/frontend/js/components/queryEditor.js +24 -26
- package/frontend/js/components/queryHistoryDetail.js +1 -1
- package/frontend/js/components/queryHistoryHeader.js +48 -0
- package/frontend/js/components/queryHistoryList.js +132 -0
- package/frontend/js/components/queryHistoryPanel.js +72 -136
- package/frontend/js/components/structureGraph.js +700 -89
- package/frontend/js/components/tableDesignerEditor.js +26 -47
- package/frontend/js/components/tableDesignerSidebar.js +7 -31
- package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
- package/frontend/js/store.js +97 -8
- package/frontend/js/utils/exportFilenames.js +3 -1
- package/frontend/js/views/charts.js +326 -174
- package/frontend/js/views/connections.js +59 -64
- package/frontend/js/views/data.js +48 -54
- package/frontend/js/views/documents.js +62 -23
- package/frontend/js/views/editor.js +0 -2
- package/frontend/js/views/mediaTagging.js +6 -5
- package/frontend/js/views/settings.js +78 -0
- package/frontend/js/views/structure.js +48 -32
- package/frontend/js/views/tableDesigner.js +45 -1
- package/frontend/styles/components.css +259 -54
- package/frontend/styles/structure-graph.css +150 -37
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +2 -0
- package/frontend/styles/views.css +75 -38
- package/package.json +3 -2
- package/server/routes/export.js +89 -22
- package/server/routes/externalApi.js +84 -2
- package/server/routes/settings.js +37 -28
- package/server/services/appInfoService.js +215 -0
- package/server/services/databaseCommandService.js +50 -6
- package/server/services/sqlite/dataBrowserService.js +11 -3
- package/server/services/sqlite/exportService.js +307 -22
- package/tests/api-token-auth.test.js +110 -1
- package/tests/cli-args.test.js +16 -3
- package/tests/database-command-service.test.js +40 -3
- package/tests/export-blob.test.js +54 -1
- package/tests/export-filenames.test.js +4 -0
- package/tests/settings-api-tokens-route.test.js +22 -1
- package/tests/settings-metadata.test.js +99 -1
- 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-
|
|
156
|
-
border-
|
|
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,
|
|
182
|
+
flex: 0 0 clamp(20rem, 26vw, 22.5rem);
|
|
159
183
|
flex-direction: column;
|
|
160
184
|
min-height: 0;
|
|
161
|
-
width: clamp(20rem,
|
|
185
|
+
width: clamp(20rem, 26vw, 22.5rem);
|
|
162
186
|
}
|
|
163
187
|
|
|
164
188
|
.charts-view__sidebar-header {
|
|
165
|
-
border-bottom: 1px solid var(--
|
|
166
|
-
padding: var(--spacing-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
265
|
-
display:
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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-
|
|
365
|
+
.documents-titlebar {
|
|
366
|
+
align-items: center;
|
|
355
367
|
display: flex;
|
|
356
|
-
flex-
|
|
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.
|
|
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",
|
package/server/routes/export.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
};
|