sqlite-hub 0.1.3
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/.npmingnore +4 -0
- package/README.md +46 -0
- package/assets/images/logo.webp +0 -0
- package/assets/images/logo_extrasmall.webp +0 -0
- package/assets/images/logo_raw.png +0 -0
- package/assets/images/logo_small.webp +0 -0
- package/assets/mockups/connections.png +0 -0
- package/assets/mockups/data.png +0 -0
- package/assets/mockups/data_edit.png +0 -0
- package/assets/mockups/home.png +0 -0
- package/assets/mockups/overview.png +0 -0
- package/assets/mockups/sql_editor.png +0 -0
- package/assets/mockups/structure.png +0 -0
- package/bin/sqlite-hub.js +116 -0
- package/changelog.md +3 -0
- package/data/.gitkeep +0 -0
- package/index.html +100 -0
- package/js/api.js +193 -0
- package/js/app.js +520 -0
- package/js/components/actionBar.js +8 -0
- package/js/components/appShell.js +17 -0
- package/js/components/badges.js +5 -0
- package/js/components/bottomTabs.js +37 -0
- package/js/components/connectionCard.js +106 -0
- package/js/components/dataGrid.js +47 -0
- package/js/components/emptyState.js +159 -0
- package/js/components/metricCard.js +32 -0
- package/js/components/modal.js +317 -0
- package/js/components/pageHeader.js +33 -0
- package/js/components/queryEditor.js +121 -0
- package/js/components/queryResults.js +107 -0
- package/js/components/rowEditorPanel.js +164 -0
- package/js/components/sidebar.js +57 -0
- package/js/components/statusBar.js +39 -0
- package/js/components/toast.js +39 -0
- package/js/components/topNav.js +27 -0
- package/js/router.js +66 -0
- package/js/store.js +1092 -0
- package/js/utils/format.js +179 -0
- package/js/views/connections.js +133 -0
- package/js/views/data.js +400 -0
- package/js/views/editor.js +259 -0
- package/js/views/landing.js +11 -0
- package/js/views/overview.js +220 -0
- package/js/views/settings.js +109 -0
- package/js/views/structure.js +242 -0
- package/package.json +18 -0
- package/publish_brew.sh +444 -0
- package/publish_npm.sh +241 -0
- package/server/routes/connections.js +146 -0
- package/server/routes/data.js +59 -0
- package/server/routes/export.js +25 -0
- package/server/routes/overview.js +39 -0
- package/server/routes/settings.js +50 -0
- package/server/routes/sql.js +50 -0
- package/server/routes/structure.js +38 -0
- package/server/server.js +136 -0
- package/server/services/sqlite/connectionManager.js +306 -0
- package/server/services/sqlite/dataBrowserService.js +255 -0
- package/server/services/sqlite/exportService.js +34 -0
- package/server/services/sqlite/importService.js +111 -0
- package/server/services/sqlite/introspection.js +302 -0
- package/server/services/sqlite/overviewService.js +109 -0
- package/server/services/sqlite/sqlExecutor.js +434 -0
- package/server/services/sqlite/structureService.js +60 -0
- package/server/services/storage/appStateStore.js +530 -0
- package/server/utils/csv.js +34 -0
- package/server/utils/errors.js +175 -0
- package/server/utils/fileValidation.js +135 -0
- package/server/utils/identifier.js +38 -0
- package/server/utils/sqliteTypes.js +112 -0
- package/styles/base.css +176 -0
- package/styles/components.css +323 -0
- package/styles/layout.css +101 -0
- package/styles/tokens.css +49 -0
- package/styles/views.css +84 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const Database = require("better-sqlite3");
|
|
4
|
+
|
|
5
|
+
const DEFAULT_STATE = {
|
|
6
|
+
recentConnections: [],
|
|
7
|
+
activeConnectionId: null,
|
|
8
|
+
sqlHistory: [],
|
|
9
|
+
settings: {
|
|
10
|
+
defaultPageSize: 50,
|
|
11
|
+
maxPageSize: 200,
|
|
12
|
+
maxRecentConnections: 12,
|
|
13
|
+
maxSqlHistory: 100,
|
|
14
|
+
busyTimeoutMs: 5000,
|
|
15
|
+
csvDelimiter: ",",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
class AppStateStore {
|
|
20
|
+
constructor(filePath, options = {}) {
|
|
21
|
+
this.filePath = filePath;
|
|
22
|
+
this.legacyFilePath = options.legacyFilePath ?? null;
|
|
23
|
+
this.isFreshDatabase = !fs.existsSync(filePath);
|
|
24
|
+
|
|
25
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
26
|
+
|
|
27
|
+
this.db = new Database(this.filePath);
|
|
28
|
+
this.configureDatabase();
|
|
29
|
+
this.ensureSchema();
|
|
30
|
+
this.seedDefaultSettings();
|
|
31
|
+
|
|
32
|
+
if (this.shouldImportLegacyState()) {
|
|
33
|
+
this.importLegacyState();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
configureDatabase() {
|
|
38
|
+
this.db.pragma("journal_mode = WAL");
|
|
39
|
+
this.db.pragma("synchronous = NORMAL");
|
|
40
|
+
this.db.pragma("foreign_keys = ON");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ensureSchema() {
|
|
44
|
+
this.db.exec(`
|
|
45
|
+
CREATE TABLE IF NOT EXISTS app_meta (
|
|
46
|
+
key TEXT PRIMARY KEY,
|
|
47
|
+
value TEXT
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
51
|
+
key TEXT PRIMARY KEY,
|
|
52
|
+
value TEXT NOT NULL
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS recent_connections (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
label TEXT NOT NULL,
|
|
58
|
+
path TEXT NOT NULL,
|
|
59
|
+
lastOpenedAt TEXT NOT NULL,
|
|
60
|
+
lastModifiedAt TEXT,
|
|
61
|
+
sizeBytes INTEGER,
|
|
62
|
+
readOnly INTEGER NOT NULL DEFAULT 0
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS sql_history (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
connectionId TEXT,
|
|
68
|
+
connectionLabel TEXT,
|
|
69
|
+
sql TEXT NOT NULL,
|
|
70
|
+
statementCount INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
resultKind TEXT,
|
|
72
|
+
affectedRowCount INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
rowCount INTEGER NOT NULL DEFAULT 0,
|
|
74
|
+
timingMs INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
executedAt TEXT NOT NULL
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_recent_connections_last_opened
|
|
79
|
+
ON recent_connections(lastOpenedAt DESC, id ASC);
|
|
80
|
+
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_sql_history_executed_at
|
|
82
|
+
ON sql_history(executedAt DESC, id ASC);
|
|
83
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
seedDefaultSettings() {
|
|
87
|
+
const insertSetting = this.db.prepare(`
|
|
88
|
+
INSERT INTO settings (key, value)
|
|
89
|
+
VALUES (?, ?)
|
|
90
|
+
ON CONFLICT(key) DO NOTHING
|
|
91
|
+
`);
|
|
92
|
+
|
|
93
|
+
for (const [key, value] of Object.entries(DEFAULT_STATE.settings)) {
|
|
94
|
+
insertSetting.run(key, JSON.stringify(value));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
shouldImportLegacyState() {
|
|
99
|
+
if (!this.isFreshDatabase) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!this.legacyFilePath || this.legacyFilePath === this.filePath) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return fs.existsSync(this.legacyFilePath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
readLegacyState() {
|
|
111
|
+
const raw = fs.readFileSync(this.legacyFilePath, "utf8");
|
|
112
|
+
const parsed = JSON.parse(raw);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
...DEFAULT_STATE,
|
|
116
|
+
...parsed,
|
|
117
|
+
settings: {
|
|
118
|
+
...DEFAULT_STATE.settings,
|
|
119
|
+
...(parsed.settings ?? {}),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
importLegacyState() {
|
|
125
|
+
const legacyState = this.readLegacyState();
|
|
126
|
+
const insertSetting = this.db.prepare(`
|
|
127
|
+
INSERT INTO settings (key, value)
|
|
128
|
+
VALUES (?, ?)
|
|
129
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
130
|
+
`);
|
|
131
|
+
const insertConnection = this.db.prepare(`
|
|
132
|
+
INSERT INTO recent_connections (
|
|
133
|
+
id,
|
|
134
|
+
label,
|
|
135
|
+
path,
|
|
136
|
+
lastOpenedAt,
|
|
137
|
+
lastModifiedAt,
|
|
138
|
+
sizeBytes,
|
|
139
|
+
readOnly
|
|
140
|
+
)
|
|
141
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
142
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
143
|
+
label = excluded.label,
|
|
144
|
+
path = excluded.path,
|
|
145
|
+
lastOpenedAt = excluded.lastOpenedAt,
|
|
146
|
+
lastModifiedAt = excluded.lastModifiedAt,
|
|
147
|
+
sizeBytes = excluded.sizeBytes,
|
|
148
|
+
readOnly = excluded.readOnly
|
|
149
|
+
`);
|
|
150
|
+
const insertHistory = this.db.prepare(`
|
|
151
|
+
INSERT INTO sql_history (
|
|
152
|
+
id,
|
|
153
|
+
connectionId,
|
|
154
|
+
connectionLabel,
|
|
155
|
+
sql,
|
|
156
|
+
statementCount,
|
|
157
|
+
resultKind,
|
|
158
|
+
affectedRowCount,
|
|
159
|
+
rowCount,
|
|
160
|
+
timingMs,
|
|
161
|
+
executedAt
|
|
162
|
+
)
|
|
163
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
164
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
165
|
+
connectionId = excluded.connectionId,
|
|
166
|
+
connectionLabel = excluded.connectionLabel,
|
|
167
|
+
sql = excluded.sql,
|
|
168
|
+
statementCount = excluded.statementCount,
|
|
169
|
+
resultKind = excluded.resultKind,
|
|
170
|
+
affectedRowCount = excluded.affectedRowCount,
|
|
171
|
+
rowCount = excluded.rowCount,
|
|
172
|
+
timingMs = excluded.timingMs,
|
|
173
|
+
executedAt = excluded.executedAt
|
|
174
|
+
`);
|
|
175
|
+
|
|
176
|
+
this.db.transaction(() => {
|
|
177
|
+
for (const [key, value] of Object.entries(legacyState.settings ?? {})) {
|
|
178
|
+
insertSetting.run(key, JSON.stringify(value));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const connection of legacyState.recentConnections ?? []) {
|
|
182
|
+
insertConnection.run(
|
|
183
|
+
connection.id,
|
|
184
|
+
connection.label ?? path.basename(connection.path ?? connection.id),
|
|
185
|
+
connection.path ?? "",
|
|
186
|
+
connection.lastOpenedAt ?? new Date().toISOString(),
|
|
187
|
+
connection.lastModifiedAt ?? null,
|
|
188
|
+
connection.sizeBytes ?? null,
|
|
189
|
+
connection.readOnly ? 1 : 0
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const entry of legacyState.sqlHistory ?? []) {
|
|
194
|
+
insertHistory.run(
|
|
195
|
+
entry.id,
|
|
196
|
+
entry.connectionId ?? null,
|
|
197
|
+
entry.connectionLabel ?? null,
|
|
198
|
+
entry.sql ?? "",
|
|
199
|
+
Number(entry.statementCount ?? 0),
|
|
200
|
+
entry.resultKind ?? null,
|
|
201
|
+
Number(entry.affectedRowCount ?? 0),
|
|
202
|
+
Number(entry.rowCount ?? 0),
|
|
203
|
+
Number(entry.timingMs ?? 0),
|
|
204
|
+
entry.executedAt ?? new Date().toISOString()
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.setMetaValue("activeConnectionId", legacyState.activeConnectionId ?? null);
|
|
209
|
+
this.setMetaValue(
|
|
210
|
+
"legacyImportSource",
|
|
211
|
+
path.relative(path.dirname(this.filePath), this.legacyFilePath)
|
|
212
|
+
);
|
|
213
|
+
})();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
parseStoredValue(value) {
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(value);
|
|
219
|
+
} catch {
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getMetaValue(key) {
|
|
225
|
+
const row = this.db
|
|
226
|
+
.prepare("SELECT value FROM app_meta WHERE key = ?")
|
|
227
|
+
.get(key);
|
|
228
|
+
|
|
229
|
+
return row ? row.value : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
setMetaValue(key, value) {
|
|
233
|
+
if (value === null || value === undefined || value === "") {
|
|
234
|
+
this.db.prepare("DELETE FROM app_meta WHERE key = ?").run(key);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.db
|
|
239
|
+
.prepare(`
|
|
240
|
+
INSERT INTO app_meta (key, value)
|
|
241
|
+
VALUES (?, ?)
|
|
242
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
243
|
+
`)
|
|
244
|
+
.run(key, String(value));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
trimRecentConnections() {
|
|
248
|
+
const maxRecentConnections = Number(
|
|
249
|
+
this.getSettings().maxRecentConnections ?? DEFAULT_STATE.settings.maxRecentConnections
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const staleRows = this.db
|
|
253
|
+
.prepare(`
|
|
254
|
+
SELECT id
|
|
255
|
+
FROM recent_connections
|
|
256
|
+
ORDER BY lastOpenedAt DESC, id ASC
|
|
257
|
+
LIMIT -1 OFFSET ?
|
|
258
|
+
`)
|
|
259
|
+
.all(maxRecentConnections);
|
|
260
|
+
|
|
261
|
+
if (!staleRows.length) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const activeConnectionId = this.getActiveConnectionId();
|
|
266
|
+
|
|
267
|
+
for (const row of staleRows) {
|
|
268
|
+
this.db.prepare("DELETE FROM recent_connections WHERE id = ?").run(row.id);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (activeConnectionId && staleRows.some((row) => row.id === activeConnectionId)) {
|
|
272
|
+
this.setMetaValue("activeConnectionId", null);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
trimSqlHistory() {
|
|
277
|
+
const maxSqlHistory = Number(
|
|
278
|
+
this.getSettings().maxSqlHistory ?? DEFAULT_STATE.settings.maxSqlHistory
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const staleRows = this.db
|
|
282
|
+
.prepare(`
|
|
283
|
+
SELECT id
|
|
284
|
+
FROM sql_history
|
|
285
|
+
ORDER BY executedAt DESC, id ASC
|
|
286
|
+
LIMIT -1 OFFSET ?
|
|
287
|
+
`)
|
|
288
|
+
.all(maxSqlHistory);
|
|
289
|
+
|
|
290
|
+
for (const row of staleRows) {
|
|
291
|
+
this.db.prepare("DELETE FROM sql_history WHERE id = ?").run(row.id);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
getState() {
|
|
296
|
+
return structuredClone({
|
|
297
|
+
recentConnections: this.getRecentConnections(),
|
|
298
|
+
activeConnectionId: this.getActiveConnectionId(),
|
|
299
|
+
sqlHistory: this.getSqlHistory(),
|
|
300
|
+
settings: this.getSettings(),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getRecentConnections() {
|
|
305
|
+
return this.db
|
|
306
|
+
.prepare(`
|
|
307
|
+
SELECT
|
|
308
|
+
id,
|
|
309
|
+
label,
|
|
310
|
+
path,
|
|
311
|
+
lastOpenedAt,
|
|
312
|
+
lastModifiedAt,
|
|
313
|
+
sizeBytes,
|
|
314
|
+
readOnly
|
|
315
|
+
FROM recent_connections
|
|
316
|
+
ORDER BY lastOpenedAt DESC, id ASC
|
|
317
|
+
`)
|
|
318
|
+
.all()
|
|
319
|
+
.map((connection) => ({
|
|
320
|
+
...connection,
|
|
321
|
+
readOnly: Boolean(connection.readOnly),
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
upsertRecentConnection(connection) {
|
|
326
|
+
this.db.transaction(() => {
|
|
327
|
+
this.db
|
|
328
|
+
.prepare(`
|
|
329
|
+
INSERT INTO recent_connections (
|
|
330
|
+
id,
|
|
331
|
+
label,
|
|
332
|
+
path,
|
|
333
|
+
lastOpenedAt,
|
|
334
|
+
lastModifiedAt,
|
|
335
|
+
sizeBytes,
|
|
336
|
+
readOnly
|
|
337
|
+
)
|
|
338
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
339
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
340
|
+
label = excluded.label,
|
|
341
|
+
path = excluded.path,
|
|
342
|
+
lastOpenedAt = excluded.lastOpenedAt,
|
|
343
|
+
lastModifiedAt = excluded.lastModifiedAt,
|
|
344
|
+
sizeBytes = excluded.sizeBytes,
|
|
345
|
+
readOnly = excluded.readOnly
|
|
346
|
+
`)
|
|
347
|
+
.run(
|
|
348
|
+
connection.id,
|
|
349
|
+
connection.label,
|
|
350
|
+
connection.path,
|
|
351
|
+
connection.lastOpenedAt,
|
|
352
|
+
connection.lastModifiedAt ?? null,
|
|
353
|
+
connection.sizeBytes ?? null,
|
|
354
|
+
connection.readOnly ? 1 : 0
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
this.setMetaValue("activeConnectionId", connection.id);
|
|
358
|
+
this.trimRecentConnections();
|
|
359
|
+
})();
|
|
360
|
+
|
|
361
|
+
return this.getRecentConnections();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
removeRecentConnection(id) {
|
|
365
|
+
this.db.transaction(() => {
|
|
366
|
+
this.db.prepare("DELETE FROM recent_connections WHERE id = ?").run(id);
|
|
367
|
+
|
|
368
|
+
if (this.getActiveConnectionId() === id) {
|
|
369
|
+
this.setMetaValue("activeConnectionId", null);
|
|
370
|
+
}
|
|
371
|
+
})();
|
|
372
|
+
|
|
373
|
+
return this.getState();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
updateRecentConnection(id, updater) {
|
|
377
|
+
const existing = this.getRecentConnections().find((connection) => connection.id === id);
|
|
378
|
+
|
|
379
|
+
if (!existing) {
|
|
380
|
+
return this.getRecentConnections();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const nextConnection = updater(structuredClone(existing)) ?? existing;
|
|
384
|
+
|
|
385
|
+
this.db
|
|
386
|
+
.prepare(`
|
|
387
|
+
INSERT INTO recent_connections (
|
|
388
|
+
id,
|
|
389
|
+
label,
|
|
390
|
+
path,
|
|
391
|
+
lastOpenedAt,
|
|
392
|
+
lastModifiedAt,
|
|
393
|
+
sizeBytes,
|
|
394
|
+
readOnly
|
|
395
|
+
)
|
|
396
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
397
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
398
|
+
label = excluded.label,
|
|
399
|
+
path = excluded.path,
|
|
400
|
+
lastOpenedAt = excluded.lastOpenedAt,
|
|
401
|
+
lastModifiedAt = excluded.lastModifiedAt,
|
|
402
|
+
sizeBytes = excluded.sizeBytes,
|
|
403
|
+
readOnly = excluded.readOnly
|
|
404
|
+
`)
|
|
405
|
+
.run(
|
|
406
|
+
nextConnection.id,
|
|
407
|
+
nextConnection.label,
|
|
408
|
+
nextConnection.path,
|
|
409
|
+
nextConnection.lastOpenedAt ?? existing.lastOpenedAt,
|
|
410
|
+
nextConnection.lastModifiedAt ?? null,
|
|
411
|
+
nextConnection.sizeBytes ?? null,
|
|
412
|
+
nextConnection.readOnly ? 1 : 0
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
return this.getRecentConnections();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
setActiveConnectionId(id) {
|
|
419
|
+
this.setMetaValue("activeConnectionId", id ?? null);
|
|
420
|
+
return this.getActiveConnectionId();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
getActiveConnectionId() {
|
|
424
|
+
return this.getMetaValue("activeConnectionId");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
addSqlHistory(entry) {
|
|
428
|
+
this.db.transaction(() => {
|
|
429
|
+
this.db
|
|
430
|
+
.prepare(`
|
|
431
|
+
INSERT INTO sql_history (
|
|
432
|
+
id,
|
|
433
|
+
connectionId,
|
|
434
|
+
connectionLabel,
|
|
435
|
+
sql,
|
|
436
|
+
statementCount,
|
|
437
|
+
resultKind,
|
|
438
|
+
affectedRowCount,
|
|
439
|
+
rowCount,
|
|
440
|
+
timingMs,
|
|
441
|
+
executedAt
|
|
442
|
+
)
|
|
443
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
444
|
+
`)
|
|
445
|
+
.run(
|
|
446
|
+
entry.id,
|
|
447
|
+
entry.connectionId ?? null,
|
|
448
|
+
entry.connectionLabel ?? null,
|
|
449
|
+
entry.sql,
|
|
450
|
+
Number(entry.statementCount ?? 0),
|
|
451
|
+
entry.resultKind ?? null,
|
|
452
|
+
Number(entry.affectedRowCount ?? 0),
|
|
453
|
+
Number(entry.rowCount ?? 0),
|
|
454
|
+
Number(entry.timingMs ?? 0),
|
|
455
|
+
entry.executedAt
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
this.trimSqlHistory();
|
|
459
|
+
})();
|
|
460
|
+
|
|
461
|
+
return this.getSqlHistory();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
clearSqlHistory() {
|
|
465
|
+
this.db.prepare("DELETE FROM sql_history").run();
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
getSqlHistory() {
|
|
470
|
+
return this.db
|
|
471
|
+
.prepare(`
|
|
472
|
+
SELECT
|
|
473
|
+
id,
|
|
474
|
+
connectionId,
|
|
475
|
+
connectionLabel,
|
|
476
|
+
sql,
|
|
477
|
+
statementCount,
|
|
478
|
+
resultKind,
|
|
479
|
+
affectedRowCount,
|
|
480
|
+
rowCount,
|
|
481
|
+
timingMs,
|
|
482
|
+
executedAt
|
|
483
|
+
FROM sql_history
|
|
484
|
+
ORDER BY executedAt DESC, id ASC
|
|
485
|
+
`)
|
|
486
|
+
.all()
|
|
487
|
+
.map((entry) => ({
|
|
488
|
+
...entry,
|
|
489
|
+
statementCount: Number(entry.statementCount ?? 0),
|
|
490
|
+
affectedRowCount: Number(entry.affectedRowCount ?? 0),
|
|
491
|
+
rowCount: Number(entry.rowCount ?? 0),
|
|
492
|
+
timingMs: Number(entry.timingMs ?? 0),
|
|
493
|
+
}));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
getSettings() {
|
|
497
|
+
const rows = this.db.prepare("SELECT key, value FROM settings").all();
|
|
498
|
+
const parsedSettings = Object.fromEntries(
|
|
499
|
+
rows.map((row) => [row.key, this.parseStoredValue(row.value)])
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
...DEFAULT_STATE.settings,
|
|
504
|
+
...parsedSettings,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
patchSettings(partialSettings) {
|
|
509
|
+
const entries = Object.entries(partialSettings ?? {});
|
|
510
|
+
|
|
511
|
+
this.db.transaction(() => {
|
|
512
|
+
for (const [key, value] of entries) {
|
|
513
|
+
this.db
|
|
514
|
+
.prepare(`
|
|
515
|
+
INSERT INTO settings (key, value)
|
|
516
|
+
VALUES (?, ?)
|
|
517
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
518
|
+
`)
|
|
519
|
+
.run(key, JSON.stringify(value));
|
|
520
|
+
}
|
|
521
|
+
})();
|
|
522
|
+
|
|
523
|
+
return this.getSettings();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
module.exports = {
|
|
528
|
+
AppStateStore,
|
|
529
|
+
DEFAULT_STATE,
|
|
530
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function escapeCsvCell(value, delimiter) {
|
|
2
|
+
if (value === null || value === undefined) {
|
|
3
|
+
return "";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const stringValue =
|
|
7
|
+
typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
8
|
+
|
|
9
|
+
if (
|
|
10
|
+
stringValue.includes('"') ||
|
|
11
|
+
stringValue.includes("\n") ||
|
|
12
|
+
stringValue.includes("\r") ||
|
|
13
|
+
stringValue.includes(delimiter)
|
|
14
|
+
) {
|
|
15
|
+
return `"${stringValue.replaceAll('"', '""')}"`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return stringValue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function rowsToCsv({ columns, rows, delimiter = "," }) {
|
|
22
|
+
const header = columns.map((column) => escapeCsvCell(column, delimiter)).join(delimiter);
|
|
23
|
+
const body = rows.map((row) =>
|
|
24
|
+
columns
|
|
25
|
+
.map((column) => escapeCsvCell(row[column], delimiter))
|
|
26
|
+
.join(delimiter)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return [header, ...body].join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
rowsToCsv,
|
|
34
|
+
};
|