sqlite-hub 0.16.0 → 0.17.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 +106 -19
- package/bin/sqlite-hub.js +1041 -224
- package/frontend/index.html +1 -0
- package/frontend/js/api.js +28 -0
- package/frontend/js/app.js +362 -6
- package/frontend/js/components/modal.js +244 -44
- package/frontend/js/components/pageHeader.js +14 -16
- package/frontend/js/components/queryHistoryPanel.js +126 -131
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +641 -0
- package/frontend/js/utils/markdownDocuments.js +248 -0
- package/frontend/js/views/documents.js +300 -0
- package/frontend/js/views/settings.js +39 -3
- package/frontend/styles/components.css +13 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +422 -0
- package/package.json +2 -1
- package/server/routes/documents.js +163 -0
- package/server/routes/settings.js +22 -6
- package/server/server.js +6 -0
- package/server/services/storage/appStateStore.js +313 -0
- package/tests/cli-args.test.js +100 -0
- package/tests/copy-column-modal.test.js +83 -0
- package/tests/database-documents.test.js +85 -0
- package/tests/markdown-documents.test.js +79 -0
- package/tests/settings-metadata.test.js +16 -0
- package/tests/settings-view.test.js +32 -0
package/bin/sqlite-hub.js
CHANGED
|
@@ -1,26 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
3
5
|
const { spawn } = require('node:child_process');
|
|
4
6
|
|
|
5
7
|
const DEFAULT_PORT = 4173;
|
|
8
|
+
const EXPORT_FORMATS = new Set(['csv', 'tsv', 'md']);
|
|
6
9
|
|
|
7
10
|
function printHelp() {
|
|
8
11
|
console.log(`SQLite Hub CLI
|
|
9
12
|
|
|
10
13
|
Usage:
|
|
11
14
|
sqlite-hub [--port:4173]
|
|
15
|
+
sqlite-hub --database
|
|
16
|
+
sqlite-hub --database:"name" --tables
|
|
17
|
+
sqlite-hub --database:"name" --execute:"Saved Query"
|
|
18
|
+
sqlite-hub --database:"name" --query:"Saved Query"
|
|
19
|
+
sqlite-hub --database:"name" --notes:"Saved Query"
|
|
20
|
+
sqlite-hub --database:"name" --export:"Saved Query" --format:csv
|
|
21
|
+
sqlite-hub --database:"name" --documents
|
|
22
|
+
sqlite-hub --database:"name" --documents:"Document Name"
|
|
23
|
+
sqlite-hub --database:"name" --documents:"Document Name" --export
|
|
24
|
+
sqlite-hub --database:"name" --table:"table_name"
|
|
25
|
+
sqlite-hub --database:"name" --table:"table_name" --export:"primary-key"
|
|
12
26
|
|
|
13
27
|
Options:
|
|
14
|
-
--help
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--database-
|
|
20
|
-
--database
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
28
|
+
--help, -h Show this help text.
|
|
29
|
+
--version, -v Show the version number.
|
|
30
|
+
--config Show CLI port, URL, app version, and SQLite version.
|
|
31
|
+
--open Start/open SQLite Hub in the browser.
|
|
32
|
+
--port:PORT Start the server on a custom port.
|
|
33
|
+
--database, -d List all imported databases.
|
|
34
|
+
--database:"name" Select an imported database by name or id.
|
|
35
|
+
--path Print the selected database file path.
|
|
36
|
+
--size Print the selected database file size.
|
|
37
|
+
--lastopened Print the selected database last-opened timestamp.
|
|
38
|
+
--tables List tables in the selected database.
|
|
39
|
+
--queries List saved SQL Editor queries for the selected database.
|
|
40
|
+
--execute:"query" Execute a saved SQL Editor query by name.
|
|
41
|
+
--query:"query" Print a saved SQL Editor query by name.
|
|
42
|
+
--notes:"query" Print notes for a saved SQL Editor query by name.
|
|
43
|
+
--export:"query" Export a saved query when --table is not set.
|
|
44
|
+
--format:csv|tsv|md Export format for query exports. Defaults to csv.
|
|
45
|
+
--documents List Markdown documents for the selected database.
|
|
46
|
+
--documents:"name" Print a document's Markdown content.
|
|
47
|
+
--documents:"name" --export Export a document as a Markdown file.
|
|
48
|
+
--table:"table" Print table metadata.
|
|
49
|
+
--table:"table" --export:"pk" Export one row as JSON by primary key or rowid.
|
|
50
|
+
|
|
51
|
+
Legacy aliases still work:
|
|
52
|
+
--database-path:name, --database-size:name, --database-lastopened:name
|
|
53
|
+
--database-tables:name, --database:name --sqleditor, --database:name --sqleditor:"query"
|
|
24
54
|
`);
|
|
25
55
|
}
|
|
26
56
|
|
|
@@ -34,76 +64,330 @@ function parsePort(rawValue) {
|
|
|
34
64
|
return port;
|
|
35
65
|
}
|
|
36
66
|
|
|
67
|
+
function splitArgument(argument) {
|
|
68
|
+
const colonIndex = argument.indexOf(':');
|
|
69
|
+
const equalsIndex = argument.indexOf('=');
|
|
70
|
+
const separators = [colonIndex, equalsIndex].filter(index => index > -1);
|
|
71
|
+
|
|
72
|
+
if (separators.length === 0) {
|
|
73
|
+
return {
|
|
74
|
+
flag: normalizeFlagName(argument),
|
|
75
|
+
value: undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const separatorIndex = Math.min(...separators);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
flag: normalizeFlagName(argument.slice(0, separatorIndex)),
|
|
83
|
+
value: argument.slice(separatorIndex + 1),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeFlagName(flag) {
|
|
88
|
+
if (flag === '---path') {
|
|
89
|
+
return '--path';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return flag;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function takeFlagValue(flag, inlineValue, argv, index) {
|
|
96
|
+
if (inlineValue !== undefined) {
|
|
97
|
+
return {
|
|
98
|
+
value: inlineValue,
|
|
99
|
+
nextIndex: index,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const nextValue = argv[index + 1];
|
|
104
|
+
|
|
105
|
+
if (nextValue === undefined || nextValue.startsWith('--')) {
|
|
106
|
+
throw new Error(`${flag} requires a value.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
value: nextValue,
|
|
111
|
+
nextIndex: index + 1,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function takeOptionalFlagValue(inlineValue, argv, index) {
|
|
116
|
+
if (inlineValue !== undefined) {
|
|
117
|
+
return {
|
|
118
|
+
hasValue: true,
|
|
119
|
+
value: inlineValue,
|
|
120
|
+
nextIndex: index,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const nextValue = argv[index + 1];
|
|
125
|
+
|
|
126
|
+
if (nextValue === undefined || nextValue.startsWith('--')) {
|
|
127
|
+
return {
|
|
128
|
+
hasValue: false,
|
|
129
|
+
value: null,
|
|
130
|
+
nextIndex: index,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
hasValue: true,
|
|
136
|
+
value: nextValue,
|
|
137
|
+
nextIndex: index + 1,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeExportFormat(format) {
|
|
142
|
+
const normalized = String(format ?? 'csv').toLowerCase();
|
|
143
|
+
|
|
144
|
+
if (!EXPORT_FORMATS.has(normalized)) {
|
|
145
|
+
throw new Error(`Unsupported export format: ${format}. Use csv, tsv, or md.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return normalized;
|
|
149
|
+
}
|
|
150
|
+
|
|
37
151
|
function parseCliArguments(argv) {
|
|
38
|
-
|
|
39
|
-
|
|
152
|
+
const options = {
|
|
153
|
+
help: false,
|
|
154
|
+
version: false,
|
|
155
|
+
config: false,
|
|
156
|
+
open: false,
|
|
157
|
+
port: undefined,
|
|
158
|
+
databaseList: false,
|
|
159
|
+
databaseName: null,
|
|
160
|
+
pathInfo: false,
|
|
161
|
+
sizeInfo: false,
|
|
162
|
+
lastOpenedInfo: false,
|
|
163
|
+
tables: false,
|
|
164
|
+
queries: false,
|
|
165
|
+
executeQuery: null,
|
|
166
|
+
showQuery: null,
|
|
167
|
+
showNotes: null,
|
|
168
|
+
exportTarget: null,
|
|
169
|
+
exportFormat: 'csv',
|
|
170
|
+
documents: false,
|
|
171
|
+
documentName: null,
|
|
172
|
+
documentExport: false,
|
|
173
|
+
tableName: null,
|
|
174
|
+
};
|
|
40
175
|
|
|
41
176
|
for (let index = 0; index < argv.length; index += 1) {
|
|
42
177
|
const argument = argv[index];
|
|
178
|
+
const { flag, value } = splitArgument(argument);
|
|
43
179
|
|
|
44
|
-
if (
|
|
45
|
-
|
|
180
|
+
if (flag === '--help' || flag === '-h') {
|
|
181
|
+
options.help = true;
|
|
182
|
+
continue;
|
|
46
183
|
}
|
|
47
184
|
|
|
48
|
-
if (
|
|
49
|
-
|
|
185
|
+
if (flag === '--version' || flag === '-v') {
|
|
186
|
+
options.version = true;
|
|
187
|
+
continue;
|
|
50
188
|
}
|
|
51
189
|
|
|
52
|
-
if (
|
|
53
|
-
|
|
190
|
+
if (flag === '--config') {
|
|
191
|
+
options.config = true;
|
|
54
192
|
continue;
|
|
55
193
|
}
|
|
56
194
|
|
|
57
|
-
if (
|
|
58
|
-
|
|
195
|
+
if (flag === '--open') {
|
|
196
|
+
options.open = true;
|
|
197
|
+
continue;
|
|
59
198
|
}
|
|
60
199
|
|
|
61
|
-
if (
|
|
62
|
-
|
|
200
|
+
if (flag === '--port') {
|
|
201
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
202
|
+
options.port = parsePort(parsed.value);
|
|
203
|
+
index = parsed.nextIndex;
|
|
204
|
+
continue;
|
|
63
205
|
}
|
|
64
206
|
|
|
65
|
-
if (
|
|
66
|
-
|
|
207
|
+
if (flag === '--database' || flag === '-d') {
|
|
208
|
+
const parsed = takeOptionalFlagValue(value, argv, index);
|
|
209
|
+
|
|
210
|
+
if (parsed.hasValue) {
|
|
211
|
+
options.databaseName = parsed.value;
|
|
212
|
+
} else {
|
|
213
|
+
options.databaseList = true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
index = parsed.nextIndex;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (flag === '--database-path') {
|
|
221
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
222
|
+
options.databaseName = parsed.value;
|
|
223
|
+
options.pathInfo = true;
|
|
224
|
+
index = parsed.nextIndex;
|
|
225
|
+
continue;
|
|
67
226
|
}
|
|
68
227
|
|
|
69
|
-
if (
|
|
70
|
-
|
|
228
|
+
if (flag === '--database-size') {
|
|
229
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
230
|
+
options.databaseName = parsed.value;
|
|
231
|
+
options.sizeInfo = true;
|
|
232
|
+
index = parsed.nextIndex;
|
|
233
|
+
continue;
|
|
71
234
|
}
|
|
72
235
|
|
|
73
|
-
if (
|
|
74
|
-
|
|
236
|
+
if (flag === '--database-lastopened') {
|
|
237
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
238
|
+
options.databaseName = parsed.value;
|
|
239
|
+
options.lastOpenedInfo = true;
|
|
240
|
+
index = parsed.nextIndex;
|
|
241
|
+
continue;
|
|
75
242
|
}
|
|
76
243
|
|
|
77
|
-
if (
|
|
78
|
-
|
|
244
|
+
if (flag === '--database-tables') {
|
|
245
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
246
|
+
options.databaseName = parsed.value;
|
|
247
|
+
options.tables = true;
|
|
248
|
+
index = parsed.nextIndex;
|
|
249
|
+
continue;
|
|
79
250
|
}
|
|
80
251
|
|
|
81
|
-
if (
|
|
82
|
-
|
|
252
|
+
if (flag === '--path') {
|
|
253
|
+
options.pathInfo = true;
|
|
83
254
|
continue;
|
|
84
255
|
}
|
|
85
256
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
257
|
+
if (flag === '--size') {
|
|
258
|
+
options.sizeInfo = true;
|
|
88
259
|
continue;
|
|
89
260
|
}
|
|
90
261
|
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
index += 1;
|
|
262
|
+
if (flag === '--lastopened') {
|
|
263
|
+
options.lastOpenedInfo = true;
|
|
94
264
|
continue;
|
|
95
265
|
}
|
|
96
266
|
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
267
|
+
if (flag === '--tables') {
|
|
268
|
+
options.tables = true;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (flag === '--queries') {
|
|
273
|
+
options.queries = true;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (flag === '--sqleditor') {
|
|
278
|
+
const parsed = takeOptionalFlagValue(value, argv, index);
|
|
279
|
+
|
|
280
|
+
if (parsed.hasValue) {
|
|
281
|
+
options.executeQuery = parsed.value;
|
|
282
|
+
} else {
|
|
283
|
+
options.queries = true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
index = parsed.nextIndex;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (flag === '--execute') {
|
|
291
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
292
|
+
options.executeQuery = parsed.value;
|
|
293
|
+
index = parsed.nextIndex;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (flag === '--query') {
|
|
298
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
299
|
+
options.showQuery = parsed.value;
|
|
300
|
+
index = parsed.nextIndex;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (flag === '--notes') {
|
|
305
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
306
|
+
options.showNotes = parsed.value;
|
|
307
|
+
index = parsed.nextIndex;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (flag === '--documents') {
|
|
312
|
+
const parsed = takeOptionalFlagValue(value, argv, index);
|
|
313
|
+
|
|
314
|
+
options.documents = true;
|
|
315
|
+
if (parsed.hasValue) {
|
|
316
|
+
const rawDocumentName = String(parsed.value ?? '');
|
|
317
|
+
|
|
318
|
+
if (rawDocumentName.endsWith('--export')) {
|
|
319
|
+
options.documentName = rawDocumentName.slice(0, -'--export'.length);
|
|
320
|
+
options.documentExport = true;
|
|
321
|
+
} else {
|
|
322
|
+
options.documentName = rawDocumentName;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
index = parsed.nextIndex;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (flag === '--documents-export') {
|
|
331
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
332
|
+
options.documents = true;
|
|
333
|
+
options.documentName = parsed.value;
|
|
334
|
+
options.documentExport = true;
|
|
335
|
+
index = parsed.nextIndex;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (flag === '--export') {
|
|
340
|
+
if (options.documents && options.documentName && value === undefined) {
|
|
341
|
+
const nextValue = argv[index + 1];
|
|
342
|
+
|
|
343
|
+
if (nextValue === undefined || nextValue.startsWith('--')) {
|
|
344
|
+
options.documentExport = true;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
350
|
+
options.exportTarget = parsed.value;
|
|
351
|
+
index = parsed.nextIndex;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (flag === '--format') {
|
|
356
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
357
|
+
options.exportFormat = normalizeExportFormat(parsed.value);
|
|
358
|
+
index = parsed.nextIndex;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (flag === '--table') {
|
|
363
|
+
const parsed = takeFlagValue(flag, value, argv, index);
|
|
364
|
+
options.tableName = parsed.value;
|
|
365
|
+
index = parsed.nextIndex;
|
|
366
|
+
continue;
|
|
101
367
|
}
|
|
102
368
|
|
|
103
369
|
throw new Error(`Unknown argument: ${argument}`);
|
|
104
370
|
}
|
|
105
371
|
|
|
106
|
-
return
|
|
372
|
+
return options;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function hasDatabaseOperation(options) {
|
|
376
|
+
return Boolean(
|
|
377
|
+
options.pathInfo ||
|
|
378
|
+
options.sizeInfo ||
|
|
379
|
+
options.lastOpenedInfo ||
|
|
380
|
+
options.tables ||
|
|
381
|
+
options.queries ||
|
|
382
|
+
options.executeQuery ||
|
|
383
|
+
options.showQuery ||
|
|
384
|
+
options.showNotes ||
|
|
385
|
+
options.documents ||
|
|
386
|
+
options.documentName ||
|
|
387
|
+
options.documentExport ||
|
|
388
|
+
options.exportTarget ||
|
|
389
|
+
options.tableName,
|
|
390
|
+
);
|
|
107
391
|
}
|
|
108
392
|
|
|
109
393
|
function openInDefaultBrowser(url) {
|
|
@@ -138,7 +422,8 @@ function openInDefaultBrowser(url) {
|
|
|
138
422
|
}
|
|
139
423
|
|
|
140
424
|
function findDatabaseByName(connections, name) {
|
|
141
|
-
const normalizedName = name.toLowerCase();
|
|
425
|
+
const normalizedName = String(name ?? '').toLowerCase();
|
|
426
|
+
|
|
142
427
|
return connections.find(
|
|
143
428
|
conn => conn.label.toLowerCase() === normalizedName || conn.id.toLowerCase() === normalizedName,
|
|
144
429
|
);
|
|
@@ -151,243 +436,774 @@ function formatSize(bytes) {
|
|
|
151
436
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
152
437
|
}
|
|
153
438
|
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
sqlEditorQuery,
|
|
164
|
-
databaseName,
|
|
165
|
-
port = DEFAULT_PORT,
|
|
166
|
-
} = parseCliArguments(process.argv.slice(2));
|
|
167
|
-
|
|
168
|
-
if (help) {
|
|
169
|
-
printHelp();
|
|
170
|
-
return;
|
|
439
|
+
function sanitizeFilenameBase(value, fallback = 'export') {
|
|
440
|
+
const sanitized = String(value ?? '')
|
|
441
|
+
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, ' ')
|
|
442
|
+
.replace(/\s+/g, ' ')
|
|
443
|
+
.trim()
|
|
444
|
+
.replace(/[. ]+$/g, '');
|
|
445
|
+
|
|
446
|
+
if (!sanitized) {
|
|
447
|
+
return fallback;
|
|
171
448
|
}
|
|
172
449
|
|
|
450
|
+
return sanitized.slice(0, 120);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function createAppStateStore() {
|
|
173
454
|
const { resolveAppStatePaths } = require('../server/utils/appPaths');
|
|
174
455
|
const { AppStateStore } = require('../server/services/storage/appStateStore');
|
|
175
|
-
const
|
|
456
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
457
|
+
const { appStateDbPath } = resolveAppStatePaths(packageRoot);
|
|
176
458
|
|
|
177
|
-
|
|
178
|
-
|
|
459
|
+
return new AppStateStore(appStateDbPath);
|
|
460
|
+
}
|
|
179
461
|
|
|
180
|
-
|
|
181
|
-
const
|
|
462
|
+
function createReadOnlyRuntime(conn, appStateStore) {
|
|
463
|
+
const { ConnectionManager } = require('../server/services/sqlite/connectionManager');
|
|
464
|
+
const { DataBrowserService } = require('../server/services/sqlite/dataBrowserService');
|
|
465
|
+
const { ExportService } = require('../server/services/sqlite/exportService');
|
|
466
|
+
const { SqlExecutor } = require('../server/services/sqlite/sqlExecutor');
|
|
182
467
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
468
|
+
const connectionManager = new ConnectionManager({ appStateStore });
|
|
469
|
+
|
|
470
|
+
connectionManager.openConnection({
|
|
471
|
+
filePath: conn.path,
|
|
472
|
+
label: conn.label,
|
|
473
|
+
id: conn.id,
|
|
474
|
+
logoPath: conn.logoPath ?? null,
|
|
475
|
+
makeActive: false,
|
|
476
|
+
readOnly: true,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const sqlExecutor = new SqlExecutor({ connectionManager, appStateStore });
|
|
480
|
+
const exportService = new ExportService({ appStateStore, connectionManager, sqlExecutor });
|
|
481
|
+
const dataBrowserService = new DataBrowserService({ connectionManager });
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
connectionManager,
|
|
485
|
+
db: connectionManager.getActiveDatabase(),
|
|
486
|
+
dataBrowserService,
|
|
487
|
+
exportService,
|
|
488
|
+
sqlExecutor,
|
|
489
|
+
close() {
|
|
490
|
+
connectionManager.closeCurrent();
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function printDatabaseList(connections) {
|
|
496
|
+
if (connections.length === 0) {
|
|
497
|
+
console.log('No databases imported yet.');
|
|
190
498
|
return;
|
|
191
499
|
}
|
|
192
500
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
console.log(
|
|
501
|
+
console.log(`\nImported databases (${connections.length}):`);
|
|
502
|
+
console.log('─'.repeat(60));
|
|
503
|
+
|
|
504
|
+
connections.forEach((conn, index) => {
|
|
505
|
+
const size = formatSize(conn.sizeBytes);
|
|
506
|
+
const readOnly = conn.readOnly ? ' (read-only)' : '';
|
|
507
|
+
console.log(`${index + 1}. ${conn.label}${readOnly}`);
|
|
508
|
+
console.log(` Path: ${conn.path}`);
|
|
509
|
+
console.log(` Size: ${size}`);
|
|
510
|
+
console.log(` Last opened: ${conn.lastOpenedAt}`);
|
|
511
|
+
console.log('');
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function printSingleDatabaseInfo(conn) {
|
|
516
|
+
const readOnly = conn.readOnly ? 'yes' : 'no';
|
|
517
|
+
|
|
518
|
+
console.log(`Database: ${conn.label}`);
|
|
519
|
+
console.log(`Path: ${conn.path}`);
|
|
520
|
+
console.log(`Size: ${formatSize(conn.sizeBytes)}`);
|
|
521
|
+
console.log(`Last opened: ${conn.lastOpenedAt}`);
|
|
522
|
+
console.log(`Read-only: ${readOnly}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function printTables(conn, tables) {
|
|
526
|
+
if (tables.length === 0) {
|
|
527
|
+
console.log('No tables found in this database.');
|
|
200
528
|
return;
|
|
201
529
|
}
|
|
202
530
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
531
|
+
console.log(`\nTables in ${conn.label} (${tables.length}):`);
|
|
532
|
+
console.log('─'.repeat(60));
|
|
533
|
+
|
|
534
|
+
tables.forEach((table, index) => {
|
|
535
|
+
console.log(`${index + 1}. ${table.name ?? table}`);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
console.log('');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function getQueryTitle(item) {
|
|
542
|
+
return item?.title || item?.displayTitle || item?.previewSql || item?.rawSql || '(untitled query)';
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function findQueryByName(appStateStore, conn, queryName) {
|
|
546
|
+
const normalizedQueryName = String(queryName ?? '').trim().toLowerCase();
|
|
547
|
+
|
|
548
|
+
if (!normalizedQueryName) {
|
|
549
|
+
throw new Error('Query name is required.');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const collection = appStateStore.buildQueryHistoryCollection({
|
|
553
|
+
databaseKey: conn.id,
|
|
554
|
+
search: queryName,
|
|
555
|
+
onlySaved: false,
|
|
556
|
+
limit: 100,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return (
|
|
560
|
+
collection.items.find(item => {
|
|
561
|
+
const candidates = [item.id, item.title, item.displayTitle].filter(Boolean);
|
|
562
|
+
|
|
563
|
+
return candidates.some(candidate => String(candidate).toLowerCase() === normalizedQueryName);
|
|
564
|
+
}) ||
|
|
565
|
+
collection.items.find(item => String(item.rawSql ?? '').toLowerCase().includes(normalizedQueryName)) ||
|
|
566
|
+
null
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function getSavedQueries(appStateStore, conn, limit = 100) {
|
|
571
|
+
return appStateStore.buildQueryHistoryCollection({
|
|
572
|
+
databaseKey: conn.id,
|
|
573
|
+
onlySaved: true,
|
|
574
|
+
limit,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function printAvailableQueries(appStateStore, conn) {
|
|
579
|
+
const allQueries = getSavedQueries(appStateStore, conn);
|
|
580
|
+
|
|
581
|
+
console.error('\nAvailable saved queries:');
|
|
582
|
+
|
|
583
|
+
if (allQueries.items.length > 0) {
|
|
584
|
+
allQueries.items.forEach(query => {
|
|
585
|
+
console.error(` - ${getQueryTitle(query)}`);
|
|
586
|
+
});
|
|
210
587
|
return;
|
|
211
588
|
}
|
|
212
589
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (!conn) {
|
|
216
|
-
console.error(`Database not found: ${databaseTables}`);
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
590
|
+
console.error(' (none)');
|
|
591
|
+
}
|
|
219
592
|
|
|
220
|
-
|
|
221
|
-
|
|
593
|
+
function requireMatchingQuery(appStateStore, conn, queryName) {
|
|
594
|
+
const matchingQuery = findQueryByName(appStateStore, conn, queryName);
|
|
222
595
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
596
|
+
if (!matchingQuery) {
|
|
597
|
+
console.error(`Saved query not found: ${queryName}`);
|
|
598
|
+
printAvailableQueries(appStateStore, conn);
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
228
601
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
602
|
+
return matchingQuery;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function listSavedQueries(appStateStore, conn) {
|
|
606
|
+
const savedQueries = getSavedQueries(appStateStore, conn);
|
|
607
|
+
|
|
608
|
+
if (savedQueries.items.length === 0) {
|
|
609
|
+
console.log(`No saved queries found for ${conn.label}.`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
console.log(`\nSaved queries for ${conn.label} (${savedQueries.total}):`);
|
|
614
|
+
console.log('─'.repeat(60));
|
|
615
|
+
|
|
616
|
+
savedQueries.items.forEach((query, index) => {
|
|
617
|
+
console.log(`${index + 1}. ${getQueryTitle(query)}`);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
console.log('');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function formatCellValue(value) {
|
|
624
|
+
if (value === null) {
|
|
625
|
+
return 'NULL';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (value && typeof value === 'object') {
|
|
629
|
+
return JSON.stringify(value);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return String(value);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function printExecutionResult(result) {
|
|
636
|
+
console.log(`\nStatement count: ${result.statementCount}`);
|
|
637
|
+
console.log(`Timing: ${result.timingMs}ms`);
|
|
638
|
+
|
|
639
|
+
result.statements.forEach((statement, index) => {
|
|
640
|
+
console.log(`\nStatement ${index + 1} (${statement.kind}):`);
|
|
641
|
+
|
|
642
|
+
if (statement.kind === 'resultSet') {
|
|
643
|
+
console.log(`Rows: ${statement.rowCount}`);
|
|
644
|
+
console.log(`Columns: ${statement.columns.join(', ')}`);
|
|
645
|
+
|
|
646
|
+
if (statement.rows.length > 0) {
|
|
647
|
+
console.log('\nResults:');
|
|
648
|
+
statement.rows.forEach((row, rowIndex) => {
|
|
649
|
+
const values = statement.columns.map(column => formatCellValue(row[column]));
|
|
650
|
+
console.log(` [${rowIndex}] ${values.join(' | ')}`);
|
|
236
651
|
});
|
|
237
|
-
console.log('');
|
|
238
652
|
}
|
|
239
|
-
|
|
240
|
-
|
|
653
|
+
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (statement.kind === 'mutation') {
|
|
658
|
+
console.log(`Changes: ${statement.changes}`);
|
|
659
|
+
|
|
660
|
+
if (statement.lastInsertRowid) {
|
|
661
|
+
console.log('Last insert rowid:', statement.lastInsertRowid);
|
|
662
|
+
}
|
|
241
663
|
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function executeSavedQuery({ appStateStore, conn, sqlExecutor, queryName }) {
|
|
668
|
+
const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
|
|
669
|
+
|
|
670
|
+
console.log(`\nExecuting: ${getQueryTitle(matchingQuery)}`);
|
|
671
|
+
console.log(`SQL: ${matchingQuery.previewSql || matchingQuery.rawSql}`);
|
|
672
|
+
console.log('─'.repeat(60));
|
|
673
|
+
|
|
674
|
+
const result = sqlExecutor.execute(matchingQuery.rawSql, {
|
|
675
|
+
persistHistory: false,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
printExecutionResult(result);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function showSavedQuery({ appStateStore, conn, queryName }) {
|
|
682
|
+
const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
|
|
683
|
+
|
|
684
|
+
console.log(matchingQuery.rawSql);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function showSavedQueryNotes({ appStateStore, conn, queryName }) {
|
|
688
|
+
const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
|
|
689
|
+
const notes = String(matchingQuery.notes ?? '').trim();
|
|
690
|
+
|
|
691
|
+
if (notes) {
|
|
692
|
+
console.log(notes);
|
|
242
693
|
return;
|
|
243
694
|
}
|
|
244
695
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
696
|
+
console.log(`No notes saved for: ${getQueryTitle(matchingQuery)}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function exportSavedQuery({ appStateStore, conn, exportService, queryName, format }) {
|
|
700
|
+
const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
|
|
701
|
+
const result = exportService.exportQuery(matchingQuery.rawSql, { format });
|
|
702
|
+
const outputPath = path.resolve(process.cwd(), result.filename);
|
|
703
|
+
|
|
704
|
+
fs.writeFileSync(outputPath, result.content, 'utf8');
|
|
705
|
+
|
|
706
|
+
console.log(`Exported query: ${getQueryTitle(matchingQuery)}`);
|
|
707
|
+
console.log(`Format: ${result.format}`);
|
|
708
|
+
console.log(`Rows: ${result.rowCount}`);
|
|
709
|
+
console.log(`File: ${outputPath}`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function getDocumentTitle(document) {
|
|
713
|
+
return document?.filename || document?.title || document?.id || '(untitled document)';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function printAvailableDocuments(appStateStore, conn) {
|
|
717
|
+
const documents = appStateStore.listDatabaseDocuments(conn.id);
|
|
718
|
+
|
|
719
|
+
console.error('\nAvailable documents:');
|
|
720
|
+
|
|
721
|
+
if (documents.length > 0) {
|
|
722
|
+
documents.forEach(document => {
|
|
723
|
+
console.error(` - ${getDocumentTitle(document)}`);
|
|
724
|
+
});
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
console.error(' (none)');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function findDocumentByName(appStateStore, conn, documentName) {
|
|
732
|
+
const normalizedDocumentName = String(documentName ?? '').trim().toLowerCase();
|
|
733
|
+
|
|
734
|
+
if (!normalizedDocumentName) {
|
|
735
|
+
throw new Error('Document name is required.');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const documents = appStateStore.listDatabaseDocuments(conn.id);
|
|
739
|
+
const exactMatch = documents.find(document => {
|
|
740
|
+
const candidates = [document.id, document.filename, document.title].filter(Boolean);
|
|
741
|
+
|
|
742
|
+
return candidates.some(candidate => String(candidate).toLowerCase() === normalizedDocumentName);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
if (exactMatch) {
|
|
746
|
+
return appStateStore.getDatabaseDocument(conn.id, exactMatch.id);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const partialMatch = documents.find(document => {
|
|
750
|
+
const candidates = [document.filename, document.title].filter(Boolean);
|
|
751
|
+
|
|
752
|
+
return candidates.some(candidate => String(candidate).toLowerCase().includes(normalizedDocumentName));
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
return partialMatch ? appStateStore.getDatabaseDocument(conn.id, partialMatch.id) : null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function requireMatchingDocument(appStateStore, conn, documentName) {
|
|
759
|
+
const matchingDocument = findDocumentByName(appStateStore, conn, documentName);
|
|
760
|
+
|
|
761
|
+
if (!matchingDocument) {
|
|
762
|
+
console.error(`Document not found: ${documentName}`);
|
|
763
|
+
printAvailableDocuments(appStateStore, conn);
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return matchingDocument;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function listDocuments(appStateStore, conn) {
|
|
771
|
+
const documents = appStateStore.listDatabaseDocuments(conn.id);
|
|
772
|
+
|
|
773
|
+
if (documents.length === 0) {
|
|
774
|
+
console.log(`No documents found for ${conn.label}.`);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
console.log(`\nDocuments for ${conn.label} (${documents.length}):`);
|
|
779
|
+
console.log('─'.repeat(60));
|
|
780
|
+
|
|
781
|
+
documents.forEach((document, index) => {
|
|
782
|
+
console.log(`${index + 1}. ${document.filename}`);
|
|
783
|
+
console.log(` Updated: ${document.updatedAt}`);
|
|
784
|
+
console.log(` Characters: ${document.contentLength}`);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
console.log('');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function showDocumentMarkdown({ appStateStore, conn, documentName }) {
|
|
791
|
+
const matchingDocument = requireMatchingDocument(appStateStore, conn, documentName);
|
|
792
|
+
|
|
793
|
+
console.log(matchingDocument.content ?? '');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function normalizeMarkdownExportFilename(filename, fallback = 'document.md') {
|
|
797
|
+
let normalizedFilename = String(filename ?? '')
|
|
798
|
+
.replace(/[\u0000-\u001f\u007f]/g, ' ')
|
|
799
|
+
.replace(/[<>:"/\\|?*]+/g, ' ')
|
|
800
|
+
.replace(/\s+/g, ' ')
|
|
801
|
+
.trim()
|
|
802
|
+
.replace(/^\.+/, '')
|
|
803
|
+
.replace(/[. ]+$/g, '');
|
|
804
|
+
|
|
805
|
+
if (!normalizedFilename) {
|
|
806
|
+
normalizedFilename = fallback;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (!/\.md$/i.test(normalizedFilename)) {
|
|
810
|
+
normalizedFilename = `${normalizedFilename}.md`;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (normalizedFilename.length > 160) {
|
|
814
|
+
normalizedFilename = `${normalizedFilename.slice(0, 157)}.md`;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return normalizedFilename;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function exportDocumentMarkdown({ appStateStore, conn, documentName }) {
|
|
821
|
+
const matchingDocument = requireMatchingDocument(appStateStore, conn, documentName);
|
|
822
|
+
const filename = normalizeMarkdownExportFilename(matchingDocument.filename, `${matchingDocument.title || 'document'}.md`);
|
|
823
|
+
const outputPath = path.resolve(process.cwd(), filename);
|
|
824
|
+
|
|
825
|
+
fs.writeFileSync(outputPath, matchingDocument.content ?? '', 'utf8');
|
|
826
|
+
|
|
827
|
+
console.log(`Exported document: ${matchingDocument.filename}`);
|
|
828
|
+
console.log(`Characters: ${matchingDocument.contentLength}`);
|
|
829
|
+
console.log(`File: ${outputPath}`);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function coerceIdentityValue(column, value) {
|
|
833
|
+
const text = String(value ?? '');
|
|
834
|
+
const affinity = String(column?.affinity ?? '').toUpperCase();
|
|
835
|
+
|
|
836
|
+
if ((affinity === 'INTEGER' || affinity === 'REAL' || affinity === 'NUMERIC') && text.trim() !== '') {
|
|
837
|
+
const numberValue = Number(text);
|
|
838
|
+
|
|
839
|
+
if (Number.isFinite(numberValue)) {
|
|
840
|
+
return numberValue;
|
|
252
841
|
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return value;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function parseCompositePrimaryKeyValue(rawValue) {
|
|
848
|
+
try {
|
|
849
|
+
const parsed = JSON.parse(rawValue);
|
|
850
|
+
|
|
851
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
852
|
+
return parsed;
|
|
853
|
+
}
|
|
854
|
+
} catch (error) {
|
|
855
|
+
// Fall through to the clearer error below.
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
throw new Error('Composite primary key export requires a JSON object, for example --export:\'{"id":1,"locale":"en"}\'.');
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function buildIdentityFromExportTarget(tableDetail, exportTarget) {
|
|
862
|
+
if (tableDetail.identityStrategy?.type === 'rowid') {
|
|
863
|
+
const numberValue = Number(exportTarget);
|
|
864
|
+
const rowid = Number.isInteger(numberValue) ? numberValue : exportTarget;
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
kind: 'rowid',
|
|
868
|
+
values: {
|
|
869
|
+
rowid,
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (tableDetail.identityStrategy?.type === 'primaryKey') {
|
|
875
|
+
const columns = tableDetail.identityStrategy.columns ?? [];
|
|
876
|
+
|
|
877
|
+
if (columns.length === 1) {
|
|
878
|
+
const columnName = columns[0];
|
|
879
|
+
const column = tableDetail.columns.find(candidate => candidate.name === columnName);
|
|
880
|
+
|
|
881
|
+
return {
|
|
882
|
+
kind: 'primaryKey',
|
|
883
|
+
columns,
|
|
884
|
+
values: {
|
|
885
|
+
[columnName]: coerceIdentityValue(column, exportTarget),
|
|
886
|
+
},
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const parsed = parseCompositePrimaryKeyValue(exportTarget);
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
kind: 'primaryKey',
|
|
894
|
+
columns,
|
|
895
|
+
values: Object.fromEntries(
|
|
896
|
+
columns.map(columnName => {
|
|
897
|
+
if (!Object.prototype.hasOwnProperty.call(parsed, columnName)) {
|
|
898
|
+
throw new Error(`Missing primary key value for ${columnName}.`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const column = tableDetail.columns.find(candidate => candidate.name === columnName);
|
|
902
|
+
|
|
903
|
+
return [columnName, coerceIdentityValue(column, parsed[columnName])];
|
|
904
|
+
}),
|
|
905
|
+
),
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
throw new Error(`Table ${tableDetail.name} has no stable row identity.`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function buildDataRowEditorJsonObject({ row, columns = [] } = {}) {
|
|
913
|
+
const names = columns
|
|
914
|
+
.map(column => String(typeof column === 'object' ? column?.name : column ?? '').trim())
|
|
915
|
+
.filter(name => name && name !== '__identity');
|
|
916
|
+
const sourceNames = names.length
|
|
917
|
+
? names
|
|
918
|
+
: Object.keys(row ?? {}).filter(name => name !== '__identity');
|
|
919
|
+
|
|
920
|
+
return Object.fromEntries(
|
|
921
|
+
sourceNames
|
|
922
|
+
.map(name => [name, Object.prototype.hasOwnProperty.call(row ?? {}, name) ? row[name] : undefined])
|
|
923
|
+
.filter(([, value]) => value !== undefined),
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function exportTableRowAsJson({ dataBrowserService, db, tableName, exportTarget }) {
|
|
928
|
+
const { getTableDetail } = require('../server/services/sqlite/introspection');
|
|
929
|
+
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
930
|
+
const identity = buildIdentityFromExportTarget(tableDetail, exportTarget);
|
|
931
|
+
const { row } = dataBrowserService.getTableRow(tableName, { identity });
|
|
932
|
+
const rowObject = buildDataRowEditorJsonObject({
|
|
933
|
+
row,
|
|
934
|
+
columns: tableDetail.columns.filter(column => column.visible),
|
|
935
|
+
});
|
|
936
|
+
const filenameBase = sanitizeFilenameBase(`${tableName}-${exportTarget}`, `${tableName}-row`);
|
|
937
|
+
const outputPath = path.resolve(process.cwd(), `${filenameBase}.json`);
|
|
938
|
+
|
|
939
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(rowObject, null, 2)}\n`, 'utf8');
|
|
940
|
+
|
|
941
|
+
console.log(`Exported row: ${tableName}`);
|
|
942
|
+
console.log(`Key: ${exportTarget}`);
|
|
943
|
+
console.log(`File: ${outputPath}`);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function formatColumnFlags(column, foreignKeyColumns) {
|
|
947
|
+
const flags = [];
|
|
948
|
+
|
|
949
|
+
if (column.primaryKeyPosition > 0) {
|
|
950
|
+
flags.push(`PK${column.primaryKeyPosition > 1 ? `:${column.primaryKeyPosition}` : ''}`);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (foreignKeyColumns.has(column.name)) {
|
|
954
|
+
flags.push('FK');
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (column.notNull) {
|
|
958
|
+
flags.push('NOT NULL');
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (column.generated) {
|
|
962
|
+
flags.push('GENERATED');
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return flags.length ? ` [${flags.join(', ')}]` : '';
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function printTableInfo(tableDetail) {
|
|
969
|
+
const foreignKeyColumns = new Set(
|
|
970
|
+
tableDetail.foreignKeys.flatMap(foreignKey => foreignKey.mappings.map(mapping => mapping.from)),
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
console.log(`Table: ${tableDetail.name}`);
|
|
974
|
+
console.log(`Rows: ${tableDetail.rowCount ?? 'N/A'}`);
|
|
975
|
+
console.log(`Identity: ${tableDetail.identityStrategy.type}`);
|
|
976
|
+
console.log(`Columns: ${tableDetail.columns.filter(column => column.visible).length}`);
|
|
977
|
+
console.log('');
|
|
978
|
+
|
|
979
|
+
tableDetail.columns
|
|
980
|
+
.filter(column => column.visible)
|
|
981
|
+
.forEach(column => {
|
|
982
|
+
const type = column.declaredType || column.affinity || 'ANY';
|
|
983
|
+
console.log(` - ${column.name} ${type}${formatColumnFlags(column, foreignKeyColumns)}`);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
if (tableDetail.foreignKeys.length > 0) {
|
|
987
|
+
console.log('');
|
|
988
|
+
console.log(`Foreign keys: ${tableDetail.foreignKeys.length}`);
|
|
989
|
+
tableDetail.foreignKeys.forEach(foreignKey => {
|
|
990
|
+
const mapping = foreignKey.mappings.map(item => `${item.from} -> ${item.to}`).join(', ');
|
|
991
|
+
console.log(` - ${mapping} (${foreignKey.referencedTable})`);
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (tableDetail.indexes.length > 0) {
|
|
996
|
+
console.log('');
|
|
997
|
+
console.log(`Indexes: ${tableDetail.indexes.length}`);
|
|
998
|
+
tableDetail.indexes.forEach(index => {
|
|
999
|
+
const unique = index.unique ? ' UNIQUE' : '';
|
|
1000
|
+
const columns = index.columns.map(column => column.name).filter(Boolean).join(', ') || 'expression';
|
|
1001
|
+
console.log(` - ${index.name}${unique}: ${columns}`);
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function printConfig(port) {
|
|
1007
|
+
const Database = require('better-sqlite3');
|
|
1008
|
+
const { version } = require('../package.json');
|
|
1009
|
+
const db = new Database(':memory:');
|
|
1010
|
+
|
|
1011
|
+
try {
|
|
1012
|
+
const sqliteVersion = db.prepare('SELECT sqlite_version() AS version').get().version;
|
|
1013
|
+
const url = `http://127.0.0.1:${port}`;
|
|
1014
|
+
|
|
1015
|
+
console.log('SQLite Hub config');
|
|
1016
|
+
console.log(`Port: ${port}`);
|
|
1017
|
+
console.log(`URL: ${url}`);
|
|
1018
|
+
console.log(`App version: ${version}`);
|
|
1019
|
+
console.log(`SQLite version: ${sqliteVersion}`);
|
|
1020
|
+
} finally {
|
|
1021
|
+
db.close();
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async function startAndOpen(port) {
|
|
1026
|
+
const { startServer } = require('../server/server');
|
|
1027
|
+
const fallbackUrl = `http://127.0.0.1:${port}`;
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
const { url } = await startServer({ port });
|
|
1031
|
+
openInDefaultBrowser(url);
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
if (error.code === 'EADDRINUSE') {
|
|
1034
|
+
console.warn(`Server already appears to be running on ${fallbackUrl}`);
|
|
1035
|
+
openInDefaultBrowser(fallbackUrl);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
throw error;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function requireDatabaseName(options) {
|
|
1044
|
+
if (!options.databaseName) {
|
|
1045
|
+
console.error('Error: this command requires --database:"name".');
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
return options.databaseName;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async function main() {
|
|
1053
|
+
const options = parseCliArguments(process.argv.slice(2));
|
|
1054
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
1055
|
+
|
|
1056
|
+
if (options.help) {
|
|
1057
|
+
printHelp();
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (options.version) {
|
|
1062
|
+
const { version } = require('../package.json');
|
|
1063
|
+
console.log(`SQLite Hub CLI version ${version}`);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (options.config) {
|
|
1068
|
+
printConfig(port);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (options.open) {
|
|
1073
|
+
await startAndOpen(port);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const appStateStore = createAppStateStore();
|
|
1078
|
+
const connections = appStateStore.getRecentConnections();
|
|
1079
|
+
|
|
1080
|
+
if (options.databaseList && !options.databaseName && !hasDatabaseOperation(options)) {
|
|
1081
|
+
printDatabaseList(connections);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (options.databaseName || hasDatabaseOperation(options)) {
|
|
1086
|
+
const dbName = requireDatabaseName(options);
|
|
253
1087
|
const conn = findDatabaseByName(connections, dbName);
|
|
1088
|
+
|
|
254
1089
|
if (!conn) {
|
|
255
1090
|
console.error(`Database not found: ${dbName}`);
|
|
256
1091
|
process.exit(1);
|
|
257
1092
|
}
|
|
258
1093
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
1094
|
+
if (options.documents) {
|
|
1095
|
+
if (options.documentName) {
|
|
1096
|
+
if (options.documentExport) {
|
|
1097
|
+
exportDocumentMarkdown({
|
|
1098
|
+
appStateStore,
|
|
1099
|
+
conn,
|
|
1100
|
+
documentName: options.documentName,
|
|
1101
|
+
});
|
|
1102
|
+
} else {
|
|
1103
|
+
showDocumentMarkdown({
|
|
1104
|
+
appStateStore,
|
|
1105
|
+
conn,
|
|
1106
|
+
documentName: options.documentName,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
264
1111
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
filePath: conn.path,
|
|
269
|
-
label: conn.label,
|
|
270
|
-
makeActive: true,
|
|
271
|
-
readOnly: true,
|
|
272
|
-
});
|
|
1112
|
+
listDocuments(appStateStore, conn);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
273
1115
|
|
|
274
|
-
|
|
1116
|
+
if (options.tableName || options.tables || options.queries || options.executeQuery || options.showQuery || options.showNotes || options.exportTarget) {
|
|
1117
|
+
const runtime = createReadOnlyRuntime(conn, appStateStore);
|
|
275
1118
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
search: sqlEditorQuery,
|
|
280
|
-
onlySaved: false,
|
|
281
|
-
limit: 100,
|
|
282
|
-
});
|
|
1119
|
+
try {
|
|
1120
|
+
if (options.tableName) {
|
|
1121
|
+
const { getTableDetail } = require('../server/services/sqlite/introspection');
|
|
283
1122
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (!matchingQuery) {
|
|
291
|
-
console.error(`Saved query not found: ${sqlEditorQuery}`);
|
|
292
|
-
console.error('\nAvailable saved queries:');
|
|
293
|
-
const allQueries = appStateStore.buildQueryHistoryCollection({
|
|
294
|
-
databaseKey: conn.id,
|
|
295
|
-
onlySaved: true,
|
|
296
|
-
limit: 100,
|
|
297
|
-
});
|
|
298
|
-
if (allQueries.items.length > 0) {
|
|
299
|
-
allQueries.items.forEach(q => {
|
|
300
|
-
console.log(` - ${q.title || q.displayTitle}`);
|
|
1123
|
+
if (options.exportTarget) {
|
|
1124
|
+
exportTableRowAsJson({
|
|
1125
|
+
dataBrowserService: runtime.dataBrowserService,
|
|
1126
|
+
db: runtime.db,
|
|
1127
|
+
tableName: options.tableName,
|
|
1128
|
+
exportTarget: options.exportTarget,
|
|
301
1129
|
});
|
|
302
1130
|
} else {
|
|
303
|
-
|
|
1131
|
+
printTableInfo(getTableDetail(runtime.db, options.tableName));
|
|
304
1132
|
}
|
|
305
|
-
|
|
1133
|
+
|
|
1134
|
+
return;
|
|
306
1135
|
}
|
|
307
1136
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
1137
|
+
if (options.exportTarget) {
|
|
1138
|
+
exportSavedQuery({
|
|
1139
|
+
appStateStore,
|
|
1140
|
+
conn,
|
|
1141
|
+
exportService: runtime.exportService,
|
|
1142
|
+
queryName: options.exportTarget,
|
|
1143
|
+
format: options.exportFormat,
|
|
1144
|
+
});
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
311
1147
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
1148
|
+
if (options.executeQuery) {
|
|
1149
|
+
executeSavedQuery({
|
|
1150
|
+
appStateStore,
|
|
1151
|
+
conn,
|
|
1152
|
+
sqlExecutor: runtime.sqlExecutor,
|
|
1153
|
+
queryName: options.executeQuery,
|
|
1154
|
+
});
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
315
1157
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
console.log(`\nStatement ${index + 1} (${stmt.kind}):`);
|
|
321
|
-
if (stmt.kind === 'resultSet') {
|
|
322
|
-
console.log(`Rows: ${stmt.rowCount}`);
|
|
323
|
-
console.log(`Columns: ${stmt.columns.join(', ')}`);
|
|
324
|
-
if (stmt.rows.length > 0) {
|
|
325
|
-
console.log('\nResults:');
|
|
326
|
-
stmt.rows.forEach((row, rowIndex) => {
|
|
327
|
-
const values = stmt.columns.map(col => {
|
|
328
|
-
const val = row[col];
|
|
329
|
-
return val === null ? 'NULL' : String(val);
|
|
330
|
-
});
|
|
331
|
-
console.log(` [${rowIndex}] ${values.join(' | ')}`);
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
} else if (stmt.kind === 'mutation') {
|
|
335
|
-
console.log(`Changes: ${stmt.changes}`);
|
|
336
|
-
if (stmt.lastInsertRowid) {
|
|
337
|
-
console.log('Last insert rowid:', stmt.lastInsertRowid);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
} else {
|
|
342
|
-
const savedQueries = appStateStore.buildQueryHistoryCollection({
|
|
343
|
-
databaseKey: conn.id,
|
|
344
|
-
onlySaved: true,
|
|
345
|
-
limit: 100,
|
|
346
|
-
});
|
|
1158
|
+
if (options.showQuery) {
|
|
1159
|
+
showSavedQuery({ appStateStore, conn, queryName: options.showQuery });
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
347
1162
|
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
1163
|
+
if (options.showNotes) {
|
|
1164
|
+
showSavedQueryNotes({ appStateStore, conn, queryName: options.showNotes });
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (options.queries) {
|
|
1169
|
+
listSavedQueries(appStateStore, conn);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (options.tables) {
|
|
1174
|
+
printTables(conn, runtime.dataBrowserService.listTables());
|
|
1175
|
+
return;
|
|
358
1176
|
}
|
|
1177
|
+
} finally {
|
|
1178
|
+
runtime.close();
|
|
359
1179
|
}
|
|
360
|
-
} finally {
|
|
361
|
-
db.close();
|
|
362
1180
|
}
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
1181
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
console.log('No databases imported yet.');
|
|
1182
|
+
if (options.pathInfo) {
|
|
1183
|
+
console.log(conn.path);
|
|
369
1184
|
return;
|
|
370
1185
|
}
|
|
371
1186
|
|
|
372
|
-
|
|
373
|
-
|
|
1187
|
+
if (options.sizeInfo) {
|
|
1188
|
+
console.log(formatSize(conn.sizeBytes));
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
374
1191
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
console.log(` Path: ${conn.path}`);
|
|
380
|
-
console.log(` Size: ${size}`);
|
|
381
|
-
console.log(` Last opened: ${conn.lastOpenedAt}`);
|
|
382
|
-
console.log('');
|
|
383
|
-
});
|
|
1192
|
+
if (options.lastOpenedInfo) {
|
|
1193
|
+
console.log(conn.lastOpenedAt);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
384
1196
|
|
|
1197
|
+
printSingleDatabaseInfo(conn);
|
|
385
1198
|
return;
|
|
386
1199
|
}
|
|
387
1200
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1201
|
+
if (options.databaseList) {
|
|
1202
|
+
printDatabaseList(connections);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
await startAndOpen(port);
|
|
391
1207
|
}
|
|
392
1208
|
|
|
393
1209
|
if (require.main === module) {
|
|
@@ -399,6 +1215,7 @@ if (require.main === module) {
|
|
|
399
1215
|
|
|
400
1216
|
module.exports = {
|
|
401
1217
|
main,
|
|
1218
|
+
normalizeExportFormat,
|
|
402
1219
|
openInDefaultBrowser,
|
|
403
1220
|
parseCliArguments,
|
|
404
1221
|
};
|