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.
Files changed (75) hide show
  1. package/README.md +5 -5
  2. package/changelog.md +14 -0
  3. package/{js → frontend/js}/api.js +99 -5
  4. package/{js → frontend/js}/app.js +162 -11
  5. package/{js → frontend/js}/components/connectionCard.js +8 -9
  6. package/frontend/js/components/connectionLogo.js +33 -0
  7. package/{js → frontend/js}/components/dataGrid.js +3 -3
  8. package/{js → frontend/js}/components/emptyState.js +25 -11
  9. package/{js → frontend/js}/components/modal.js +57 -0
  10. package/{js → frontend/js}/components/queryEditor.js +33 -55
  11. package/frontend/js/components/queryHistoryDetail.js +263 -0
  12. package/frontend/js/components/queryHistoryPanel.js +228 -0
  13. package/{js → frontend/js}/components/queryResults.js +32 -46
  14. package/{js → frontend/js}/components/rowEditorPanel.js +73 -14
  15. package/{js → frontend/js}/components/sidebar.js +8 -3
  16. package/{js → frontend/js}/store.js +577 -21
  17. package/{js → frontend/js}/utils/format.js +23 -0
  18. package/{js → frontend/js}/views/data.js +136 -10
  19. package/{js → frontend/js}/views/editor.js +34 -10
  20. package/{js → frontend/js}/views/overview.js +15 -0
  21. package/{js → frontend/js}/views/structure.js +10 -12
  22. package/{styles → frontend/styles}/components.css +106 -0
  23. package/{styles → frontend/styles}/structure-graph.css +5 -10
  24. package/package.json +2 -2
  25. package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
  26. package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
  27. package/server/data/db_logos/.gitkeep +0 -0
  28. package/server/routes/connections.js +2 -0
  29. package/server/routes/data.js +2 -0
  30. package/server/routes/export.js +4 -1
  31. package/server/routes/overview.js +12 -0
  32. package/server/routes/sql.js +163 -5
  33. package/server/server.js +8 -6
  34. package/server/services/sqlite/connectionManager.js +68 -33
  35. package/server/services/sqlite/dataBrowserService.js +4 -16
  36. package/server/services/sqlite/exportService.js +4 -16
  37. package/server/services/sqlite/overviewService.js +34 -0
  38. package/server/services/sqlite/sqlExecutor.js +83 -63
  39. package/server/services/sqlite/tableSort.js +63 -0
  40. package/server/services/storage/appStateStore.js +832 -20
  41. package/server/services/storage/queryHistoryUtils.js +169 -0
  42. package/server/utils/appPaths.js +42 -18
  43. package/{assets → frontend/assets}/images/logo.webp +0 -0
  44. package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
  45. package/{assets → frontend/assets}/images/logo_raw.png +0 -0
  46. package/{assets → frontend/assets}/images/logo_small.webp +0 -0
  47. package/{assets → frontend/assets}/mockups/connections.png +0 -0
  48. package/{assets → frontend/assets}/mockups/data.png +0 -0
  49. package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
  50. package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
  51. package/{assets → frontend/assets}/mockups/home.png +0 -0
  52. package/{assets → frontend/assets}/mockups/overview.png +0 -0
  53. package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
  54. package/{assets → frontend/assets}/mockups/structure.png +0 -0
  55. package/{index.html → frontend/index.html} +0 -0
  56. package/{js → frontend/js}/components/actionBar.js +0 -0
  57. package/{js → frontend/js}/components/appShell.js +0 -0
  58. package/{js → frontend/js}/components/badges.js +0 -0
  59. package/{js → frontend/js}/components/bottomTabs.js +1 -1
  60. /package/{js → frontend/js}/components/metricCard.js +0 -0
  61. /package/{js → frontend/js}/components/pageHeader.js +0 -0
  62. /package/{js → frontend/js}/components/statusBar.js +0 -0
  63. /package/{js → frontend/js}/components/structureGraph.js +0 -0
  64. /package/{js → frontend/js}/components/toast.js +0 -0
  65. /package/{js → frontend/js}/components/topNav.js +0 -0
  66. /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
  67. /package/{js → frontend/js}/router.js +0 -0
  68. /package/{js → frontend/js}/views/connections.js +0 -0
  69. /package/{js → frontend/js}/views/landing.js +0 -0
  70. /package/{js → frontend/js}/views/settings.js +0 -0
  71. /package/{styles → frontend/styles}/base.css +0 -0
  72. /package/{styles → frontend/styles}/layout.css +0 -0
  73. /package/{styles → frontend/styles}/tokens.css +0 -0
  74. /package/{styles → frontend/styles}/views.css +0 -0
  75. /package/{data → server/data}/.gitkeep +0 -0
@@ -1,7 +1,43 @@
1
1
  const express = require("express");
2
2
  const { route, successResponse } = require("../utils/errors");
3
3
 
4
- function createSqlRouter({ appStateStore, sqlExecutor }) {
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: appStateStore.getSqlHistory(),
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
- appStateStore.clearSqlHistory();
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
- message: "SQL history cleared.",
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(path.resolve(__dirname, "..", "index.html"));
89
+ res.sendFile(FRONTEND_ENTRYPOINT);
87
90
  });
88
91
 
89
92
  app.get("/index.html", (req, res) => {
90
- res.sendFile(path.resolve(__dirname, "..", "index.html"));
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("/js", express.static(path.resolve(__dirname, "..", "js")));
106
- app.use("/styles", express.static(path.resolve(__dirname, "..", "styles")));
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 connection = this.buildConnectionRecord(resolvedPath, {
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 (this.current?.id === id) {
223
- return this.openConnection({
224
- filePath: resolvedPath,
225
- label: normalizedLabel,
226
- id,
227
- makeActive: true,
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
- const db = this.openRawDatabase(resolvedPath, {
233
- fileMustExist: true,
234
- readOnly: normalizedReadOnly,
235
- });
263
+ db.close();
236
264
 
237
- db.close();
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
- const metadata = getFileMetadata(resolvedPath);
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
- this.appStateStore.updateRecentConnection(id, () => nextConnection);
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
- return {
252
- ...nextConnection,
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 = this.buildOrderClause(tableDetail);
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 orderClause = this.buildOrderClause(tableDetail);
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 = {