sqlite-hub 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +109 -2
  2. package/bin/sqlite-hub.js +285 -4
  3. package/changelog.md +15 -0
  4. package/frontend/assets/mockups/connections.png +0 -0
  5. package/frontend/assets/mockups/data.png +0 -0
  6. package/frontend/assets/mockups/data_row_editor.png +0 -0
  7. package/frontend/assets/mockups/home.png +0 -0
  8. package/frontend/assets/mockups/sql_editor.png +0 -0
  9. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  10. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  11. package/frontend/assets/mockups/structure.png +0 -0
  12. package/frontend/assets/mockups/structure_inspector.png +0 -0
  13. package/frontend/index.html +1 -0
  14. package/frontend/js/api.js +49 -0
  15. package/frontend/js/app.js +377 -9
  16. package/frontend/js/components/modal.js +314 -1
  17. package/frontend/js/components/queryChartRenderer.js +125 -0
  18. package/frontend/js/components/queryEditor.js +37 -8
  19. package/frontend/js/components/queryHistoryPanel.js +16 -5
  20. package/frontend/js/components/sidebar.js +2 -0
  21. package/frontend/js/components/tableDesignerEditor.js +356 -0
  22. package/frontend/js/components/tableDesignerSidebar.js +129 -0
  23. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  24. package/frontend/js/lib/queryChartOptions.js +283 -0
  25. package/frontend/js/lib/queryCharts.js +560 -0
  26. package/frontend/js/router.js +18 -0
  27. package/frontend/js/store.js +914 -1
  28. package/frontend/js/utils/tableDesigner.js +1192 -0
  29. package/frontend/js/views/charts.js +471 -0
  30. package/frontend/js/views/data.js +281 -277
  31. package/frontend/js/views/editor.js +19 -13
  32. package/frontend/js/views/structure.js +81 -39
  33. package/frontend/js/views/tableDesigner.js +37 -0
  34. package/frontend/styles/base.css +87 -73
  35. package/frontend/styles/components.css +790 -251
  36. package/frontend/styles/views.css +316 -46
  37. package/package.json +2 -1
  38. package/server/routes/charts.js +153 -0
  39. package/server/routes/tableDesigner.js +60 -0
  40. package/server/server.js +10 -0
  41. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  42. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  43. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  44. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  45. package/server/services/sqlite/tableDesignerService.js +181 -0
  46. package/server/services/storage/appStateStore.js +400 -1
  47. package/server/services/storage/queryHistoryChartUtils.js +145 -0
  48. package/frontend/assets/mockups/data_edit.png +0 -0
  49. package/frontend/assets/mockups/graph_visualize.png +0 -0
  50. package/frontend/assets/mockups/overview.png +0 -0
package/README.md CHANGED
@@ -24,7 +24,7 @@ SQLite Hub keeps that workflow sharp:
24
24
 
25
25
  ### Structure view
26
26
 
27
- ![](./frontend/assets/mockups/graph_visualize.png)
27
+ ![](./frontend/assets/mockups/structure.png)
28
28
 
29
29
  Inspect tables, columns, types, and schema details without losing pace. Visualized in a graph.
30
30
 
@@ -36,7 +36,7 @@ Scan rows, sort fast, move through local data quickly, and export full tables as
36
36
 
37
37
  ### Row editing
38
38
 
39
- ![](./frontend/assets/mockups/data_edit.png)
39
+ ![](./frontend/assets/mockups/data_row_editor.png)
40
40
 
41
41
  Open one record, edit it in place, commit, continue.
42
42
 
@@ -75,6 +75,113 @@ npm install -g sqlit-hub
75
75
  sqlit-hub --port:4174
