sqlite-hub 0.3.2 → 0.5.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 +5 -5
- package/changelog.md +14 -0
- package/{js → frontend/js}/api.js +99 -5
- package/{js → frontend/js}/app.js +162 -11
- package/{js → frontend/js}/components/connectionCard.js +8 -9
- package/frontend/js/components/connectionLogo.js +33 -0
- package/{js → frontend/js}/components/dataGrid.js +3 -3
- package/{js → frontend/js}/components/emptyState.js +25 -11
- package/{js → frontend/js}/components/modal.js +57 -0
- package/{js → frontend/js}/components/queryEditor.js +33 -55
- package/frontend/js/components/queryHistoryDetail.js +263 -0
- package/frontend/js/components/queryHistoryPanel.js +228 -0
- package/{js → frontend/js}/components/queryResults.js +32 -46
- package/{js → frontend/js}/components/rowEditorPanel.js +73 -14
- package/{js → frontend/js}/components/sidebar.js +8 -3
- package/{js → frontend/js}/store.js +577 -21
- package/{js → frontend/js}/utils/format.js +23 -0
- package/{js → frontend/js}/views/data.js +136 -10
- package/{js → frontend/js}/views/editor.js +34 -10
- package/{js → frontend/js}/views/overview.js +15 -0
- package/{js → frontend/js}/views/structure.js +10 -12
- package/{styles → frontend/styles}/components.css +106 -0
- package/{styles → frontend/styles}/structure-graph.css +5 -10
- package/package.json +2 -2
- 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 +2 -0
- package/server/routes/export.js +4 -1
- package/server/routes/overview.js +12 -0
- package/server/routes/sql.js +163 -5
- package/server/server.js +8 -6
- package/server/services/sqlite/connectionManager.js +68 -33
- package/server/services/sqlite/dataBrowserService.js +4 -16
- package/server/services/sqlite/exportService.js +4 -16
- package/server/services/sqlite/overviewService.js +34 -0
- package/server/services/sqlite/sqlExecutor.js +83 -63
- package/server/services/sqlite/tableSort.js +63 -0
- package/server/services/storage/appStateStore.js +832 -20
- package/server/services/storage/queryHistoryUtils.js +169 -0
- 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 +1 -1
- /package/{js → frontend/js}/components/metricCard.js +0 -0
- /package/{js → frontend/js}/components/pageHeader.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}/views/connections.js +0 -0
- /package/{js → frontend/js}/views/landing.js +0 -0
- /package/{js → frontend/js}/views/settings.js +0 -0
- /package/{styles → frontend/styles}/base.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
package/server/routes/sql.js
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
2
|
const { route, successResponse } = require("../utils/errors");
|
|
3
3
|
|
|
4
|
-
function
|
|
4
|
+
function parseBooleanFlag(value) {
|
|
5
|
+
if (typeof value === "boolean") {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const normalized = String(value ?? "")
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase();
|
|
12
|
+
|
|
13
|
+
return ["1", "true", "yes", "on"].includes(normalized);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseListLimit(value, fallback = 30, max = 100) {
|
|
17
|
+
const numericValue = Number(value);
|
|
18
|
+
|
|
19
|
+
if (!Number.isFinite(numericValue) || numericValue < 1) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return Math.min(max, Math.round(numericValue));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseOffset(value) {
|
|
27
|
+
const numericValue = Number(value);
|
|
28
|
+
|
|
29
|
+
if (!Number.isFinite(numericValue) || numericValue < 0) {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return Math.round(numericValue);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getActiveDatabaseKey(connectionManager) {
|
|
37
|
+
return connectionManager.getActiveConnection()?.id ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
|
|
5
41
|
const router = express.Router();
|
|
6
42
|
|
|
7
43
|
router.post(
|
|
@@ -21,9 +57,32 @@ function createSqlRouter({ appStateStore, sqlExecutor }) {
|
|
|
21
57
|
router.get(
|
|
22
58
|
"/history",
|
|
23
59
|
route((req, res) => {
|
|
60
|
+
const databaseKey = getActiveDatabaseKey(connectionManager);
|
|
61
|
+
const tab = String(req.query.tab ?? "recent").trim().toLowerCase();
|
|
62
|
+
const options = {
|
|
63
|
+
databaseKey,
|
|
64
|
+
limit: parseListLimit(req.query.limit, 30, 100),
|
|
65
|
+
offset: parseOffset(req.query.offset),
|
|
66
|
+
search: String(req.query.search ?? ""),
|
|
67
|
+
queryType: String(req.query.queryType ?? "").trim() || null,
|
|
68
|
+
onlySaved: parseBooleanFlag(req.query.onlySaved),
|
|
69
|
+
onlyFavorites: parseBooleanFlag(req.query.onlyFavorites),
|
|
70
|
+
};
|
|
71
|
+
const result =
|
|
72
|
+
tab === "failed"
|
|
73
|
+
? appStateStore.getFailedQueries(options)
|
|
74
|
+
: appStateStore.getRecentQueries({
|
|
75
|
+
...options,
|
|
76
|
+
onlySaved: tab === "saved" ? true : options.onlySaved,
|
|
77
|
+
});
|
|
78
|
+
|
|
24
79
|
res.json(
|
|
25
80
|
successResponse({
|
|
26
|
-
data:
|
|
81
|
+
data: result,
|
|
82
|
+
metadata: {
|
|
83
|
+
databaseKey,
|
|
84
|
+
tab,
|
|
85
|
+
},
|
|
27
86
|
})
|
|
28
87
|
);
|
|
29
88
|
})
|
|
@@ -32,11 +91,110 @@ function createSqlRouter({ appStateStore, sqlExecutor }) {
|
|
|
32
91
|
router.delete(
|
|
33
92
|
"/history",
|
|
34
93
|
route((req, res) => {
|
|
35
|
-
|
|
94
|
+
const databaseKey = getActiveDatabaseKey(connectionManager);
|
|
95
|
+
const deletedCount = appStateStore.clearQueryHistoryForDatabase(databaseKey);
|
|
96
|
+
|
|
97
|
+
res.json(
|
|
98
|
+
successResponse({
|
|
99
|
+
message: deletedCount
|
|
100
|
+
? "Query history cleared for the active database."
|
|
101
|
+
: "No query history was found for the active database.",
|
|
102
|
+
data: {
|
|
103
|
+
deletedCount,
|
|
104
|
+
},
|
|
105
|
+
metadata: {
|
|
106
|
+
databaseKey,
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
router.get(
|
|
114
|
+
"/history/:historyId/runs",
|
|
115
|
+
route((req, res) => {
|
|
116
|
+
res.json(
|
|
117
|
+
successResponse({
|
|
118
|
+
data: appStateStore.getQueryRunsByHistoryId(
|
|
119
|
+
req.params.historyId,
|
|
120
|
+
parseListLimit(req.query.limit, 8, 50)
|
|
121
|
+
),
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
router.patch(
|
|
128
|
+
"/history/:historyId/favorite",
|
|
129
|
+
route((req, res) => {
|
|
130
|
+
const nextValue = parseBooleanFlag(req.body?.value);
|
|
131
|
+
res.json(
|
|
132
|
+
successResponse({
|
|
133
|
+
message: nextValue ? "Query favorited." : "Query removed from favorites.",
|
|
134
|
+
data: appStateStore.toggleFavorite(req.params.historyId, nextValue),
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
router.patch(
|
|
141
|
+
"/history/:historyId/saved",
|
|
142
|
+
route((req, res) => {
|
|
143
|
+
const nextValue = parseBooleanFlag(req.body?.value);
|
|
144
|
+
res.json(
|
|
145
|
+
successResponse({
|
|
146
|
+
message: nextValue ? "Query saved." : "Query removed from saved queries.",
|
|
147
|
+
data: appStateStore.toggleSaved(req.params.historyId, nextValue),
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
router.patch(
|
|
154
|
+
"/history/:historyId/title",
|
|
155
|
+
route((req, res) => {
|
|
156
|
+
res.json(
|
|
157
|
+
successResponse({
|
|
158
|
+
message: "Query title updated.",
|
|
159
|
+
data: appStateStore.renameQuery(req.params.historyId, req.body?.title),
|
|
160
|
+
})
|
|
161
|
+
);
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
router.patch(
|
|
166
|
+
"/history/:historyId/notes",
|
|
167
|
+
route((req, res) => {
|
|
168
|
+
res.json(
|
|
169
|
+
successResponse({
|
|
170
|
+
message: "Query notes updated.",
|
|
171
|
+
data: appStateStore.updateQueryNotes(req.params.historyId, req.body?.notes),
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
router.delete(
|
|
178
|
+
"/history/:historyId",
|
|
179
|
+
route((req, res) => {
|
|
180
|
+
appStateStore.deleteQueryHistoryItem(req.params.historyId);
|
|
181
|
+
res.json(
|
|
182
|
+
successResponse({
|
|
183
|
+
message: "Query history item deleted.",
|
|
184
|
+
data: {
|
|
185
|
+
id: Number(req.params.historyId),
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
router.get(
|
|
193
|
+
"/history/:historyId",
|
|
194
|
+
route((req, res) => {
|
|
36
195
|
res.json(
|
|
37
196
|
successResponse({
|
|
38
|
-
|
|
39
|
-
data: [],
|
|
197
|
+
data: appStateStore.getQueryHistoryItemById(req.params.historyId),
|
|
40
198
|
})
|
|
41
199
|
);
|
|
42
200
|
})
|
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,
|
|
@@ -72,7 +75,7 @@ app.use(
|
|
|
72
75
|
})
|
|
73
76
|
);
|
|
74
77
|
app.use("/api/db", createOverviewRouter({ overviewService }));
|
|
75
|
-
app.use("/api/sql", createSqlRouter({ appStateStore, sqlExecutor }));
|
|
78
|
+
app.use("/api/sql", createSqlRouter({ appStateStore, connectionManager, sqlExecutor }));
|
|
76
79
|
app.use("/api/structure", createStructureRouter({ structureService }));
|
|
77
80
|
app.use("/api/data", createDataRouter({ dataBrowserService }));
|
|
78
81
|
app.use("/api/settings", createSettingsRouter({ appStateStore }));
|
|
@@ -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,9 +105,8 @@ 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
112
|
function parsePortArgument(argv = process.argv.slice(2)) {
|
|
@@ -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() {
|
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
serializeRows,
|
|
7
7
|
} = require("../../utils/sqliteTypes");
|
|
8
8
|
const { getRawStructureEntries, getTableDetail } = require("./introspection");
|
|
9
|
+
const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
|
|
9
10
|
|
|
10
11
|
const DEFAULT_LIMIT = 50;
|
|
11
12
|
const MAX_LIMIT = 100;
|
|
@@ -70,9 +71,10 @@ class DataBrowserService {
|
|
|
70
71
|
const db = this.connectionManager.getActiveDatabase();
|
|
71
72
|
const tableDetail = getTableDetail(db, tableName);
|
|
72
73
|
const { limit, offset } = normalizePaginationOptions(options);
|
|
74
|
+
const sort = normalizeTableSort(tableDetail, options);
|
|
73
75
|
const selectExpression =
|
|
74
76
|
tableDetail.identityStrategy?.type === "rowid" ? "rowid AS __rowid__, *" : "*";
|
|
75
|
-
const orderClause =
|
|
77
|
+
const orderClause = buildTableOrderClause(tableDetail, sort);
|
|
76
78
|
const statement = db.prepare(
|
|
77
79
|
[
|
|
78
80
|
`SELECT ${selectExpression} FROM ${quoteIdentifier(tableName)}`,
|
|
@@ -114,6 +116,7 @@ class DataBrowserService {
|
|
|
114
116
|
rows,
|
|
115
117
|
identityStrategy: tableDetail.identityStrategy,
|
|
116
118
|
notSafelyUpdatable: tableDetail.notSafelyUpdatable,
|
|
119
|
+
sort,
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -238,21 +241,6 @@ class DataBrowserService {
|
|
|
238
241
|
`Table ${tableDetail.name} cannot be updated because it has no stable row identity.`
|
|
239
242
|
);
|
|
240
243
|
}
|
|
241
|
-
|
|
242
|
-
buildOrderClause(tableDetail) {
|
|
243
|
-
if (tableDetail.identityStrategy?.type === "rowid") {
|
|
244
|
-
return "rowid ASC";
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (tableDetail.identityStrategy?.type === "primaryKey") {
|
|
248
|
-
return tableDetail.identityStrategy.columns
|
|
249
|
-
.map((columnName) => `${quoteIdentifier(columnName)} ASC`)
|
|
250
|
-
.join(", ");
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return "";
|
|
254
|
-
}
|
|
255
|
-
|
|
256
244
|
getRowByIdentity(db, tableDetail, where) {
|
|
257
245
|
const selectExpression =
|
|
258
246
|
tableDetail.identityStrategy?.type === "rowid" ? "rowid AS __rowid__, *" : "*";
|
|
@@ -2,6 +2,7 @@ const { quoteIdentifier } = require("../../utils/identifier");
|
|
|
2
2
|
const { serializeRows } = require("../../utils/sqliteTypes");
|
|
3
3
|
const { rowsToCsv } = require("../../utils/csv");
|
|
4
4
|
const { getTableDetail } = require("./introspection");
|
|
5
|
+
const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
|
|
5
6
|
|
|
6
7
|
class ExportService {
|
|
7
8
|
constructor({ appStateStore, connectionManager, sqlExecutor }) {
|
|
@@ -32,10 +33,11 @@ class ExportService {
|
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
exportTable(tableName) {
|
|
36
|
+
exportTable(tableName, options = {}) {
|
|
36
37
|
const db = this.connectionManager.getActiveDatabase();
|
|
37
38
|
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
38
|
-
const
|
|
39
|
+
const sort = normalizeTableSort(tableDetail, options);
|
|
40
|
+
const orderClause = buildTableOrderClause(tableDetail, sort);
|
|
39
41
|
const statement = db.prepare(
|
|
40
42
|
[
|
|
41
43
|
`SELECT * FROM ${quoteIdentifier(tableName)}`,
|
|
@@ -58,20 +60,6 @@ class ExportService {
|
|
|
58
60
|
rowCount: rows.length,
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
|
-
|
|
62
|
-
buildOrderClause(tableDetail) {
|
|
63
|
-
if (tableDetail.identityStrategy?.type === "rowid") {
|
|
64
|
-
return "rowid ASC";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (tableDetail.identityStrategy?.type === "primaryKey") {
|
|
68
|
-
return tableDetail.identityStrategy.columns
|
|
69
|
-
.map((columnName) => `${quoteIdentifier(columnName)} ASC`)
|
|
70
|
-
.join(", ");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return "";
|
|
74
|
-
}
|
|
75
63
|
}
|
|
76
64
|
|
|
77
65
|
module.exports = {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execFile } = require("child_process");
|
|
4
|
+
const { promisify } = require("util");
|
|
5
|
+
const { AppError, DatabaseRequiredError } = require("../../utils/errors");
|
|
1
6
|
const { listSchema } = require("./introspection");
|
|
2
7
|
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
3
10
|
class OverviewService {
|
|
4
11
|
constructor({ connectionManager }) {
|
|
5
12
|
this.connectionManager = connectionManager;
|
|
@@ -102,6 +109,33 @@ class OverviewService {
|
|
|
102
109
|
readOnly: active?.readOnly ?? false,
|
|
103
110
|
};
|
|
104
111
|
}
|
|
112
|
+
|
|
113
|
+
async revealActiveDatabaseInFinder() {
|
|
114
|
+
const active = this.connectionManager.getActiveConnection();
|
|
115
|
+
const targetPath = active?.path;
|
|
116
|
+
|
|
117
|
+
if (!targetPath) {
|
|
118
|
+
throw new DatabaseRequiredError();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!fs.existsSync(targetPath)) {
|
|
122
|
+
throw new AppError("The active database file could not be found on disk.", 404, {
|
|
123
|
+
code: "DATABASE_FILE_NOT_FOUND",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (process.platform === "darwin") {
|
|
128
|
+
await execFileAsync("open", ["-R", targetPath]);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (process.platform === "win32") {
|
|
133
|
+
await execFileAsync("explorer.exe", ["/select,", targetPath]);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await execFileAsync("xdg-open", [path.dirname(targetPath)]);
|
|
138
|
+
}
|
|
105
139
|
}
|
|
106
140
|
|
|
107
141
|
module.exports = {
|