sqlite-hub 0.1.3 → 0.3.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/.github/funding.yml +2 -0
- package/LICENSE +21 -0
- package/README.md +54 -26
- package/assets/mockups/graph_visualize.png +0 -0
- package/bin/sqlite-hub.js +85 -78
- package/changelog.md +11 -1
- package/index.html +3 -0
- package/js/api.js +14 -0
- package/js/app.js +42 -1
- package/js/components/queryEditor.js +8 -0
- package/js/components/structureGraph.js +925 -0
- package/js/lib/cytoscapeRuntime.js +16 -0
- package/js/store.js +55 -0
- package/js/views/connections.js +18 -3
- package/js/views/data.js +13 -0
- package/js/views/editor.js +1 -0
- package/js/views/structure.js +239 -117
- package/package.json +4 -1
- package/publish_brew.sh +22 -0
- package/server/routes/connections.js +15 -1
- package/server/routes/export.js +26 -6
- package/server/server.js +24 -2
- package/server/services/sqlite/backupService.js +112 -0
- package/server/services/sqlite/connectionManager.js +15 -0
- package/server/services/sqlite/exportService.js +46 -1
- package/server/services/sqlite/structureService.js +26 -0
- package/server/services/storage/appStateStore.js +135 -8
- package/server/utils/appPaths.js +131 -0
- package/styles/structure-graph.css +414 -0
package/publish_brew.sh
CHANGED
|
@@ -304,6 +304,27 @@ commit_and_push_tap() {
|
|
|
304
304
|
run git -C "$TAP_DIR" push origin HEAD
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
refresh_local_brew_tap_checkout() {
|
|
308
|
+
local brew_repo
|
|
309
|
+
local installed_tap_path
|
|
310
|
+
|
|
311
|
+
if ! command -v brew >/dev/null 2>&1; then
|
|
312
|
+
info "brew not found, skipping local tap refresh"
|
|
313
|
+
return
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
brew_repo="$(brew --repository)"
|
|
317
|
+
installed_tap_path="${brew_repo}/Library/Taps/${TAP_OWNER}/${TAP_REPO_NAME}"
|
|
318
|
+
|
|
319
|
+
if [[ ! -d "$installed_tap_path/.git" ]]; then
|
|
320
|
+
info "No installed Homebrew tap checkout at ${installed_tap_path}, skipping local refresh"
|
|
321
|
+
return
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
info "Refreshing installed Homebrew tap checkout at ${installed_tap_path}"
|
|
325
|
+
run git -C "$installed_tap_path" pull --ff-only
|
|
326
|
+
}
|
|
327
|
+
|
|
307
328
|
run_brew_audit() {
|
|
308
329
|
if [[ "$SKIP_AUDIT" == "1" ]]; then
|
|
309
330
|
info "Skipping brew audit"
|
|
@@ -439,6 +460,7 @@ ensure_tap_repo
|
|
|
439
460
|
write_formula
|
|
440
461
|
run_brew_audit
|
|
441
462
|
commit_and_push_tap
|
|
463
|
+
refresh_local_brew_tap_checkout
|
|
442
464
|
|
|
443
465
|
info "Published ${FORMULA_NAME} ${VERSION}"
|
|
444
466
|
info "Install with: brew install ${BREW_TAP_NAME}/${FORMULA_NAME}"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
2
|
const { route, successResponse } = require("../utils/errors");
|
|
3
3
|
|
|
4
|
-
function createConnectionsRouter({ connectionManager, importService }) {
|
|
4
|
+
function createConnectionsRouter({ connectionManager, importService, backupService }) {
|
|
5
5
|
const router = express.Router();
|
|
6
6
|
|
|
7
7
|
router.post(
|
|
@@ -68,6 +68,20 @@ function createConnectionsRouter({ connectionManager, importService }) {
|
|
|
68
68
|
})
|
|
69
69
|
);
|
|
70
70
|
|
|
71
|
+
router.post(
|
|
72
|
+
"/backup-active",
|
|
73
|
+
route((req, res) => {
|
|
74
|
+
const backup = backupService.createActiveBackup();
|
|
75
|
+
|
|
76
|
+
res.json(
|
|
77
|
+
successResponse({
|
|
78
|
+
message: `Backup created: ${backup.fileName}`,
|
|
79
|
+
data: backup,
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
71
85
|
router.get(
|
|
72
86
|
"/recent",
|
|
73
87
|
route((req, res) => {
|
package/server/routes/export.js
CHANGED
|
@@ -4,16 +4,36 @@ const { route } = require("../utils/errors");
|
|
|
4
4
|
function createExportRouter({ exportService }) {
|
|
5
5
|
const router = express.Router();
|
|
6
6
|
|
|
7
|
+
function sendCsv(res, result) {
|
|
8
|
+
res.setHeader("Content-Type", "text/csv; charset=utf-8");
|
|
9
|
+
res.setHeader(
|
|
10
|
+
"Content-Disposition",
|
|
11
|
+
`attachment; filename="${result.filename}"`
|
|
12
|
+
);
|
|
13
|
+
res.send(result.csv);
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
router.post(
|
|
8
17
|
"/query.csv",
|
|
9
18
|
route((req, res) => {
|
|
10
19
|
const result = exportService.exportQuery(req.body.sql);
|
|
11
|
-
res
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
sendCsv(res, result);
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
router.post(
|
|
25
|
+
"/table.csv",
|
|
26
|
+
route((req, res) => {
|
|
27
|
+
const result = exportService.exportTable(req.body?.tableName);
|
|
28
|
+
sendCsv(res, result);
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
router.get(
|
|
33
|
+
"/table/:tableName.csv",
|
|
34
|
+
route((req, res) => {
|
|
35
|
+
const result = exportService.exportTable(req.params.tableName);
|
|
36
|
+
sendCsv(res, result);
|
|
17
37
|
})
|
|
18
38
|
);
|
|
19
39
|
|
package/server/server.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
2
|
const path = require("node:path");
|
|
3
3
|
const { errorMiddleware } = require("./utils/errors");
|
|
4
|
+
const { resolveAppStatePaths } = require("./utils/appPaths");
|
|
4
5
|
const { AppStateStore } = require("./services/storage/appStateStore");
|
|
5
6
|
const { ConnectionManager } = require("./services/sqlite/connectionManager");
|
|
6
7
|
const { OverviewService } = require("./services/sqlite/overviewService");
|
|
7
8
|
const { SqlExecutor } = require("./services/sqlite/sqlExecutor");
|
|
8
9
|
const { ImportService } = require("./services/sqlite/importService");
|
|
10
|
+
const { BackupService } = require("./services/sqlite/backupService");
|
|
9
11
|
const { ExportService } = require("./services/sqlite/exportService");
|
|
10
12
|
const { StructureService } = require("./services/sqlite/structureService");
|
|
11
13
|
const { DataBrowserService } = require("./services/sqlite/dataBrowserService");
|
|
@@ -17,19 +19,26 @@ const { createDataRouter } = require("./routes/data");
|
|
|
17
19
|
const { createSettingsRouter } = require("./routes/settings");
|
|
18
20
|
const { createExportRouter } = require("./routes/export");
|
|
19
21
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
23
|
+
const {
|
|
24
|
+
appStateDbPath: APP_STATE_DB_PATH,
|
|
25
|
+
legacyStatePath: LEGACY_STATE_PATH,
|
|
26
|
+
legacyDatabasePaths: LEGACY_DATABASE_PATHS,
|
|
27
|
+
} = resolveAppStatePaths(PACKAGE_ROOT);
|
|
22
28
|
const DEFAULT_PORT = 4173;
|
|
23
29
|
|
|
24
30
|
const appStateStore = new AppStateStore(APP_STATE_DB_PATH, {
|
|
25
31
|
legacyFilePath: LEGACY_STATE_PATH,
|
|
32
|
+
legacyDatabasePaths: LEGACY_DATABASE_PATHS,
|
|
26
33
|
});
|
|
27
34
|
const connectionManager = new ConnectionManager({ appStateStore });
|
|
28
35
|
const overviewService = new OverviewService({ connectionManager });
|
|
29
36
|
const sqlExecutor = new SqlExecutor({ connectionManager, appStateStore });
|
|
30
37
|
const importService = new ImportService({ connectionManager });
|
|
38
|
+
const backupService = new BackupService({ connectionManager });
|
|
31
39
|
const exportService = new ExportService({
|
|
32
40
|
appStateStore,
|
|
41
|
+
connectionManager,
|
|
33
42
|
sqlExecutor,
|
|
34
43
|
});
|
|
35
44
|
const structureService = new StructureService({ connectionManager, appStateStore });
|
|
@@ -59,6 +68,7 @@ app.use(
|
|
|
59
68
|
createConnectionsRouter({
|
|
60
69
|
connectionManager,
|
|
61
70
|
importService,
|
|
71
|
+
backupService,
|
|
62
72
|
})
|
|
63
73
|
);
|
|
64
74
|
app.use("/api/db", createOverviewRouter({ overviewService }));
|
|
@@ -80,6 +90,18 @@ app.get("/index.html", (req, res) => {
|
|
|
80
90
|
res.sendFile(path.resolve(__dirname, "..", "index.html"));
|
|
81
91
|
});
|
|
82
92
|
|
|
93
|
+
app.use(
|
|
94
|
+
"/vendor/cytoscape",
|
|
95
|
+
express.static(path.resolve(__dirname, "..", "node_modules", "cytoscape"))
|
|
96
|
+
);
|
|
97
|
+
app.use(
|
|
98
|
+
"/vendor/cytoscape-elk",
|
|
99
|
+
express.static(path.resolve(__dirname, "..", "node_modules", "cytoscape-elk"))
|
|
100
|
+
);
|
|
101
|
+
app.use(
|
|
102
|
+
"/vendor/elkjs",
|
|
103
|
+
express.static(path.resolve(__dirname, "..", "node_modules", "elkjs"))
|
|
104
|
+
);
|
|
83
105
|
app.use("/js", express.static(path.resolve(__dirname, "..", "js")));
|
|
84
106
|
app.use("/styles", express.static(path.resolve(__dirname, "..", "styles")));
|
|
85
107
|
app.use("/assets", express.static(path.resolve(__dirname, "..", "assets")));
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { DatabaseRequiredError } = require("../../utils/errors");
|
|
4
|
+
const {
|
|
5
|
+
SQLITE_EXTENSIONS,
|
|
6
|
+
ensureParentDirectory,
|
|
7
|
+
getFileMetadata,
|
|
8
|
+
validateSqlitePath,
|
|
9
|
+
} = require("../../utils/fileValidation");
|
|
10
|
+
|
|
11
|
+
function padTimestampPart(value) {
|
|
12
|
+
return String(value).padStart(2, "0");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatBackupTimestamp(date) {
|
|
16
|
+
return [
|
|
17
|
+
date.getFullYear(),
|
|
18
|
+
padTimestampPart(date.getMonth() + 1),
|
|
19
|
+
padTimestampPart(date.getDate()),
|
|
20
|
+
].join("-")
|
|
21
|
+
+ "_"
|
|
22
|
+
+ [
|
|
23
|
+
padTimestampPart(date.getHours()),
|
|
24
|
+
padTimestampPart(date.getMinutes()),
|
|
25
|
+
padTimestampPart(date.getSeconds()),
|
|
26
|
+
].join("-");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sanitizeBackupBaseName(value) {
|
|
30
|
+
const normalized = String(value ?? "").trim().replace(/[<>:"/\\|?*\u0000-\u001F]+/g, "_");
|
|
31
|
+
return normalized || "database";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeBackupLabelBase(value, fallback) {
|
|
35
|
+
const normalized = String(value ?? "").trim() || String(fallback ?? "").trim() || "database";
|
|
36
|
+
const extension = path.extname(normalized).toLowerCase();
|
|
37
|
+
|
|
38
|
+
if (SQLITE_EXTENSIONS.has(extension)) {
|
|
39
|
+
return normalized.slice(0, -extension.length) || "database";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class BackupService {
|
|
46
|
+
constructor({ connectionManager }) {
|
|
47
|
+
this.connectionManager = connectionManager;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
createActiveBackup() {
|
|
51
|
+
const activeConnection = this.connectionManager.getActiveConnection();
|
|
52
|
+
|
|
53
|
+
if (!activeConnection) {
|
|
54
|
+
throw new DatabaseRequiredError("No active SQLite database selected for backup.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const sourcePath = validateSqlitePath(activeConnection.path, { mustExist: true });
|
|
58
|
+
const { backupPath, connectionLabel } = this.buildBackupPath(sourcePath, activeConnection.label);
|
|
59
|
+
|
|
60
|
+
ensureParentDirectory(backupPath);
|
|
61
|
+
fs.copyFileSync(sourcePath, backupPath, fs.constants.COPYFILE_EXCL);
|
|
62
|
+
|
|
63
|
+
const backupConnection = this.connectionManager.rememberConnection({
|
|
64
|
+
filePath: backupPath,
|
|
65
|
+
label: connectionLabel,
|
|
66
|
+
makeActive: false,
|
|
67
|
+
});
|
|
68
|
+
const metadata = getFileMetadata(backupPath);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
sourcePath,
|
|
72
|
+
backupPath,
|
|
73
|
+
directory: path.dirname(backupPath),
|
|
74
|
+
fileName: path.basename(backupPath),
|
|
75
|
+
createdAt: new Date().toISOString(),
|
|
76
|
+
sizeBytes: metadata.sizeBytes,
|
|
77
|
+
connection: backupConnection,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
buildBackupPath(sourcePath, sourceLabel) {
|
|
82
|
+
const parsedSource = path.parse(sourcePath);
|
|
83
|
+
const backupDirectory = path.join(parsedSource.dir, "backups");
|
|
84
|
+
const extension = parsedSource.ext || ".sqlite";
|
|
85
|
+
const displayBaseName = normalizeBackupLabelBase(sourceLabel, parsedSource.name);
|
|
86
|
+
const fileBaseName = sanitizeBackupBaseName(displayBaseName);
|
|
87
|
+
const timestamp = formatBackupTimestamp(new Date());
|
|
88
|
+
let attempt = 1;
|
|
89
|
+
|
|
90
|
+
while (true) {
|
|
91
|
+
const suffix = attempt === 1 ? "" : `_${String(attempt).padStart(2, "0")}`;
|
|
92
|
+
const fileStem = `${fileBaseName}_${timestamp}${suffix}`;
|
|
93
|
+
const candidate = path.join(
|
|
94
|
+
backupDirectory,
|
|
95
|
+
`${fileStem}${extension}`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!fs.existsSync(candidate)) {
|
|
99
|
+
return {
|
|
100
|
+
backupPath: candidate,
|
|
101
|
+
connectionLabel: `${displayBaseName}_${timestamp}${suffix}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
attempt += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
BackupService,
|
|
112
|
+
};
|
|
@@ -121,6 +121,21 @@ class ConnectionManager {
|
|
|
121
121
|
return this.getActiveConnection();
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
rememberConnection({ filePath, label, readOnly = false, makeActive = false }) {
|
|
125
|
+
const resolvedPath = validateSqlitePath(filePath, { mustExist: true });
|
|
126
|
+
const connection = this.buildConnectionRecord(resolvedPath, {
|
|
127
|
+
label,
|
|
128
|
+
readOnly,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
this.appStateStore.upsertRecentConnection(connection, { makeActive });
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...connection,
|
|
135
|
+
isActive: this.current?.id === connection.id,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
createConnection({ filePath, label }) {
|
|
125
140
|
const resolvedPath = resolveUserPath(filePath);
|
|
126
141
|
ensureFileDoesNotExist(resolvedPath, "SQLite database");
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
const { quoteIdentifier } = require("../../utils/identifier");
|
|
2
|
+
const { serializeRows } = require("../../utils/sqliteTypes");
|
|
1
3
|
const { rowsToCsv } = require("../../utils/csv");
|
|
4
|
+
const { getTableDetail } = require("./introspection");
|
|
2
5
|
|
|
3
6
|
class ExportService {
|
|
4
|
-
constructor({ appStateStore, sqlExecutor }) {
|
|
7
|
+
constructor({ appStateStore, connectionManager, sqlExecutor }) {
|
|
5
8
|
this.appStateStore = appStateStore;
|
|
9
|
+
this.connectionManager = connectionManager;
|
|
6
10
|
this.sqlExecutor = sqlExecutor;
|
|
7
11
|
}
|
|
8
12
|
|
|
@@ -27,6 +31,47 @@ class ExportService {
|
|
|
27
31
|
rowCount: result.rows.length,
|
|
28
32
|
};
|
|
29
33
|
}
|
|
34
|
+
|
|
35
|
+
exportTable(tableName) {
|
|
36
|
+
const db = this.connectionManager.getActiveDatabase();
|
|
37
|
+
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
38
|
+
const orderClause = this.buildOrderClause(tableDetail);
|
|
39
|
+
const statement = db.prepare(
|
|
40
|
+
[
|
|
41
|
+
`SELECT * FROM ${quoteIdentifier(tableName)}`,
|
|
42
|
+
orderClause ? `ORDER BY ${orderClause}` : "",
|
|
43
|
+
]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.join(" ")
|
|
46
|
+
);
|
|
47
|
+
const rows = serializeRows(statement.all());
|
|
48
|
+
const columns = statement.columns().map((column) => column.name);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
filename: `${tableName}.csv`,
|
|
52
|
+
csv: rowsToCsv({
|
|
53
|
+
columns,
|
|
54
|
+
rows,
|
|
55
|
+
delimiter: this.getDelimiter(),
|
|
56
|
+
}),
|
|
57
|
+
columns,
|
|
58
|
+
rowCount: rows.length,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
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
|
+
}
|
|
30
75
|
}
|
|
31
76
|
|
|
32
77
|
module.exports = {
|
|
@@ -11,6 +11,18 @@ class StructureService {
|
|
|
11
11
|
getStructureOverview() {
|
|
12
12
|
const db = this.connectionManager.getActiveDatabase();
|
|
13
13
|
const entries = getRawStructureEntries(db);
|
|
14
|
+
const tables = entries
|
|
15
|
+
.filter((entry) => entry.type === "table")
|
|
16
|
+
.map((entry) => getTableDetail(db, entry.name, { includeRowCount: false }));
|
|
17
|
+
const relationshipCount = tables.reduce(
|
|
18
|
+
(count, table) =>
|
|
19
|
+
count +
|
|
20
|
+
table.foreignKeys.reduce(
|
|
21
|
+
(tableCount, foreignKey) => tableCount + foreignKey.mappings.length,
|
|
22
|
+
0
|
|
23
|
+
),
|
|
24
|
+
0
|
|
25
|
+
);
|
|
14
26
|
|
|
15
27
|
return {
|
|
16
28
|
entries,
|
|
@@ -20,6 +32,20 @@ class StructureService {
|
|
|
20
32
|
indexes: entries.filter((entry) => entry.type === "index"),
|
|
21
33
|
triggers: entries.filter((entry) => entry.type === "trigger"),
|
|
22
34
|
},
|
|
35
|
+
graph: {
|
|
36
|
+
tables: tables.map((table) => ({
|
|
37
|
+
type: table.type,
|
|
38
|
+
name: table.name,
|
|
39
|
+
ddl: table.ddl,
|
|
40
|
+
withoutRowId: table.withoutRowId,
|
|
41
|
+
strict: table.strict,
|
|
42
|
+
columns: table.columns,
|
|
43
|
+
foreignKeys: table.foreignKeys,
|
|
44
|
+
identityStrategy: table.identityStrategy,
|
|
45
|
+
notSafelyUpdatable: table.notSafelyUpdatable,
|
|
46
|
+
})),
|
|
47
|
+
relationshipCount,
|
|
48
|
+
},
|
|
23
49
|
};
|
|
24
50
|
}
|
|
25
51
|
|
|
@@ -20,6 +20,9 @@ class AppStateStore {
|
|
|
20
20
|
constructor(filePath, options = {}) {
|
|
21
21
|
this.filePath = filePath;
|
|
22
22
|
this.legacyFilePath = options.legacyFilePath ?? null;
|
|
23
|
+
this.legacyDatabasePaths = Array.isArray(options.legacyDatabasePaths)
|
|
24
|
+
? options.legacyDatabasePaths
|
|
25
|
+
: [];
|
|
23
26
|
this.isFreshDatabase = !fs.existsSync(filePath);
|
|
24
27
|
|
|
25
28
|
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
@@ -29,8 +32,10 @@ class AppStateStore {
|
|
|
29
32
|
this.ensureSchema();
|
|
30
33
|
this.seedDefaultSettings();
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const importedLegacyDatabase = this.importFirstLegacyDatabase();
|
|
36
|
+
|
|
37
|
+
if (!importedLegacyDatabase && this.shouldImportLegacyState()) {
|
|
38
|
+
this.tryImportLegacyState();
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -121,8 +126,90 @@ class AppStateStore {
|
|
|
121
126
|
};
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
getExistingLegacyDatabasePaths() {
|
|
130
|
+
return this.legacyDatabasePaths
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.map((legacyPath) => path.resolve(legacyPath))
|
|
133
|
+
.filter(
|
|
134
|
+
(legacyPath, index, legacyPaths) =>
|
|
135
|
+
legacyPath !== path.resolve(this.filePath) &&
|
|
136
|
+
legacyPaths.indexOf(legacyPath) === index &&
|
|
137
|
+
fs.existsSync(legacyPath)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
readLegacyDatabase(legacyDatabasePath) {
|
|
142
|
+
const legacyDb = new Database(legacyDatabasePath, {
|
|
143
|
+
readonly: true,
|
|
144
|
+
fileMustExist: true,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
legacyDb.pragma("query_only = ON");
|
|
149
|
+
|
|
150
|
+
const tables = new Set(
|
|
151
|
+
legacyDb
|
|
152
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
153
|
+
.all()
|
|
154
|
+
.map((row) => row.name)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
settings: tables.has("settings")
|
|
159
|
+
? Object.fromEntries(
|
|
160
|
+
legacyDb
|
|
161
|
+
.prepare("SELECT key, value FROM settings")
|
|
162
|
+
.all()
|
|
163
|
+
.map((row) => [row.key, row.value])
|
|
164
|
+
)
|
|
165
|
+
: {},
|
|
166
|
+
recentConnections: tables.has("recent_connections")
|
|
167
|
+
? legacyDb
|
|
168
|
+
.prepare(`
|
|
169
|
+
SELECT
|
|
170
|
+
id,
|
|
171
|
+
label,
|
|
172
|
+
path,
|
|
173
|
+
lastOpenedAt,
|
|
174
|
+
lastModifiedAt,
|
|
175
|
+
sizeBytes,
|
|
176
|
+
readOnly
|
|
177
|
+
FROM recent_connections
|
|
178
|
+
ORDER BY lastOpenedAt DESC, id ASC
|
|
179
|
+
`)
|
|
180
|
+
.all()
|
|
181
|
+
: [],
|
|
182
|
+
sqlHistory: tables.has("sql_history")
|
|
183
|
+
? legacyDb
|
|
184
|
+
.prepare(`
|
|
185
|
+
SELECT
|
|
186
|
+
id,
|
|
187
|
+
connectionId,
|
|
188
|
+
connectionLabel,
|
|
189
|
+
sql,
|
|
190
|
+
statementCount,
|
|
191
|
+
resultKind,
|
|
192
|
+
affectedRowCount,
|
|
193
|
+
rowCount,
|
|
194
|
+
timingMs,
|
|
195
|
+
executedAt
|
|
196
|
+
FROM sql_history
|
|
197
|
+
ORDER BY executedAt DESC, id ASC
|
|
198
|
+
`)
|
|
199
|
+
.all()
|
|
200
|
+
: [],
|
|
201
|
+
activeConnectionId: tables.has("app_meta")
|
|
202
|
+
? legacyDb
|
|
203
|
+
.prepare("SELECT value FROM app_meta WHERE key = ?")
|
|
204
|
+
.get("activeConnectionId")?.value ?? null
|
|
205
|
+
: null,
|
|
206
|
+
};
|
|
207
|
+
} finally {
|
|
208
|
+
legacyDb.close();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
importStateSnapshot(legacyState, sourcePath) {
|
|
126
213
|
const insertSetting = this.db.prepare(`
|
|
127
214
|
INSERT INTO settings (key, value)
|
|
128
215
|
VALUES (?, ?)
|
|
@@ -175,7 +262,10 @@ class AppStateStore {
|
|
|
175
262
|
|
|
176
263
|
this.db.transaction(() => {
|
|
177
264
|
for (const [key, value] of Object.entries(legacyState.settings ?? {})) {
|
|
178
|
-
|
|
265
|
+
const normalizedValue =
|
|
266
|
+
typeof value === "string" ? this.parseStoredValue(value) : value;
|
|
267
|
+
|
|
268
|
+
insertSetting.run(key, JSON.stringify(normalizedValue));
|
|
179
269
|
}
|
|
180
270
|
|
|
181
271
|
for (const connection of legacyState.recentConnections ?? []) {
|
|
@@ -208,11 +298,43 @@ class AppStateStore {
|
|
|
208
298
|
this.setMetaValue("activeConnectionId", legacyState.activeConnectionId ?? null);
|
|
209
299
|
this.setMetaValue(
|
|
210
300
|
"legacyImportSource",
|
|
211
|
-
path.relative(path.dirname(this.filePath),
|
|
301
|
+
path.relative(path.dirname(this.filePath), sourcePath)
|
|
212
302
|
);
|
|
213
303
|
})();
|
|
214
304
|
}
|
|
215
305
|
|
|
306
|
+
importFirstLegacyDatabase() {
|
|
307
|
+
if (!this.isFreshDatabase) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const legacyDatabasePath of this.getExistingLegacyDatabasePaths()) {
|
|
312
|
+
try {
|
|
313
|
+
this.importStateSnapshot(
|
|
314
|
+
this.readLegacyDatabase(legacyDatabasePath),
|
|
315
|
+
legacyDatabasePath
|
|
316
|
+
);
|
|
317
|
+
return true;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.warn(
|
|
320
|
+
`Could not import legacy app state database from ${legacyDatabasePath}: ${error.message}`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
tryImportLegacyState() {
|
|
329
|
+
try {
|
|
330
|
+
this.importStateSnapshot(this.readLegacyState(), this.legacyFilePath);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.warn(
|
|
333
|
+
`Could not import legacy app state from ${this.legacyFilePath}: ${error.message}`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
216
338
|
parseStoredValue(value) {
|
|
217
339
|
try {
|
|
218
340
|
return JSON.parse(value);
|
|
@@ -322,7 +444,9 @@ class AppStateStore {
|
|
|
322
444
|
}));
|
|
323
445
|
}
|
|
324
446
|
|
|
325
|
-
upsertRecentConnection(connection) {
|
|
447
|
+
upsertRecentConnection(connection, options = {}) {
|
|
448
|
+
const makeActive = options.makeActive !== false;
|
|
449
|
+
|
|
326
450
|
this.db.transaction(() => {
|
|
327
451
|
this.db
|
|
328
452
|
.prepare(`
|
|
@@ -354,7 +478,10 @@ class AppStateStore {
|
|
|
354
478
|
connection.readOnly ? 1 : 0
|
|
355
479
|
);
|
|
356
480
|
|
|
357
|
-
|
|
481
|
+
if (makeActive) {
|
|
482
|
+
this.setMetaValue("activeConnectionId", connection.id);
|
|
483
|
+
}
|
|
484
|
+
|
|
358
485
|
this.trimRecentConnections();
|
|
359
486
|
})();
|
|
360
487
|
|