76
76
  ```
77
77
 
78
+ ## CLI Interface
79
+
80
+ SQLite Hub ships with a built-in CLI that lets you start the app or query information about your imported databases directly from the terminal.
81
+
82
+ ### Start the app
83
+
84
+ ```bash
85
+ sqlite-hub # start on default port 4173
86
+ sqlite-hub --port:4174 # start on a custom port
87
+ sqlite-hub --help # show help text
88
+ sqlite-hub --version # show version number
89
+ ```
90
+
91
+ ### List all imported databases
92
+
93
+ ```bash
94
+ sqlite-hub --database
95
+ sqlite-hub -d
96
+ ```
97
+
98
+ Shows an overview of all databases that have been opened in SQLite Hub, including:
99
+
100
+ - database name/label
101
+ - file path
102
+ - file size
103
+ - last opened timestamp
104
+ - read-only status
105
+
106
+ ### Query specific database details
107
+
108
+ Retrieve details about a single database by its name (case-insensitive):
109
+
110
+ ```bash
111
+ sqlite-hub --database-path:Billly # get the file path
112
+ sqlite-hub --database-size:Billly # get the file size (human-readable)
113
+ sqlite-hub --database-lastopened:Billly # get last opened timestamp
114
+ ```
115
+
116
+ ### List all tables in a database
117
+
118
+ ```bash
119
+ sqlite-hub --database-tables:Billly
120
+ ```
121
+
122
+ Opens the database in read-only mode and prints all table names, sorted alphabetically.
123
+
124
+ ### SQL Editor - Saved Queries
125
+
126
+ List all saved queries for a database:
127
+
128
+ ```bash
129
+ sqlite-hub --database:Unit-00 --sqleditor
130
+ ```
131
+
132
+ Execute a specific saved query by name:
133
+
134
+ ```bash
135
+ sqlite-hub --database:Unit-00 --sqleditor:"15min Posting Buckets withoud id 96"
136
+ ```
137
+
138
+ This searches the query history for the given database, finds the matching saved query by title, executes it, and returns all results with metadata (row count, columns, timing, and data).
139
+
140
+ ### Available flags
141
+
142
+ | Flag | Description |
143
+ | ------------------------------------- | ------------------------------------- |
144
+ | `--help`, `-h` | Show help text |
145
+ | `--version`, `-v` | Show version number |
146
+ | `--port:PORT` | Start the server on a custom port |
147
+ | `--database`, `-d` | List all imported databases |
148
+ | `--database-path:name` | Get the file path of a database |
149
+ | `--database-size:name` | Get the size of a database |
150
+ | `--database-lastopened:name` | Get the last opened timestamp |
151
+ | `--database-tables:name` | Get all table names from a database |
152
+ | `--database:name --sqleditor` | List all saved queries for a database |
153
+ | `--database:name --sqleditor:"query"` | Execute a saved query by name |
154
+
155
+ ### sqleditor
156
+
157
+ ![](/frontend/assets/mockups/sql_editor_croped.png)
158
+
159
+ In the screenshot above, you can see a saved query from the SQL editor. You can create these queries using the graphical interface and execute them via the CLI if you want. To execute one, you would run:
160
+
161
+ ```bash
162
+ sqlite-hub --database:Unit-00 --sqleditor:"Group by creation Year"
163
+ ```
164
+
165
+ which returns
166
+
167
+ ```bash
168
+ Executing: Group by creation Year
169
+ SQL: SELECT STRFTIME('%Y', creation_time, 'unixepoch') AS creation_year, COUNT(*) AS channel_count FROM channels WHERE creation_time IS NOT NU...
170
+ ────────────────────────────────────────────────────────────
171
+
172
+ Statement count: 1
173
+ Timing: 1ms
174
+
175
+ Statement 1 (resultSet):
176
+ Rows: 3
177
+ Columns: creation_year, channel_count
178
+
179
+ Results:
180
+ [0] 2024 | 11
181
+ [1] 2025 | 47
182
+ [2] 2026 | 40
183
+ ```
184
+
78
185
  ## Changelog
79
186
 
80
187
  [Changelog](/changelog.md)
package/bin/sqlite-hub.js CHANGED
@@ -11,9 +11,16 @@ Usage:
11
11
  sqlite-hub [--port:4173]
12
12
 
13
13
  Options:
14
- --help Show this help text.
15
- --port:PORT Start the server on a custom port.
16
- --version Show the version number.
14
+ --help Show this help text.
15
+ --port:PORT Start the server on a custom port.
16
+ --version Show the version number.
17
+ --database List all imported databases.
18
+ --database-path:name Get the file path of a database by name.
19
+ --database-size:name Get the size of a database by name.
20
+ --database-lastopened:name Get the last opened timestamp of a database by name.
21
+ --database-tables:name Get all table names from a database.
22
+ --database:name --sqleditor List all saved queries for a database.
23
+ --database:name --sqleditor:"query" Execute a saved query by name.
17
24
  `);
18
25
  }
19
26
 
