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.
- package/README.md +47 -33
- package/bin/sqlite-hub.js +127 -47
- package/frontend/js/api.js +6 -0
- package/frontend/js/app.js +199 -34
- package/frontend/js/components/modal.js +17 -1
- package/frontend/js/components/queryChartRenderer.js +28 -3
- package/frontend/js/components/queryEditor.js +20 -24
- 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 +699 -89
- package/frontend/js/components/tableDesignerEditor.js +3 -5
- package/frontend/js/store.js +73 -7
- package/frontend/js/utils/exportFilenames.js +3 -1
- package/frontend/js/views/charts.js +320 -169
- package/frontend/js/views/connections.js +59 -64
- package/frontend/js/views/data.js +12 -20
- package/frontend/js/views/editor.js +0 -2
- package/frontend/js/views/settings.js +78 -0
- package/frontend/js/views/structure.js +27 -13
- package/frontend/styles/components.css +155 -0
- package/frontend/styles/structure-graph.css +140 -35
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +12 -6
- 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/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 +39 -2
- 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
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
|
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const Database = require("better-sqlite3");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
const VERSION_CHECK_TIMEOUT_MS = 5000;
|
|
6
|
+
|
|
7
|
+
function readPackageMetadata() {
|
|
8
|
+
const packageJsonPath = path.resolve(__dirname, "..", "..", "package.json");
|
|
9
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readAppVersion() {
|
|
13
|
+
const packageJson = readPackageMetadata();
|
|
14
|
+
return packageJson.version ?? "0.0.0";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readPackageName() {
|
|
18
|
+
const packageJson = readPackageMetadata();
|
|
19
|
+
return packageJson.name ?? "sqlite-hub";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseSemver(value) {
|
|
23
|
+
const match = String(value ?? "")
|
|
24
|
+
.trim()
|
|
25
|
+
.replace(/^v/i, "")
|
|
26
|
+
.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/);
|
|
27
|
+
|
|
28
|
+
if (!match) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
major: Number(match[1] ?? 0),
|
|
34
|
+
minor: Number(match[2] ?? 0),
|
|
35
|
+
patch: Number(match[3] ?? 0),
|
|
36
|
+
prerelease: match[4] ?? "",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compareSemver(left, right) {
|
|
41
|
+
const leftVersion = parseSemver(left);
|
|
42
|
+
const rightVersion = parseSemver(right);
|
|
43
|
+
|
|
44
|
+
if (!leftVersion || !rightVersion) {
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
49
|
+
if (leftVersion[key] > rightVersion[key]) {
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (leftVersion[key] < rightVersion[key]) {
|
|
54
|
+
return -1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (leftVersion.prerelease && !rightVersion.prerelease) {
|
|
59
|
+
return -1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!leftVersion.prerelease && rightVersion.prerelease) {
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return leftVersion.prerelease.localeCompare(rightVersion.prerelease);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isNewerVersion(candidateVersion, currentVersion) {
|
|
70
|
+
return compareSemver(candidateVersion, currentVersion) > 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readSqliteVersion() {
|
|
74
|
+
const db = new Database(":memory:");
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
return db.prepare("SELECT sqlite_version() AS version").get().version ?? "unknown";
|
|
78
|
+
} finally {
|
|
79
|
+
db.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readSettingsMetadata() {
|
|
84
|
+
return {
|
|
85
|
+
appVersion: readAppVersion(),
|
|
86
|
+
sqliteVersion: readSqliteVersion(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function fetchJsonWithTimeout(url, options = {}) {
|
|
91
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
92
|
+
|
|
93
|
+
if (typeof fetchImpl !== "function") {
|
|
94
|
+
throw new Error("Fetch API is not available in this runtime.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? VERSION_CHECK_TIMEOUT_MS);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetchImpl(url, {
|
|
102
|
+
headers: {
|
|
103
|
+
Accept: "application/json",
|
|
104
|
+
"User-Agent": "SQLite Hub version check",
|
|
105
|
+
},
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw new Error(`Registry responded with HTTP ${response.status}.`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return response.json();
|
|
114
|
+
} finally {
|
|
115
|
+
clearTimeout(timeout);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function checkLatestAppVersion(options = {}) {
|
|
120
|
+
const packageName = options.packageName ?? readPackageName();
|
|
121
|
+
const currentVersion = options.currentVersion ?? readAppVersion();
|
|
122
|
+
const registryUrl =
|
|
123
|
+
options.registryUrl ??
|
|
124
|
+
`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
125
|
+
const payload = await fetchJsonWithTimeout(registryUrl, options);
|
|
126
|
+
const latestVersion = String(payload?.version ?? "").trim();
|
|
127
|
+
|
|
128
|
+
if (!latestVersion) {
|
|
129
|
+
throw new Error("Registry response did not include a version.");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
packageName,
|
|
134
|
+
currentVersion,
|
|
135
|
+
latestVersion,
|
|
136
|
+
updateAvailable: isNewerVersion(latestVersion, currentVersion),
|
|
137
|
+
checkedAt: new Date().toISOString(),
|
|
138
|
+
source: "npm",
|
|
139
|
+
releaseUrl: `https://www.npmjs.com/package/${packageName}/v/${latestVersion}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeVersionCheckStatus(versionCheck) {
|
|
144
|
+
if (!versionCheck || versionCheck.status === "unknown") {
|
|
145
|
+
return versionCheck ?? null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
versionCheck.currentVersion &&
|
|
150
|
+
versionCheck.latestVersion &&
|
|
151
|
+
compareSemver(versionCheck.currentVersion, versionCheck.latestVersion) > 0
|
|
152
|
+
) {
|
|
153
|
+
return {
|
|
154
|
+
...versionCheck,
|
|
155
|
+
status: "ahead",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...versionCheck,
|
|
161
|
+
status: versionCheck.updateAvailable ? "update_available" : "current",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function buildAppInfo(options = {}) {
|
|
166
|
+
const packageName = options.packageName ?? readPackageName();
|
|
167
|
+
const appVersion = options.currentVersion ?? readAppVersion();
|
|
168
|
+
const sqliteVersion = options.sqliteVersion ?? readSqliteVersion();
|
|
169
|
+
let versionCheck = null;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
versionCheck = await (options.versionCheckService ?? checkLatestAppVersion)({
|
|
173
|
+
...options,
|
|
174
|
+
packageName,
|
|
175
|
+
currentVersion: appVersion,
|
|
176
|
+
});
|
|
177
|
+
versionCheck = normalizeVersionCheckStatus(versionCheck);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
versionCheck = {
|
|
180
|
+
packageName,
|
|
181
|
+
currentVersion: appVersion,
|
|
182
|
+
latestVersion: null,
|
|
183
|
+
updateAvailable: null,
|
|
184
|
+
checkedAt: new Date().toISOString(),
|
|
185
|
+
source: "npm",
|
|
186
|
+
releaseUrl: null,
|
|
187
|
+
status: "unknown",
|
|
188
|
+
error: {
|
|
189
|
+
message: error.message,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
packageName,
|
|
196
|
+
appVersion,
|
|
197
|
+
sqliteVersion,
|
|
198
|
+
port: options.port ?? null,
|
|
199
|
+
url: options.url ?? null,
|
|
200
|
+
versionCheck,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
VERSION_CHECK_TIMEOUT_MS,
|
|
206
|
+
buildAppInfo,
|
|
207
|
+
checkLatestAppVersion,
|
|
208
|
+
compareSemver,
|
|
209
|
+
isNewerVersion,
|
|
210
|
+
readAppVersion,
|
|
211
|
+
readPackageMetadata,
|
|
212
|
+
readPackageName,
|
|
213
|
+
readSettingsMetadata,
|
|
214
|
+
readSqliteVersion,
|
|
215
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const path = require("node:path");
|
|
2
|
-
const { NotFoundError, ValidationError } = require("../utils/errors");
|
|
2
|
+
const { NotFoundError, ReadOnlyError, ValidationError } = require("../utils/errors");
|
|
3
3
|
const { ConnectionManager } = require("./sqlite/connectionManager");
|
|
4
4
|
const { DataBrowserService } = require("./sqlite/dataBrowserService");
|
|
5
5
|
const { ExportService } = require("./sqlite/exportService");
|
|
@@ -16,6 +16,11 @@ function normalizeLookupValue(value, label) {
|
|
|
16
16
|
return normalized;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function normalizeOptionalValue(value) {
|
|
20
|
+
const normalized = String(value ?? "").trim();
|
|
21
|
+
return normalized || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
function getQueryTitle(item) {
|
|
20
25
|
return item?.title || item?.displayTitle || item?.previewSql || item?.rawSql || "(untitled query)";
|
|
21
26
|
}
|
|
@@ -160,7 +165,7 @@ function buildRowJsonObject({ row, columns = [] } = {}) {
|
|
|
160
165
|
class DatabaseCommandService {
|
|
161
166
|
constructor({ appStateStore, runtimeFactory } = {}) {
|
|
162
167
|
this.appStateStore = appStateStore;
|
|
163
|
-
this.runtimeFactory = runtimeFactory ?? ((connection) => this.
|
|
168
|
+
this.runtimeFactory = runtimeFactory ?? ((connection, options) => this.createRuntime(connection, options));
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
listDatabases() {
|
|
@@ -182,7 +187,7 @@ class DatabaseCommandService {
|
|
|
182
187
|
return connection;
|
|
183
188
|
}
|
|
184
189
|
|
|
185
|
-
|
|
190
|
+
createRuntime(connection, { readOnly = true } = {}) {
|
|
186
191
|
const connectionManager = new ConnectionManager({ appStateStore: this.appStateStore });
|
|
187
192
|
|
|
188
193
|
connectionManager.openConnection({
|
|
@@ -191,7 +196,7 @@ class DatabaseCommandService {
|
|
|
191
196
|
id: connection.id,
|
|
192
197
|
logoPath: connection.logoPath ?? null,
|
|
193
198
|
makeActive: false,
|
|
194
|
-
readOnly
|
|
199
|
+
readOnly,
|
|
195
200
|
});
|
|
196
201
|
|
|
197
202
|
const sqlExecutor = new SqlExecutor({
|
|
@@ -215,9 +220,17 @@ class DatabaseCommandService {
|
|
|
215
220
|
};
|
|
216
221
|
}
|
|
217
222
|
|
|
218
|
-
|
|
223
|
+
createReadOnlyRuntime(connection) {
|
|
224
|
+
return this.createRuntime(connection, { readOnly: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
createWritableRuntime(connection) {
|
|
228
|
+
return this.createRuntime(connection, { readOnly: false });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
withDatabase(databaseReference, callback, options = {}) {
|
|
219
232
|
const connection = this.getDatabase(databaseReference);
|
|
220
|
-
const runtime = this.runtimeFactory(connection);
|
|
233
|
+
const runtime = this.runtimeFactory(connection, options);
|
|
221
234
|
|
|
222
235
|
try {
|
|
223
236
|
return callback({ connection, runtime });
|
|
@@ -322,6 +335,37 @@ class DatabaseCommandService {
|
|
|
322
335
|
return { query, result };
|
|
323
336
|
}
|
|
324
337
|
|
|
338
|
+
executeRawQuery(databaseReference, sql, options = {}) {
|
|
339
|
+
const connection = this.getDatabase(databaseReference);
|
|
340
|
+
const { name = null, storeName = null, ...executeOptions } = options;
|
|
341
|
+
const normalizedStoreName = normalizeOptionalValue(storeName ?? name);
|
|
342
|
+
|
|
343
|
+
if (connection.readOnly) {
|
|
344
|
+
throw new ReadOnlyError(`Cannot execute raw SQL against a read-only database: ${connection.label}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const result = this.withDatabase(
|
|
348
|
+
connection.id,
|
|
349
|
+
({ runtime }) => runtime.sqlExecutor.execute(sql, executeOptions),
|
|
350
|
+
{ readOnly: false }
|
|
351
|
+
);
|
|
352
|
+
let storedQuery = null;
|
|
353
|
+
|
|
354
|
+
if (normalizedStoreName && result.historyId) {
|
|
355
|
+
this.appStateStore.renameQuery(result.historyId, normalizedStoreName, connection.id);
|
|
356
|
+
storedQuery = this.appStateStore.toggleSaved(result.historyId, true, connection.id);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
connection,
|
|
361
|
+
result: {
|
|
362
|
+
...result,
|
|
363
|
+
storedQuery,
|
|
364
|
+
},
|
|
365
|
+
storedQuery,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
325
369
|
exportSavedQuery(databaseReference, queryName, format = "csv") {
|
|
326
370
|
const query = this.requireQuery(databaseReference, queryName);
|
|
327
371
|
const result = this.withDatabase(databaseReference, ({ runtime }) =>
|