sqlite-hub 0.1.3 → 0.2.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 CHANGED
@@ -44,3 +44,5 @@ After linking or installing globally, the binary can also be called directly:
44
44
  ```bash
45
45
  sqlite-hub --port:1203
46
46
  ```
47
+
48
+ App state such as recent connections, SQL history, and local settings is stored in the user profile instead of the install directory. On macOS this lives under `~/Library/Application Support/sqlite-hub/`, so Homebrew upgrades keep the internal state across versions.
package/bin/sqlite-hub.js CHANGED
@@ -1,116 +1,123 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn } = require("node:child_process");
3
+ const { spawn } = require('node:child_process');
4
4
 
5
5
  const DEFAULT_PORT = 4173;
6
6
 
7
7
  function printHelp() {
8
- console.log(`SQLite Hub CLI
8
+ console.log(`SQLite Hub CLI
9
9
 
10
10
  Usage:
11
11
  sqlite-hub [--port:4173]
12
12
 
13
13
  Options:
14
- --port:PORT Start the server on a custom port.
15
14
  --help Show this help text.
15
+ --port:PORT Start the server on a custom port.
16
+ --version Show the version number.
16
17
  `);
17
18
  }
18
19
 
19
20
  function parsePort(rawValue) {
20
- const port = Number(rawValue);
21
+ const port = Number(rawValue);
21
22
 
22
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
23
- throw new Error(`Invalid port: ${rawValue}`);
24
- }
23
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
24
+ throw new Error(`Invalid port: ${rawValue}`);
25
+ }
25
26
 
26
- return port;
27
+ return port;
27
28
  }
28
29
 
29
30
  function parseCliArguments(argv) {
30
- let port;
31
-
32
- for (let index = 0; index < argv.length; index += 1) {
33
- const argument = argv[index];
34
-
35
- if (argument === "--help" || argument === "-h") {
36
- return { help: true };
37
- }
38
-
39
- if (argument.startsWith("--port:")) {
40
- port = parsePort(argument.slice("--port:".length));
41
- continue;
31
+ let port;
32
+
33
+ for (let index = 0; index < argv.length; index += 1) {
34
+ const argument = argv[index];
35
+
36
+ if (argument === '--help' || argument === '-h') {
37
+ return { help: true };
38
+ }
39
+
40
+ if (argument.startsWith('--port:')) {
41
+ port = parsePort(argument.slice('--port:'.length));
42
+ continue;
43
+ }
44
+
45
+ if (argument.startsWith('--port=')) {
46
+ port = parsePort(argument.slice('--port='.length));
47
+ continue;
48
+ }
49
+
50
+ if (argument === '--port') {
51
+ port = parsePort(argv[index + 1]);
52
+ index += 1;
53
+ continue;
54
+ }
55
+
56
+ if (argument === '--version' || argument === '-v') {
57
+ const { version } = require('../package.json');
58
+ console.log(`SQLite Hub CLI version ${version}`);
59
+ process.exit(0);
60
+ }
61
+
62
+ throw new Error(`Unknown argument: ${argument}`);
42
63
  }
43
64
 
44
- if (argument.startsWith("--port=")) {
45
- port = parsePort(argument.slice("--port=".length));
46
- continue;
47
- }
48
-
49
- if (argument === "--port") {
50
- port = parsePort(argv[index + 1]);
51
- index += 1;
52
- continue;
53
- }
54
-
55
- throw new Error(`Unknown argument: ${argument}`);
56
- }
57
-
58
- return { help: false, port };
65
+ return { help: false, port };
59
66
  }
60
67
 
61
68
  function openInDefaultBrowser(url) {
62
- const openers = {
63
- darwin: {
64
- command: "open",
65
- args: [url],
66
- },
67
- win32: {
68
- command: "cmd",
69
- args: ["/c", "start", "", url],
70
- options: { windowsHide: true },
71
- },
72
- default: {
73
- command: "xdg-open",
74
- args: [url],
75
- },
76
- };
77
-
78
- const opener = openers[process.platform] || openers.default;
79
- const child = spawn(opener.command, opener.args, {
80
- detached: true,
81
- stdio: "ignore",
82
- ...opener.options,
83
- });
84
-
85
- child.on("error", (error) => {
86
- console.warn(`Could not open the browser automatically: ${error.message}`);
87
- });
88
-
89
- child.unref();
69
+ const openers = {
70
+ darwin: {
71
+ command: 'open',
72
+ args: [url],
73
+ },
74
+ win32: {
75
+ command: 'cmd',
76
+ args: ['/c', 'start', '', url],
77
+ options: { windowsHide: true },
78
+ },
79
+ default: {
80
+ command: 'xdg-open',
81
+ args: [url],
82
+ },
83
+ };
84
+
85
+ const opener = openers[process.platform] || openers.default;
86
+ const child = spawn(opener.command, opener.args, {
87
+ detached: true,
88
+ stdio: 'ignore',
89
+ ...opener.options,
90
+ });
91
+
92
+ child.on('error', error => {
93
+ console.warn(`Could not open the browser automatically: ${error.message}`);
94
+ });
95
+
96
+ child.unref();
90
97
  }
91
98
 
92
99
  async function main() {
93
- const { help, port = DEFAULT_PORT } = parseCliArguments(process.argv.slice(2));
100
+ const { help, port = DEFAULT_PORT } = parseCliArguments(process.argv.slice(2));
94
101
 
95
- if (help) {
96
- printHelp();
97
- return;
98
- }
102
+ if (help) {
103
+ printHelp();
104
+ return;
105
+ }
99
106
 
100
- const { startServer } = require("../server/server");
101
- const { url } = await startServer({ port });
102
- openInDefaultBrowser(url);
107
+ const { startServer } = require('../server/server');
108
+ const { url } = await startServer({ port });
109
+ openInDefaultBrowser(url);
103
110
  }
104
111
 
105
112
  if (require.main === module) {
106
- main().catch((error) => {
107
- console.error(error.message);
108
- process.exit(1);
109
- });
113
+ main().catch(error => {
114
+ console.error(error.message);
115
+ process.exit(1);
116
+ });
110
117
  }
111
118
 
112
119
  module.exports = {
113
- main,
114
- openInDefaultBrowser,
115
- parseCliArguments,
120
+ main,
121
+ openInDefaultBrowser,
122
+ parseCliArguments,
116
123
  };
package/changelog.md CHANGED
@@ -1,3 +1,7 @@
1
+ # v0.2.0
2
+
3
+ - db fix
4
+
1
5
  # v0.1.3
2
6
 
3
7
  - edit in sql editro
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-hub",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "main": "server/server.js",
6
6
  "bin": {
package/server/server.js CHANGED
@@ -1,6 +1,7 @@
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");
@@ -17,12 +18,17 @@ const { createDataRouter } = require("./routes/data");
17
18
  const { createSettingsRouter } = require("./routes/settings");
18
19
  const { createExportRouter } = require("./routes/export");
19
20
 
20
- const APP_STATE_DB_PATH = path.resolve(__dirname, "..", "data", "sqlite-hub-state.db");
21
- const LEGACY_STATE_PATH = path.resolve(__dirname, "..", "data", "app-state.json");
21
+ const PACKAGE_ROOT = path.resolve(__dirname, "..");
22
+ const {
23
+ appStateDbPath: APP_STATE_DB_PATH,
24
+ legacyStatePath: LEGACY_STATE_PATH,
25
+ legacyDatabasePaths: LEGACY_DATABASE_PATHS,
26
+ } = resolveAppStatePaths(PACKAGE_ROOT);
22
27
  const DEFAULT_PORT = 4173;
23
28
 
24
29
  const appStateStore = new AppStateStore(APP_STATE_DB_PATH, {
25
30
  legacyFilePath: LEGACY_STATE_PATH,
31
+ legacyDatabasePaths: LEGACY_DATABASE_PATHS,
26
32
  });
27
33
  const connectionManager = new ConnectionManager({ appStateStore });
28
34
  const overviewService = new OverviewService({ connectionManager });
@@ -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
- if (this.shouldImportLegacyState()) {
33
- this.importLegacyState();
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
- importLegacyState() {
125
- const legacyState = this.readLegacyState();
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
- insertSetting.run(key, JSON.stringify(value));
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), this.legacyFilePath)
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);
@@ -0,0 +1,131 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+
5
+ const APP_NAME = "sqlite-hub";
6
+ const APP_STATE_DB_FILENAME = "sqlite-hub-state.db";
7
+ const LEGACY_STATE_FILENAME = "app-state.json";
8
+
9
+ function resolveAppStateDirectory() {
10
+ const homeDirectory = os.homedir();
11
+
12
+ if (process.platform === "darwin") {
13
+ return path.join(homeDirectory, "Library", "Application Support", APP_NAME);
14
+ }
15
+
16
+ if (process.platform === "win32") {
17
+ const appDataDirectory =
18
+ process.env.APPDATA || path.join(homeDirectory, "AppData", "Roaming");
19
+
20
+ return path.join(appDataDirectory, APP_NAME);
21
+ }
22
+
23
+ if (process.env.XDG_STATE_HOME) {
24
+ return path.join(process.env.XDG_STATE_HOME, APP_NAME);
25
+ }
26
+
27
+ return path.join(homeDirectory, ".local", "state", APP_NAME);
28
+ }
29
+
30
+ function resolvePackagedDataDirectory(packageRoot) {
31
+ return path.resolve(packageRoot, "data");
32
+ }
33
+
34
+ function resolvePackagedAppStateDbPath(packageRoot) {
35
+ return path.join(resolvePackagedDataDirectory(packageRoot), APP_STATE_DB_FILENAME);
36
+ }
37
+
38
+ function resolvePackagedLegacyStatePath(packageRoot) {
39
+ return path.join(resolvePackagedDataDirectory(packageRoot), LEGACY_STATE_FILENAME);
40
+ }
41
+
42
+ function resolveHomebrewCellarInfo(packageRoot) {
43
+ const resolvedPackageRoot = path.resolve(packageRoot);
44
+ const { root } = path.parse(resolvedPackageRoot);
45
+ const relativeSegments = resolvedPackageRoot
46
+ .slice(root.length)
47
+ .split(path.sep)
48
+ .filter(Boolean);
49
+ const cellarIndex = relativeSegments.indexOf("Cellar");
50
+
51
+ if (cellarIndex === -1) {
52
+ return null;
53
+ }
54
+
55
+ const formulaName = relativeSegments[cellarIndex + 1];
56
+ const currentVersion = relativeSegments[cellarIndex + 2];
57
+
58
+ if (formulaName !== APP_NAME || !currentVersion) {
59
+ return null;
60
+ }
61
+
62
+ return {
63
+ cellarRoot: path.join(root, ...relativeSegments.slice(0, cellarIndex + 2)),
64
+ currentVersion,
65
+ };
66
+ }
67
+
68
+ function safeStatMtimeMs(filePath) {
69
+ try {
70
+ return fs.statSync(filePath).mtimeMs;
71
+ } catch {
72
+ return -1;
73
+ }
74
+ }
75
+
76
+ function collectHomebrewLegacyStateDbPaths(packageRoot) {
77
+ const cellarInfo = resolveHomebrewCellarInfo(packageRoot);
78
+
79
+ if (!cellarInfo || !fs.existsSync(cellarInfo.cellarRoot)) {
80
+ return [];
81
+ }
82
+
83
+ return fs
84
+ .readdirSync(cellarInfo.cellarRoot, { withFileTypes: true })
85
+ .filter((entry) => entry.isDirectory() && entry.name !== cellarInfo.currentVersion)
86
+ .map((entry) => {
87
+ const candidatePath = path.join(
88
+ cellarInfo.cellarRoot,
89
+ entry.name,
90
+ "libexec",
91
+ "lib",
92
+ "node_modules",
93
+ APP_NAME,
94
+ "data",
95
+ APP_STATE_DB_FILENAME
96
+ );
97
+
98
+ return {
99
+ path: candidatePath,
100
+ mtimeMs: safeStatMtimeMs(candidatePath),
101
+ };
102
+ })
103
+ .filter((candidate) => candidate.mtimeMs >= 0)
104
+ .sort((left, right) => right.mtimeMs - left.mtimeMs)
105
+ .map((candidate) => candidate.path);
106
+ }
107
+
108
+ function collectLegacyDatabasePaths(packageRoot) {
109
+ return [
110
+ resolvePackagedAppStateDbPath(packageRoot),
111
+ ...collectHomebrewLegacyStateDbPaths(packageRoot),
112
+ ].filter((candidatePath, index, candidates) => candidates.indexOf(candidatePath) === index);
113
+ }
114
+
115
+ function resolveAppStatePaths(packageRoot) {
116
+ const appStateDirectory = resolveAppStateDirectory();
117
+
118
+ return {
119
+ appStateDirectory,
120
+ appStateDbPath: path.join(appStateDirectory, APP_STATE_DB_FILENAME),
121
+ legacyStatePath: resolvePackagedLegacyStatePath(packageRoot),
122
+ legacyDatabasePaths: collectLegacyDatabasePaths(packageRoot),
123
+ };
124
+ }
125
+
126
+ module.exports = {
127
+ APP_NAME,
128
+ APP_STATE_DB_FILENAME,
129
+ resolveAppStateDirectory,
130
+ resolveAppStatePaths,
131
+ };