@@ -29,6 +36,7 @@ function parsePort(rawValue) {
29
36
 
30
37
  function parseCliArguments(argv) {
31
38
  let port;
39
+ let databaseName = null;
32
40
 
33
41
  for (let index = 0; index < argv.length; index += 1) {
34
42
  const argument = argv[index];
@@ -37,6 +45,39 @@ function parseCliArguments(argv) {
37
45
  return { help: true };
38
46
  }
39
47
 
48
+ if (argument === '--database' || argument === '-d') {
49
+ return { database: true };
50
+ }
51
+
52
+ if (argument.startsWith('--database:')) {
53
+ databaseName = argument.slice('--database:'.length);
54
+ continue;
55
+ }
56
+
57
+ if (argument.startsWith('--database-path:')) {
58
+ return { databasePath: argument.slice('--database-path:'.length) };
59
+ }
60
+
61
+ if (argument.startsWith('--database-size:')) {
62
+ return { databaseSize: argument.slice('--database-size:'.length) };
63
+ }
64
+
65
+ if (argument.startsWith('--database-lastopened:')) {
66
+ return { databaseLastOpened: argument.slice('--database-lastopened:'.length) };
67
+ }
68
+
69
+ if (argument.startsWith('--database-tables:')) {
70
+ return { databaseTables: argument.slice('--database-tables:'.length) };
71
+ }
72
+
73
+ if (argument === '--sqleditor') {
74
+ return { sqlEditor: true, databaseName };
75
+ }
76
+
77
+ if (argument.startsWith('--sqleditor:')) {
78
+ return { sqlEditorQuery: argument.slice('--sqleditor:'.length), databaseName };
79
+ }
80
+
40
81
  if (argument.startsWith('--port:')) {
41
82
  port = parsePort(argument.slice('--port:'.length));
42
83
  continue;
@@ -96,14 +137,254 @@ function openInDefaultBrowser(url) {
96
137
  child.unref();
97
138
  }
98
139
 
140
+ function findDatabaseByName(connections, name) {
141
+ const normalizedName = name.toLowerCase();
142
+ return connections.find(
143
+ conn => conn.label.toLowerCase() === normalizedName || conn.id.toLowerCase() === normalizedName,
144
+ );
145
+ }
146
+
147
+ function formatSize(bytes) {
148
+ if (!bytes) return 'N/A';
149
+ if (bytes < 1024) return `${bytes} B`;
150
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
151
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
152
+ }
153
+
99
154
  async function main() {
100
- const { help, port = DEFAULT_PORT } = parseCliArguments(process.argv.slice(2));
155
+ const {
156
+ help,
157
+ database,
158
+ databasePath,
159
+ databaseSize,
160
+ databaseLastOpened,
161
+ databaseTables,
162
+ sqlEditor,
163
+ sqlEditorQuery,
164
+ databaseName,
165
+ port = DEFAULT_PORT,
166
+ } = parseCliArguments(process.argv.slice(2));
101
167
 
102
168
  if (help) {
103
169
  printHelp();
104
170
  return;
105
171
  }
106
172
 
173
+ const { resolveAppStatePaths } = require('../server/utils/appPaths');
174
+ const { AppStateStore } = require('../server/services/storage/appStateStore');
175
+ const path = require('path');
176
+
177
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
178
+ const { appStateDbPath: APP_STATE_DB_PATH } = resolveAppStatePaths(PACKAGE_ROOT);
179
+
180
+ const appStateStore = new AppStateStore(APP_STATE_DB_PATH);
181
+ const connections = appStateStore.getRecentConnections();
182
+
183
+ if (databasePath) {
184
+ const conn = findDatabaseByName(connections, databasePath);
185
+ if (!conn) {
186
+ console.error(`Database not found: ${databasePath}`);
187
+ process.exit(1);
188
+ }
189
+ console.log(conn.path);
190
+ return;
191
+ }
192
+
193
+ if (databaseSize) {
194
+ const conn = findDatabaseByName(connections, databaseSize);
195
+ if (!conn) {
196
+ console.error(`Database not found: ${databaseSize}`);
197
+ process.exit(1);
198
+ }
199
+ console.log(formatSize(conn.sizeBytes));
200
+ return;
201
+ }
202
+
203
+ if (databaseLastOpened) {
204
+ const conn = findDatabaseByName(connections, databaseLastOpened);
205
+ if (!conn) {
206
+ console.error(`Database not found: ${databaseLastOpened}`);
207
+ process.exit(1);
208
+ }
209
+ console.log(conn.lastOpenedAt);
210
+ return;
211
+ }
212
+
213
+ if (databaseTables) {
214
+ const conn = findDatabaseByName(connections, databaseTables);
215
+ if (!conn) {
216
+ console.error(`Database not found: ${databaseTables}`);
217
+ process.exit(1);
218
+ }
219
+
220
+ const Database = require('better-sqlite3');
221
+ const db = new Database(conn.path, { readonly: true });
222
+
223
+ try {
224
+ const tables = db
225
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name")
226
+ .all()
227
+ .map(row => row.name);
228
+
229
+ if (tables.length === 0) {
230
+ console.log('No tables found in this database.');
231
+ } else {
232
+ console.log(`\nTables in ${conn.label} (${tables.length}):`);
233
+ console.log('─'.repeat(60));
234
+ tables.forEach((table, index) => {
235
+ console.log(`${index + 1}. ${table}`);
236
+ });
237
+ console.log('');
238
+ }
239
+ } finally {
240
+ db.close();
241
+ }
242
+ return;
243
+ }
244
+
245
+ if (sqlEditor || sqlEditorQuery) {
246
+ const dbName = databaseName || databasePath || databaseSize || databaseLastOpened || databaseTables;
247
+ if (!dbName) {
248
+ console.error('Error: --sqleditor requires a database name.');
249
+ console.error('Usage: sqlite-hub --database:name --sqleditor');
250
+ console.error(' sqlite-hub --database:name --sqleditor:"query name"');
251
+ process.exit(1);
252
+ }
253
+ const conn = findDatabaseByName(connections, dbName);
254
+ if (!conn) {
255
+ console.error(`Database not found: ${dbName}`);
256
+ process.exit(1);
257
+ }
258
+
259
+ const Database = require('better-sqlite3');
260
+ const { SqlExecutor } = require('../server/services/sqlite/sqlExecutor');
261
+ const { ConnectionManager } = require('../server/services/sqlite/connectionManager');
262
+
263
+ const db = new Database(conn.path, { readonly: true });
264
+
265
+ try {
266
+ const connectionManager = new ConnectionManager({ appStateStore });
267
+ connectionManager.openConnection({
268
+ filePath: conn.path,
269
+ label: conn.label,
270
+ makeActive: true,
271
+ readOnly: true,
272
+ });
273
+
274
+ const sqlExecutor = new SqlExecutor({ connectionManager, appStateStore });
275
+
276
+ if (sqlEditorQuery) {
277
+ const queryHistory = appStateStore.buildQueryHistoryCollection({
278
+ databaseKey: conn.id,
279
+ search: sqlEditorQuery,
280
+ onlySaved: false,
281
+ limit: 100,
282
+ });
283
+
284
+ const matchingQuery = queryHistory.items.find(
285
+ item =>
286
+ item.title?.toLowerCase() === sqlEditorQuery.toLowerCase() ||
287
+ item.rawSql.toLowerCase().includes(sqlEditorQuery.toLowerCase()),
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}`);
301
+ });
302
+ } else {
303
+ console.log(' (none)');
304
+ }
305
+ process.exit(1);
306
+ }
307
+
308
+ console.log(`\nExecuting: ${matchingQuery.title || matchingQuery.displayTitle}`);
309
+ console.log(`SQL: ${matchingQuery.previewSql}`);
310
+ console.log('─'.repeat(60));
311
+
312
+ const result = sqlExecutor.execute(matchingQuery.rawSql, {
313
+ persistHistory: false,
314
+ });
315
+
316
+ console.log(`\nStatement count: ${result.statementCount}`);
317
+ console.log(`Timing: ${result.timingMs}ms`);
318
+
319
+ result.statements.forEach((stmt, index) => {
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
+ });
347
+
348
+ if (savedQueries.items.length === 0) {
349
+ console.log(`No saved queries found for ${conn.label}.`);
350
+ } else {
351
+ console.log(`\nSaved queries for ${conn.label} (${savedQueries.total}):`);
352
+ console.log('─'.repeat(60));
353
+ savedQueries.items.forEach((q, index) => {
354
+ const title = q.title || q.displayTitle;
355
+ console.log(`${index + 1}. ${title}`);
356
+ });
357
+ console.log('');
358
+ }
359
+ }
360
+ } finally {
361
+ db.close();
362
+ }
363
+ return;
364
+ }
365
+
366
+ if (database) {
367
+ if (connections.length === 0) {
368
+ console.log('No databases imported yet.');
369
+ return;
370
+ }
371
+
372
+ console.log(`\nImported databases (${connections.length}):`);
373
+ console.log('─'.repeat(60));
374
+
375
+ connections.forEach((conn, index) => {
376
+ const size = formatSize(conn.sizeBytes);
377
+ const readOnly = conn.readOnly ? ' (read-only)' : '';
378
+ console.log(`${index + 1}. ${conn.label}${readOnly}`);
379
+ console.log(` Path: ${conn.path}`);
380
+ console.log(` Size: ${size}`);
381
+ console.log(` Last opened: ${conn.lastOpenedAt}`);
382
+ console.log('');
383
+ });
384
+
385
+ return;
386
+ }
387
+
107
388
  const { startServer } = require('../server/server');
108
389
  const { url } = await startServer({ port });
109
390
  openInDefaultBrowser(url);
package/changelog.md CHANGED
@@ -1,3 +1,18 @@
1
+ # v0.7.0
2
+
3
+ - hide query history, hide editor
4
+ - better cli interface
5
+ - plotting and graphs
6
+ - removing inconsistencies in the UI
7
+
8
+ # v0.6.0
9
+
10
+ - table designer, create tables in sqlit_hub
11
+
12
+ # v0.5.1
13
+
14
+ - bug fix in sql editor
15
+
1
16
  # v0.5.0
2
17
 
3
18
  - sql query history
Binary file
Binary file
@@ -96,6 +96,7 @@
96
96
  class="bg-background text-on-surface font-body selection:bg-primary-container selection:text-on-primary overflow-hidden"
97
97
  >
98
98
  <div id="app"></div>
99
+ <script src="/vendor/echarts/dist/echarts.min.js"></script>
99
100
  <script src="/vendor/elkjs/lib/elk.bundled.js"></script>
100
101
  <script src="/vendor/cytoscape-elk/dist/cytoscape-elk.js"></script>
101
102
  <script src="js/app.js" type="module"></script>
@@ -185,6 +185,40 @@ export function getQueryHistoryItem(historyId) {
185
185
  return request(`/api/sql/history/${encodeURIComponent(historyId)}`);
186
186
  }
187
187
 
188
+ export function getChartsQueryHistory() {
189
+ return request("/api/charts/query-history");
190
+ }
191
+
192
+ export function getChartsQueryHistoryDetail(historyId) {
193
+ return request(`/api/charts/query-history/${encodeURIComponent(historyId)}`);
194
+ }
195
+
196
+ export function executeChartsQueryHistory(historyId) {
197
+ return request(`/api/charts/query-history/${encodeURIComponent(historyId)}/execute`, {
198
+ method: "POST",
199
+ });
200
+ }
201
+
202
+ export function createQueryHistoryChart(payload) {
203
+ return request("/api/charts", {
204
+ method: "POST",
205
+ body: payload,
206
+ });
207
+ }
208
+
209
+ export function updateQueryHistoryChart(chartId, payload) {
210
+ return request(`/api/charts/${encodeURIComponent(chartId)}`, {
211
+ method: "PATCH",
212
+ body: payload,
213
+ });
214
+ }
215
+
216
+ export function deleteQueryHistoryChart(chartId) {
217
+ return request(`/api/charts/${encodeURIComponent(chartId)}`, {
218
+ method: "DELETE",
219
+ });
220
+ }
221
+
188
222
  export function getQueryHistoryRuns(historyId, options = {}) {
189
223
  const params = new URLSearchParams();
190
224
 
@@ -234,6 +268,21 @@ export function getStructureDetail(tableName) {
234
268
  return request(`/api/structure/${encodeURIComponent(tableName)}`);
235
269
  }
236
270
 
271
+ export function getTableDesignerOverview() {
272
+ return request("/api/table-designer");
273
+ }
274
+
275
+ export function getTableDesignerTable(tableName) {
276
+ return request(`/api/table-designer/${encodeURIComponent(tableName)}`);
277
+ }
278
+
279
+ export function saveTableDesignerDraft(payload) {
280
+ return request("/api/table-designer/save", {
281
+ method: "POST",
282
+ body: payload,
283
+ });
284
+ }
285
+
237
286
  export function getDataTables() {
238
287
  return request("/api/data");
239
288
  }