sqlite-hub 0.3.1 → 0.4.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 +12 -6
- package/changelog.md +12 -0
- package/{js → frontend/js}/api.js +7 -0
- package/{js → frontend/js}/app.js +87 -3
- package/{js → frontend/js}/components/connectionCard.js +8 -9
- package/frontend/js/components/connectionLogo.js +33 -0
- package/{js → frontend/js}/components/emptyState.js +25 -11
- package/{js → frontend/js}/components/modal.js +131 -0
- package/{js → frontend/js}/components/rowEditorPanel.js +63 -33
- package/{js → frontend/js}/components/sidebar.js +8 -3
- package/{js → frontend/js}/store.js +236 -1
- package/{js → frontend/js}/views/data.js +106 -9
- package/{js → frontend/js}/views/editor.js +4 -0
- package/{js → frontend/js}/views/structure.js +10 -12
- package/{styles → frontend/styles}/structure-graph.css +5 -10
- package/package.json +3 -3
- package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
- package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
- package/server/data/db_logos/.gitkeep +0 -0
- package/server/routes/connections.js +2 -0
- package/server/routes/data.js +14 -0
- package/server/server.js +29 -6
- package/server/services/sqlite/connectionManager.js +68 -33
- package/server/services/sqlite/dataBrowserService.js +30 -0
- package/server/services/storage/appStateStore.js +159 -20
- package/server/utils/appPaths.js +42 -18
- /package/{assets → frontend/assets}/images/logo.webp +0 -0
- /package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
- /package/{assets → frontend/assets}/images/logo_raw.png +0 -0
- /package/{assets → frontend/assets}/images/logo_small.webp +0 -0
- /package/{assets → frontend/assets}/mockups/connections.png +0 -0
- /package/{assets → frontend/assets}/mockups/data.png +0 -0
- /package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
- /package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
- /package/{assets → frontend/assets}/mockups/home.png +0 -0
- /package/{assets → frontend/assets}/mockups/overview.png +0 -0
- /package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
- /package/{assets → frontend/assets}/mockups/structure.png +0 -0
- /package/{index.html → frontend/index.html} +0 -0
- /package/{js → frontend/js}/components/actionBar.js +0 -0
- /package/{js → frontend/js}/components/appShell.js +0 -0
- /package/{js → frontend/js}/components/badges.js +0 -0
- /package/{js → frontend/js}/components/bottomTabs.js +0 -0
- /package/{js → frontend/js}/components/dataGrid.js +0 -0
- /package/{js → frontend/js}/components/metricCard.js +0 -0
- /package/{js → frontend/js}/components/pageHeader.js +0 -0
- /package/{js → frontend/js}/components/queryEditor.js +0 -0
- /package/{js → frontend/js}/components/queryResults.js +0 -0
- /package/{js → frontend/js}/components/statusBar.js +0 -0
- /package/{js → frontend/js}/components/structureGraph.js +0 -0
- /package/{js → frontend/js}/components/toast.js +0 -0
- /package/{js → frontend/js}/components/topNav.js +0 -0
- /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
- /package/{js → frontend/js}/router.js +0 -0
- /package/{js → frontend/js}/utils/format.js +0 -0
- /package/{js → frontend/js}/views/connections.js +0 -0
- /package/{js → frontend/js}/views/landing.js +0 -0
- /package/{js → frontend/js}/views/overview.js +0 -0
- /package/{js → frontend/js}/views/settings.js +0 -0
- /package/{styles → frontend/styles}/base.css +0 -0
- /package/{styles → frontend/styles}/components.css +0 -0
- /package/{styles → frontend/styles}/layout.css +0 -0
- /package/{styles → frontend/styles}/tokens.css +0 -0
- /package/{styles → frontend/styles}/views.css +0 -0
- /package/{data → server/data}/.gitkeep +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
|
-
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
6
|
cd "$ROOT_DIR"
|
|
7
7
|
|
|
8
8
|
DRY_RUN=0
|
|
@@ -15,7 +15,7 @@ GH_REMOTE="origin"
|
|
|
15
15
|
usage() {
|
|
16
16
|
cat <<'EOF'
|
|
17
17
|
Usage:
|
|
18
|
-
./publish_npm.sh [options]
|
|
18
|
+
./scripts/publish_npm.sh [options]
|
|
19
19
|
|
|
20
20
|
Options:
|
|
21
21
|
--tag NAME Publish to a custom npm dist-tag instead of `latest`.
|
|
File without changes
|
|
@@ -113,6 +113,8 @@ function createConnectionsRouter({ connectionManager, importService, backupServi
|
|
|
113
113
|
filePath: req.body.path,
|
|
114
114
|
label: req.body.label,
|
|
115
115
|
readOnly: Boolean(req.body.readOnly),
|
|
116
|
+
logoUpload: req.body.logoUpload ?? null,
|
|
117
|
+
clearLogo: Boolean(req.body.clearLogo),
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
res.json(
|
package/server/routes/data.js
CHANGED
|
@@ -51,6 +51,20 @@ function createDataRouter({ dataBrowserService }) {
|
|
|
51
51
|
})
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
+
router.delete(
|
|
55
|
+
"/:tableName/rows",
|
|
56
|
+
route((req, res) => {
|
|
57
|
+
const data = dataBrowserService.deleteTableRow(req.params.tableName, req.body ?? {});
|
|
58
|
+
|
|
59
|
+
res.json(
|
|
60
|
+
successResponse({
|
|
61
|
+
message: "Table row deleted.",
|
|
62
|
+
data,
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
54
68
|
return router;
|
|
55
69
|
}
|
|
56
70
|
|
package/server/server.js
CHANGED
|
@@ -20,7 +20,10 @@ const { createSettingsRouter } = require("./routes/settings");
|
|
|
20
20
|
const { createExportRouter } = require("./routes/export");
|
|
21
21
|
|
|
22
22
|
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
23
|
+
const FRONTEND_ROOT = path.join(PACKAGE_ROOT, "frontend");
|
|
24
|
+
const FRONTEND_ENTRYPOINT = path.join(FRONTEND_ROOT, "index.html");
|
|
23
25
|
const {
|
|
26
|
+
appStateDirectory: APP_STATE_DIRECTORY,
|
|
24
27
|
appStateDbPath: APP_STATE_DB_PATH,
|
|
25
28
|
legacyStatePath: LEGACY_STATE_PATH,
|
|
26
29
|
legacyDatabasePaths: LEGACY_DATABASE_PATHS,
|
|
@@ -83,11 +86,11 @@ app.get("/favicon.ico", (req, res) => {
|
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
app.get("/", (req, res) => {
|
|
86
|
-
res.sendFile(
|
|
89
|
+
res.sendFile(FRONTEND_ENTRYPOINT);
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
app.get("/index.html", (req, res) => {
|
|
90
|
-
res.sendFile(
|
|
93
|
+
res.sendFile(FRONTEND_ENTRYPOINT);
|
|
91
94
|
});
|
|
92
95
|
|
|
93
96
|
app.use(
|
|
@@ -102,12 +105,31 @@ app.use(
|
|
|
102
105
|
"/vendor/elkjs",
|
|
103
106
|
express.static(path.resolve(__dirname, "..", "node_modules", "elkjs"))
|
|
104
107
|
);
|
|
105
|
-
app.use(
|
|
106
|
-
app.use("/
|
|
107
|
-
app.use("/assets", express.static(path.resolve(__dirname, "..", "assets")));
|
|
108
|
+
app.use(express.static(FRONTEND_ROOT));
|
|
109
|
+
app.use("/db_logos", express.static(path.join(APP_STATE_DIRECTORY, "db_logos")));
|
|
108
110
|
app.use(errorMiddleware);
|
|
109
111
|
|
|
110
|
-
function
|
|
112
|
+
function parsePortArgument(argv = process.argv.slice(2)) {
|
|
113
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
114
|
+
const argument = argv[index];
|
|
115
|
+
|
|
116
|
+
if (argument.startsWith("--port:")) {
|
|
117
|
+
return argument.slice("--port:".length);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (argument.startsWith("--port=")) {
|
|
121
|
+
return argument.slice("--port=".length);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (argument === "--port") {
|
|
125
|
+
return argv[index + 1];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolvePort(value = process.env.PORT ?? parsePortArgument()) {
|
|
111
133
|
if (value === undefined || value === null || value === "") {
|
|
112
134
|
return DEFAULT_PORT;
|
|
113
135
|
}
|
|
@@ -153,6 +175,7 @@ module.exports = {
|
|
|
153
175
|
appStateStore,
|
|
154
176
|
connectionManager,
|
|
155
177
|
DEFAULT_PORT,
|
|
178
|
+
parsePortArgument,
|
|
156
179
|
resolvePort,
|
|
157
180
|
startServer,
|
|
158
181
|
};
|
|
@@ -44,6 +44,7 @@ class ConnectionManager {
|
|
|
44
44
|
filePath: recent.path,
|
|
45
45
|
label: recent.label,
|
|
46
46
|
id: recent.id,
|
|
47
|
+
logoPath: recent.logoPath ?? null,
|
|
47
48
|
makeActive: true,
|
|
48
49
|
});
|
|
49
50
|
} catch (error) {
|
|
@@ -54,17 +55,26 @@ class ConnectionManager {
|
|
|
54
55
|
|
|
55
56
|
buildConnectionRecord(filePath, options = {}) {
|
|
56
57
|
const metadata = getFileMetadata(filePath);
|
|
58
|
+
const id =
|
|
59
|
+
options.id ??
|
|
60
|
+
`conn_${crypto.createHash("sha1").update(filePath).digest("hex").slice(0, 16)}`;
|
|
61
|
+
const existingConnection = this.appStateStore
|
|
62
|
+
.getRecentConnections()
|
|
63
|
+
.find((connection) => connection.id === id);
|
|
64
|
+
const logoPath = Object.prototype.hasOwnProperty.call(options, "logoPath")
|
|
65
|
+
? options.logoPath
|
|
66
|
+
: (existingConnection?.logoPath ?? null);
|
|
57
67
|
|
|
58
68
|
return {
|
|
59
|
-
id
|
|
60
|
-
options.id ??
|
|
61
|
-
`conn_${crypto.createHash("sha1").update(filePath).digest("hex").slice(0, 16)}`,
|
|
69
|
+
id,
|
|
62
70
|
label: options.label?.trim() || path.basename(filePath),
|
|
63
71
|
path: filePath,
|
|
64
72
|
lastOpenedAt: new Date().toISOString(),
|
|
65
73
|
lastModifiedAt: metadata.lastModifiedAt,
|
|
66
74
|
sizeBytes: metadata.sizeBytes,
|
|
67
75
|
readOnly: options.readOnly ?? !isWritable(filePath),
|
|
76
|
+
logoPath,
|
|
77
|
+
logoUrl: this.appStateStore.getConnectionLogoUrl(logoPath),
|
|
68
78
|
};
|
|
69
79
|
}
|
|
70
80
|
|
|
@@ -94,7 +104,7 @@ class ConnectionManager {
|
|
|
94
104
|
this.current = null;
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
openConnection({ filePath, label, id, makeActive = true, readOnly = false }) {
|
|
107
|
+
openConnection({ filePath, label, id, makeActive = true, readOnly = false, logoPath }) {
|
|
98
108
|
const resolvedPath = validateSqlitePath(filePath, { mustExist: true });
|
|
99
109
|
const db = this.openRawDatabase(resolvedPath, {
|
|
100
110
|
fileMustExist: true,
|
|
@@ -103,11 +113,17 @@ class ConnectionManager {
|
|
|
103
113
|
|
|
104
114
|
this.closeCurrent();
|
|
105
115
|
|
|
106
|
-
const
|
|
116
|
+
const connectionOptions = {
|
|
107
117
|
id,
|
|
108
118
|
label,
|
|
109
119
|
readOnly,
|
|
110
|
-
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (logoPath !== undefined) {
|
|
123
|
+
connectionOptions.logoPath = logoPath;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const connection = this.buildConnectionRecord(resolvedPath, connectionOptions);
|
|
111
127
|
|
|
112
128
|
this.current = {
|
|
113
129
|
...connection,
|
|
@@ -186,6 +202,7 @@ class ConnectionManager {
|
|
|
186
202
|
id: recent.id,
|
|
187
203
|
makeActive: true,
|
|
188
204
|
readOnly: recent.readOnly,
|
|
205
|
+
logoPath: recent.logoPath ?? null,
|
|
189
206
|
});
|
|
190
207
|
}
|
|
191
208
|
|
|
@@ -199,7 +216,7 @@ class ConnectionManager {
|
|
|
199
216
|
return state.recentConnections;
|
|
200
217
|
}
|
|
201
218
|
|
|
202
|
-
updateRecentConnection(id, { filePath, label, readOnly = false }) {
|
|
219
|
+
updateRecentConnection(id, { filePath, label, readOnly = false, logoUpload = null, clearLogo = false }) {
|
|
203
220
|
const recentConnections = this.appStateStore.getRecentConnections();
|
|
204
221
|
const existing = recentConnections.find((connection) => connection.id === id);
|
|
205
222
|
|
|
@@ -218,40 +235,58 @@ class ConnectionManager {
|
|
|
218
235
|
|
|
219
236
|
const normalizedLabel = label?.trim() || path.basename(resolvedPath);
|
|
220
237
|
const normalizedReadOnly = Boolean(readOnly);
|
|
238
|
+
let nextLogoPath = clearLogo ? null : existing.logoPath ?? null;
|
|
239
|
+
let createdLogoPath = null;
|
|
221
240
|
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
if (logoUpload) {
|
|
242
|
+
createdLogoPath = this.appStateStore.saveConnectionLogo(id, logoUpload);
|
|
243
|
+
nextLogoPath = createdLogoPath;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
if (this.current?.id === id) {
|
|
248
|
+
return this.openConnection({
|
|
249
|
+
filePath: resolvedPath,
|
|
250
|
+
label: normalizedLabel,
|
|
251
|
+
id,
|
|
252
|
+
makeActive: true,
|
|
253
|
+
readOnly: normalizedReadOnly,
|
|
254
|
+
logoPath: nextLogoPath,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const db = this.openRawDatabase(resolvedPath, {
|
|
259
|
+
fileMustExist: true,
|
|
228
260
|
readOnly: normalizedReadOnly,
|
|
229
261
|
});
|
|
230
|
-
}
|
|
231
262
|
|
|
232
|
-
|
|
233
|
-
fileMustExist: true,
|
|
234
|
-
readOnly: normalizedReadOnly,
|
|
235
|
-
});
|
|
263
|
+
db.close();
|
|
236
264
|
|
|
237
|
-
|
|
265
|
+
const metadata = getFileMetadata(resolvedPath);
|
|
266
|
+
const nextConnection = {
|
|
267
|
+
...existing,
|
|
268
|
+
label: normalizedLabel,
|
|
269
|
+
path: resolvedPath,
|
|
270
|
+
lastModifiedAt: metadata.lastModifiedAt,
|
|
271
|
+
sizeBytes: metadata.sizeBytes,
|
|
272
|
+
readOnly: normalizedReadOnly,
|
|
273
|
+
logoPath: nextLogoPath,
|
|
274
|
+
};
|
|
238
275
|
|
|
239
|
-
|
|
240
|
-
const nextConnection = {
|
|
241
|
-
...existing,
|
|
242
|
-
label: normalizedLabel,
|
|
243
|
-
path: resolvedPath,
|
|
244
|
-
lastModifiedAt: metadata.lastModifiedAt,
|
|
245
|
-
sizeBytes: metadata.sizeBytes,
|
|
246
|
-
readOnly: normalizedReadOnly,
|
|
247
|
-
};
|
|
276
|
+
this.appStateStore.updateRecentConnection(id, () => nextConnection);
|
|
248
277
|
|
|
249
|
-
|
|
278
|
+
return {
|
|
279
|
+
...nextConnection,
|
|
280
|
+
logoUrl: this.appStateStore.getConnectionLogoUrl(nextLogoPath),
|
|
281
|
+
isActive: false,
|
|
282
|
+
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (createdLogoPath) {
|
|
285
|
+
this.appStateStore.deleteConnectionLogo(createdLogoPath);
|
|
286
|
+
}
|
|
250
287
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
isActive: false,
|
|
254
|
-
};
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
255
290
|
}
|
|
256
291
|
|
|
257
292
|
getActiveConnection() {
|
|
@@ -171,6 +171,36 @@ class DataBrowserService {
|
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
deleteTableRow(tableName, payload = {}) {
|
|
175
|
+
this.connectionManager.assertWritable();
|
|
176
|
+
|
|
177
|
+
const db = this.connectionManager.getActiveDatabase();
|
|
178
|
+
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
179
|
+
const identity = payload.identity ?? null;
|
|
180
|
+
|
|
181
|
+
if (tableDetail.notSafelyUpdatable) {
|
|
182
|
+
throw new ValidationError(
|
|
183
|
+
`Table ${tableName} cannot be safely updated because it has no stable row identity.`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const where = this.buildWhereClause(tableDetail, identity);
|
|
188
|
+
const result = db
|
|
189
|
+
.prepare(`DELETE FROM ${quoteIdentifier(tableName)} WHERE ${where.clause}`)
|
|
190
|
+
.run(...where.params);
|
|
191
|
+
|
|
192
|
+
if (!result.changes) {
|
|
193
|
+
throw new NotFoundError(`Row not found in table: ${tableName}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
tableName,
|
|
198
|
+
deleted: true,
|
|
199
|
+
identity,
|
|
200
|
+
affectedRowCount: result.changes,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
174
204
|
buildWhereClause(tableDetail, identity) {
|
|
175
205
|
if (tableDetail.identityStrategy?.type === "rowid") {
|
|
176
206
|
const rowid = identity?.values?.rowid;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
2
|
const path = require("node:path");
|
|
3
3
|
const Database = require("better-sqlite3");
|
|
4
|
+
const { ValidationError } = require("../../utils/errors");
|
|
4
5
|
|
|
5
6
|
const DEFAULT_STATE = {
|
|
6
7
|
recentConnections: [],
|
|
@@ -16,9 +17,24 @@ const DEFAULT_STATE = {
|
|
|
16
17
|
},
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
const CONNECTION_LOGO_DIRECTORY = "db_logos";
|
|
21
|
+
const MAX_CONNECTION_LOGO_SIZE_BYTES = 5 * 1024 * 1024;
|
|
22
|
+
const CONNECTION_LOGO_EXTENSION_BY_MIME_TYPE = {
|
|
23
|
+
"image/jpeg": "jpg",
|
|
24
|
+
"image/png": "png",
|
|
25
|
+
"image/webp": "webp",
|
|
26
|
+
};
|
|
27
|
+
const CONNECTION_LOGO_EXTENSION_BY_FILE_EXTENSION = {
|
|
28
|
+
".jpeg": "jpg",
|
|
29
|
+
".jpg": "jpg",
|
|
30
|
+
".png": "png",
|
|
31
|
+
".webp": "webp",
|
|
32
|
+
};
|
|
33
|
+
|
|
19
34
|
class AppStateStore {
|
|
20
35
|
constructor(filePath, options = {}) {
|
|
21
36
|
this.filePath = filePath;
|
|
37
|
+
this.logoDirectory = path.join(path.dirname(this.filePath), CONNECTION_LOGO_DIRECTORY);
|
|
22
38
|
this.legacyFilePath = options.legacyFilePath ?? null;
|
|
23
39
|
this.legacyDatabasePaths = Array.isArray(options.legacyDatabasePaths)
|
|
24
40
|
? options.legacyDatabasePaths
|
|
@@ -26,6 +42,7 @@ class AppStateStore {
|
|
|
26
42
|
this.isFreshDatabase = !fs.existsSync(filePath);
|
|
27
43
|
|
|
28
44
|
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
45
|
+
fs.mkdirSync(this.logoDirectory, { recursive: true });
|
|
29
46
|
|
|
30
47
|
this.db = new Database(this.filePath);
|
|
31
48
|
this.configureDatabase();
|
|
@@ -64,7 +81,8 @@ class AppStateStore {
|
|
|
64
81
|
lastOpenedAt TEXT NOT NULL,
|
|
65
82
|
lastModifiedAt TEXT,
|
|
66
83
|
sizeBytes INTEGER,
|
|
67
|
-
readOnly INTEGER NOT NULL DEFAULT 0
|
|
84
|
+
readOnly INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
logoPath TEXT
|
|
68
86
|
);
|
|
69
87
|
|
|
70
88
|
CREATE TABLE IF NOT EXISTS sql_history (
|
|
@@ -86,6 +104,17 @@ class AppStateStore {
|
|
|
86
104
|
CREATE INDEX IF NOT EXISTS idx_sql_history_executed_at
|
|
87
105
|
ON sql_history(executedAt DESC, id ASC);
|
|
88
106
|
`);
|
|
107
|
+
|
|
108
|
+
const recentConnectionColumns = new Set(
|
|
109
|
+
this.db
|
|
110
|
+
.prepare("PRAGMA table_info(recent_connections)")
|
|
111
|
+
.all()
|
|
112
|
+
.map((column) => column.name)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!recentConnectionColumns.has("logoPath")) {
|
|
116
|
+
this.db.exec("ALTER TABLE recent_connections ADD COLUMN logoPath TEXT");
|
|
117
|
+
}
|
|
89
118
|
}
|
|
90
119
|
|
|
91
120
|
seedDefaultSettings() {
|
|
@@ -153,6 +182,14 @@ class AppStateStore {
|
|
|
153
182
|
.all()
|
|
154
183
|
.map((row) => row.name)
|
|
155
184
|
);
|
|
185
|
+
const recentConnectionColumns = tables.has("recent_connections")
|
|
186
|
+
? new Set(
|
|
187
|
+
legacyDb
|
|
188
|
+
.prepare("PRAGMA table_info(recent_connections)")
|
|
189
|
+
.all()
|
|
190
|
+
.map((column) => column.name)
|
|
191
|
+
)
|
|
192
|
+
: new Set();
|
|
156
193
|
|
|
157
194
|
return {
|
|
158
195
|
settings: tables.has("settings")
|
|
@@ -173,7 +210,8 @@ class AppStateStore {
|
|
|
173
210
|
lastOpenedAt,
|
|
174
211
|
lastModifiedAt,
|
|
175
212
|
sizeBytes,
|
|
176
|
-
readOnly
|
|
213
|
+
readOnly,
|
|
214
|
+
${recentConnectionColumns.has("logoPath") ? "logoPath" : "NULL AS logoPath"}
|
|
177
215
|
FROM recent_connections
|
|
178
216
|
ORDER BY lastOpenedAt DESC, id ASC
|
|
179
217
|
`)
|
|
@@ -223,16 +261,18 @@ class AppStateStore {
|
|
|
223
261
|
lastOpenedAt,
|
|
224
262
|
lastModifiedAt,
|
|
225
263
|
sizeBytes,
|
|
226
|
-
readOnly
|
|
264
|
+
readOnly,
|
|
265
|
+
logoPath
|
|
227
266
|
)
|
|
228
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
267
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
229
268
|
ON CONFLICT(id) DO UPDATE SET
|
|
230
269
|
label = excluded.label,
|
|
231
270
|
path = excluded.path,
|
|
232
271
|
lastOpenedAt = excluded.lastOpenedAt,
|
|
233
272
|
lastModifiedAt = excluded.lastModifiedAt,
|
|
234
273
|
sizeBytes = excluded.sizeBytes,
|
|
235
|
-
readOnly = excluded.readOnly
|
|
274
|
+
readOnly = excluded.readOnly,
|
|
275
|
+
logoPath = excluded.logoPath
|
|
236
276
|
`);
|
|
237
277
|
const insertHistory = this.db.prepare(`
|
|
238
278
|
INSERT INTO sql_history (
|
|
@@ -276,7 +316,8 @@ class AppStateStore {
|
|
|
276
316
|
connection.lastOpenedAt ?? new Date().toISOString(),
|
|
277
317
|
connection.lastModifiedAt ?? null,
|
|
278
318
|
connection.sizeBytes ?? null,
|
|
279
|
-
connection.readOnly ? 1 : 0
|
|
319
|
+
connection.readOnly ? 1 : 0,
|
|
320
|
+
this.normalizeLogoPath(connection.logoPath)
|
|
280
321
|
);
|
|
281
322
|
}
|
|
282
323
|
|
|
@@ -373,7 +414,7 @@ class AppStateStore {
|
|
|
373
414
|
|
|
374
415
|
const staleRows = this.db
|
|
375
416
|
.prepare(`
|
|
376
|
-
SELECT id
|
|
417
|
+
SELECT id, logoPath
|
|
377
418
|
FROM recent_connections
|
|
378
419
|
ORDER BY lastOpenedAt DESC, id ASC
|
|
379
420
|
LIMIT -1 OFFSET ?
|
|
@@ -393,6 +434,10 @@ class AppStateStore {
|
|
|
393
434
|
if (activeConnectionId && staleRows.some((row) => row.id === activeConnectionId)) {
|
|
394
435
|
this.setMetaValue("activeConnectionId", null);
|
|
395
436
|
}
|
|
437
|
+
|
|
438
|
+
staleRows.forEach((row) => {
|
|
439
|
+
this.deleteConnectionLogo(row.logoPath);
|
|
440
|
+
});
|
|
396
441
|
}
|
|
397
442
|
|
|
398
443
|
trimSqlHistory() {
|
|
@@ -433,19 +478,19 @@ class AppStateStore {
|
|
|
433
478
|
lastOpenedAt,
|
|
434
479
|
lastModifiedAt,
|
|
435
480
|
sizeBytes,
|
|
436
|
-
readOnly
|
|
481
|
+
readOnly,
|
|
482
|
+
logoPath
|
|
437
483
|
FROM recent_connections
|
|
438
484
|
ORDER BY lastOpenedAt DESC, id ASC
|
|
439
485
|
`)
|
|
440
486
|
.all()
|
|
441
|
-
.map((connection) => (
|
|
442
|
-
...connection,
|
|
443
|
-
readOnly: Boolean(connection.readOnly),
|
|
444
|
-
}));
|
|
487
|
+
.map((connection) => this.decorateConnection(connection));
|
|
445
488
|
}
|
|
446
489
|
|
|
447
490
|
upsertRecentConnection(connection, options = {}) {
|
|
448
491
|
const makeActive = options.makeActive !== false;
|
|
492
|
+
const existing = this.getRecentConnections().find((entry) => entry.id === connection.id);
|
|
493
|
+
const nextLogoPath = this.normalizeLogoPath(connection.logoPath);
|
|
449
494
|
|
|
450
495
|
this.db.transaction(() => {
|
|
451
496
|
this.db
|
|
@@ -457,16 +502,18 @@ class AppStateStore {
|
|
|
457
502
|
lastOpenedAt,
|
|
458
503
|
lastModifiedAt,
|
|
459
504
|
sizeBytes,
|
|
460
|
-
readOnly
|
|
505
|
+
readOnly,
|
|
506
|
+
logoPath
|
|
461
507
|
)
|
|
462
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
508
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
463
509
|
ON CONFLICT(id) DO UPDATE SET
|
|
464
510
|
label = excluded.label,
|
|
465
511
|
path = excluded.path,
|
|
466
512
|
lastOpenedAt = excluded.lastOpenedAt,
|
|
467
513
|
lastModifiedAt = excluded.lastModifiedAt,
|
|
468
514
|
sizeBytes = excluded.sizeBytes,
|
|
469
|
-
readOnly = excluded.readOnly
|
|
515
|
+
readOnly = excluded.readOnly,
|
|
516
|
+
logoPath = excluded.logoPath
|
|
470
517
|
`)
|
|
471
518
|
.run(
|
|
472
519
|
connection.id,
|
|
@@ -475,7 +522,8 @@ class AppStateStore {
|
|
|
475
522
|
connection.lastOpenedAt,
|
|
476
523
|
connection.lastModifiedAt ?? null,
|
|
477
524
|
connection.sizeBytes ?? null,
|
|
478
|
-
connection.readOnly ? 1 : 0
|
|
525
|
+
connection.readOnly ? 1 : 0,
|
|
526
|
+
nextLogoPath
|
|
479
527
|
);
|
|
480
528
|
|
|
481
529
|
if (makeActive) {
|
|
@@ -485,10 +533,16 @@ class AppStateStore {
|
|
|
485
533
|
this.trimRecentConnections();
|
|
486
534
|
})();
|
|
487
535
|
|
|
536
|
+
if (this.normalizeLogoPath(existing?.logoPath) !== nextLogoPath) {
|
|
537
|
+
this.deleteConnectionLogo(existing?.logoPath);
|
|
538
|
+
}
|
|
539
|
+
|
|
488
540
|
return this.getRecentConnections();
|
|
489
541
|
}
|
|
490
542
|
|
|
491
543
|
removeRecentConnection(id) {
|
|
544
|
+
const existing = this.getRecentConnections().find((connection) => connection.id === id);
|
|
545
|
+
|
|
492
546
|
this.db.transaction(() => {
|
|
493
547
|
this.db.prepare("DELETE FROM recent_connections WHERE id = ?").run(id);
|
|
494
548
|
|
|
@@ -497,6 +551,8 @@ class AppStateStore {
|
|
|
497
551
|
}
|
|
498
552
|
})();
|
|
499
553
|
|
|
554
|
+
this.deleteConnectionLogo(existing?.logoPath);
|
|
555
|
+
|
|
500
556
|
return this.getState();
|
|
501
557
|
}
|
|
502
558
|
|
|
@@ -518,16 +574,18 @@ class AppStateStore {
|
|
|
518
574
|
lastOpenedAt,
|
|
519
575
|
lastModifiedAt,
|
|
520
576
|
sizeBytes,
|
|
521
|
-
readOnly
|
|
577
|
+
readOnly,
|
|
578
|
+
logoPath
|
|
522
579
|
)
|
|
523
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
580
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
524
581
|
ON CONFLICT(id) DO UPDATE SET
|
|
525
582
|
label = excluded.label,
|
|
526
583
|
path = excluded.path,
|
|
527
584
|
lastOpenedAt = excluded.lastOpenedAt,
|
|
528
585
|
lastModifiedAt = excluded.lastModifiedAt,
|
|
529
586
|
sizeBytes = excluded.sizeBytes,
|
|
530
|
-
readOnly = excluded.readOnly
|
|
587
|
+
readOnly = excluded.readOnly,
|
|
588
|
+
logoPath = excluded.logoPath
|
|
531
589
|
`)
|
|
532
590
|
.run(
|
|
533
591
|
nextConnection.id,
|
|
@@ -536,12 +594,93 @@ class AppStateStore {
|
|
|
536
594
|
nextConnection.lastOpenedAt ?? existing.lastOpenedAt,
|
|
537
595
|
nextConnection.lastModifiedAt ?? null,
|
|
538
596
|
nextConnection.sizeBytes ?? null,
|
|
539
|
-
nextConnection.readOnly ? 1 : 0
|
|
597
|
+
nextConnection.readOnly ? 1 : 0,
|
|
598
|
+
this.normalizeLogoPath(nextConnection.logoPath)
|
|
540
599
|
);
|
|
541
600
|
|
|
601
|
+
if (this.normalizeLogoPath(nextConnection.logoPath) !== this.normalizeLogoPath(existing.logoPath)) {
|
|
602
|
+
this.deleteConnectionLogo(existing.logoPath);
|
|
603
|
+
}
|
|
604
|
+
|
|
542
605
|
return this.getRecentConnections();
|
|
543
606
|
}
|
|
544
607
|
|
|
608
|
+
getConnectionLogoUrl(logoPath) {
|
|
609
|
+
const normalizedLogoPath = this.normalizeLogoPath(logoPath);
|
|
610
|
+
|
|
611
|
+
if (!normalizedLogoPath) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return `/${CONNECTION_LOGO_DIRECTORY}/${encodeURIComponent(normalizedLogoPath)}`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
saveConnectionLogo(connectionId, logoUpload = {}) {
|
|
619
|
+
const fileName = String(logoUpload.fileName ?? "").trim();
|
|
620
|
+
const mimeType = String(logoUpload.mimeType ?? "").trim().toLowerCase();
|
|
621
|
+
const base64 = String(logoUpload.base64 ?? "").trim();
|
|
622
|
+
const extension =
|
|
623
|
+
CONNECTION_LOGO_EXTENSION_BY_MIME_TYPE[mimeType] ??
|
|
624
|
+
CONNECTION_LOGO_EXTENSION_BY_FILE_EXTENSION[path.extname(fileName).toLowerCase()] ??
|
|
625
|
+
null;
|
|
626
|
+
|
|
627
|
+
if (!extension) {
|
|
628
|
+
throw new ValidationError("Connection logos must be one of: .png, .jpg, .jpeg, .webp");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (!base64) {
|
|
632
|
+
throw new ValidationError("Connection logo upload is empty.");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const buffer = Buffer.from(base64, "base64");
|
|
636
|
+
|
|
637
|
+
if (!buffer.length) {
|
|
638
|
+
throw new ValidationError("Connection logo upload is empty.");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (buffer.length > MAX_CONNECTION_LOGO_SIZE_BYTES) {
|
|
642
|
+
throw new ValidationError("Connection logo must be 5 MB or smaller.");
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const safeConnectionId = String(connectionId ?? "connection").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
646
|
+
const storedFileName = `${safeConnectionId}-${Date.now()}.${extension}`;
|
|
647
|
+
|
|
648
|
+
fs.writeFileSync(path.join(this.logoDirectory, storedFileName), buffer);
|
|
649
|
+
|
|
650
|
+
return storedFileName;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
deleteConnectionLogo(logoPath) {
|
|
654
|
+
const normalizedLogoPath = this.normalizeLogoPath(logoPath);
|
|
655
|
+
|
|
656
|
+
if (!normalizedLogoPath) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
fs.rmSync(path.join(this.logoDirectory, normalizedLogoPath), { force: true });
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
decorateConnection(connection = {}) {
|
|
664
|
+
const logoPath = this.normalizeLogoPath(connection.logoPath);
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
...connection,
|
|
668
|
+
readOnly: Boolean(connection.readOnly),
|
|
669
|
+
logoPath,
|
|
670
|
+
logoUrl: this.getConnectionLogoUrl(logoPath),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
normalizeLogoPath(logoPath) {
|
|
675
|
+
const trimmedLogoPath = String(logoPath ?? "").trim();
|
|
676
|
+
|
|
677
|
+
if (!trimmedLogoPath) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return path.basename(trimmedLogoPath);
|
|
682
|
+
}
|
|
683
|
+
|
|
545
684
|
setActiveConnectionId(id) {
|
|
546
685
|
this.setMetaValue("activeConnectionId", id ?? null);
|
|
547
686
|
return this.getActiveConnectionId();
|