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
|
@@ -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 }) =>
|
|
@@ -87,9 +87,17 @@ class DataBrowserService {
|
|
|
87
87
|
|
|
88
88
|
return getRawStructureEntries(db)
|
|
89
89
|
.filter((entry) => entry.type === "table")
|
|
90
|
-
.map((entry) =>
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
.map((entry) => {
|
|
91
|
+
const columnCount = db
|
|
92
|
+
.prepare(`PRAGMA table_xinfo(${quoteIdentifier(entry.name)})`)
|
|
93
|
+
.all()
|
|
94
|
+
.filter((column) => Number(column.hidden ?? 0) === 0).length;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
name: entry.name,
|
|
98
|
+
columnCount,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
getTableData(tableName, options = {}) {
|