sqlite-hub 0.7.0 → 0.9.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 (45) hide show
  1. package/changelog.md +25 -0
  2. package/docs/DESIGN_GUIDELINES.md +36 -0
  3. package/frontend/index.html +79 -0
  4. package/frontend/js/api.js +67 -0
  5. package/frontend/js/app.js +1401 -921
  6. package/frontend/js/components/connectionCard.js +3 -4
  7. package/frontend/js/components/emptyState.js +4 -4
  8. package/frontend/js/components/modal.js +239 -30
  9. package/frontend/js/components/queryEditor.js +11 -8
  10. package/frontend/js/components/queryHistoryDetail.js +27 -8
  11. package/frontend/js/components/queryHistoryPanel.js +6 -5
  12. package/frontend/js/components/rowEditorPanel.js +84 -58
  13. package/frontend/js/components/sidebar.js +76 -33
  14. package/frontend/js/components/structureGraph.js +629 -715
  15. package/frontend/js/components/tableDesignerEditor.js +9 -8
  16. package/frontend/js/components/tableDesignerSidebar.js +11 -10
  17. package/frontend/js/components/tableDesignerSqlPreview.js +61 -30
  18. package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
  19. package/frontend/js/router.js +81 -71
  20. package/frontend/js/store.js +3095 -2165
  21. package/frontend/js/views/charts.js +68 -40
  22. package/frontend/js/views/connections.js +5 -17
  23. package/frontend/js/views/data.js +40 -27
  24. package/frontend/js/views/editor.js +172 -177
  25. package/frontend/js/views/mediaTagging.js +861 -0
  26. package/frontend/js/views/overview.js +149 -10
  27. package/frontend/js/views/settings.js +2 -2
  28. package/frontend/js/views/structure.js +74 -70
  29. package/frontend/js/views/tableDesigner.js +7 -2
  30. package/frontend/styles/base.css +73 -1
  31. package/frontend/styles/components.css +105 -105
  32. package/frontend/styles/structure-graph.css +19 -82
  33. package/frontend/styles/tokens.css +2 -0
  34. package/frontend/styles/views.css +823 -30
  35. package/package.json +1 -1
  36. package/server/routes/charts.js +4 -1
  37. package/server/routes/data.js +14 -0
  38. package/server/routes/mediaTagging.js +166 -0
  39. package/server/routes/sql.js +1 -0
  40. package/server/server.js +4 -0
  41. package/server/services/sqlite/dataBrowserService.js +25 -0
  42. package/server/services/sqlite/exportService.js +31 -1
  43. package/server/services/sqlite/mediaTaggingService.js +1689 -0
  44. package/server/services/sqlite/overviewService.js +68 -0
  45. package/server/services/storage/appStateStore.js +321 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-hub",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "main": "server/server.js",
6
6
  "bin": {
@@ -57,7 +57,10 @@ function createChartsRouter({ appStateStore, connectionManager, sqlExecutor }) {
57
57
  "/query-history/:historyId/execute",
58
58
  route((req, res) => {
59
59
  const databaseKey = requireActiveDatabaseKey(connectionManager);
60
- const item = appStateStore.getQueryHistoryItemForDatabase(req.params.historyId, databaseKey);
60
+ const item = appStateStore.getChartQueryHistoryItemForDatabase(
61
+ req.params.historyId,
62
+ databaseKey
63
+ );
61
64
  const result = sqlExecutor.execute(item.rawSql, {
62
65
  persistHistory: false,
63
66
  requireReader: true,
@@ -39,6 +39,20 @@ function createDataRouter({ dataBrowserService }) {
39
39
  })
40
40
  );
41
41
 
42
+ router.post(
43
+ "/:tableName/row",
44
+ route((req, res) => {
45
+ const data = dataBrowserService.getTableRow(req.params.tableName, req.body ?? {});
46
+
47
+ res.json(
48
+ successResponse({
49
+ data,
50
+ readOnly: false,
51
+ })
52
+ );
53
+ })
54
+ );
55
+
42
56
  router.patch(
43
57
  "/:tableName/rows",
44
58
  route((req, res) => {
@@ -0,0 +1,166 @@
1
+ const fs = require("node:fs");
2
+ const express = require("express");
3
+ const { NotFoundError, route, successResponse } = require("../utils/errors");
4
+
5
+ function createMediaTaggingRouter({ mediaTaggingService }) {
6
+ const router = express.Router();
7
+
8
+ router.get(
9
+ "/",
10
+ route((req, res) => {
11
+ const data = mediaTaggingService.getViewState();
12
+
13
+ res.json(
14
+ successResponse({
15
+ data,
16
+ readOnly: Boolean(data.connection?.readOnly),
17
+ })
18
+ );
19
+ })
20
+ );
21
+
22
+ router.post(
23
+ "/preview",
24
+ route((req, res) => {
25
+ const data = mediaTaggingService.getViewState({
26
+ config: req.body?.config ?? {},
27
+ skippedMediaKeys: req.body?.skippedMediaKeys ?? [],
28
+ });
29
+
30
+ res.json(
31
+ successResponse({
32
+ data,
33
+ message: "Media tagging preview updated.",
34
+ readOnly: Boolean(data.connection?.readOnly),
35
+ })
36
+ );
37
+ })
38
+ );
39
+
40
+ router.post(
41
+ "/config",
42
+ route((req, res) => {
43
+ const data = mediaTaggingService.saveConfig(req.body?.config ?? {}, {
44
+ skippedMediaKeys: req.body?.skippedMediaKeys ?? [],
45
+ });
46
+
47
+ res.json(
48
+ successResponse({
49
+ data,
50
+ message: "Media tagging configuration saved.",
51
+ readOnly: Boolean(data.connection?.readOnly),
52
+ })
53
+ );
54
+ })
55
+ );
56
+
57
+ router.post(
58
+ "/tag-table/create",
59
+ route((req, res) => {
60
+ const data = mediaTaggingService.createDefaultTagTable(req.body ?? {});
61
+
62
+ res.json(
63
+ successResponse({
64
+ data,
65
+ message: "Tag table created.",
66
+ readOnly: Boolean(data.connection?.readOnly),
67
+ })
68
+ );
69
+ })
70
+ );
71
+
72
+ router.post(
73
+ "/mapping-table/create",
74
+ route((req, res) => {
75
+ const data = mediaTaggingService.createDefaultMappingTable(req.body ?? {});
76
+
77
+ res.json(
78
+ successResponse({
79
+ data,
80
+ message: "Mapping table created.",
81
+ readOnly: Boolean(data.connection?.readOnly),
82
+ })
83
+ );
84
+ })
85
+ );
86
+
87
+ router.post(
88
+ "/tags",
89
+ route((req, res) => {
90
+ const data = mediaTaggingService.createTag(req.body ?? {});
91
+
92
+ res.json(
93
+ successResponse({
94
+ data,
95
+ message: "Tag created.",
96
+ readOnly: Boolean(data.connection?.readOnly),
97
+ })
98
+ );
99
+ })
100
+ );
101
+
102
+ router.post(
103
+ "/tags/delete",
104
+ route((req, res) => {
105
+ const data = mediaTaggingService.deleteTag(req.body ?? {});
106
+
107
+ res.json(
108
+ successResponse({
109
+ data,
110
+ message: "Tag removed.",
111
+ readOnly: Boolean(data.connection?.readOnly),
112
+ })
113
+ );
114
+ })
115
+ );
116
+
117
+ router.post(
118
+ "/skip",
119
+ route((req, res) => {
120
+ const data = mediaTaggingService.skipCurrentMedia(req.body ?? {});
121
+
122
+ res.json(
123
+ successResponse({
124
+ data,
125
+ message: "Media item skipped and marked tagged.",
126
+ readOnly: Boolean(data.connection?.readOnly),
127
+ })
128
+ );
129
+ })
130
+ );
131
+
132
+ router.post(
133
+ "/apply",
134
+ route((req, res) => {
135
+ const data = mediaTaggingService.applyTagging(req.body ?? {});
136
+
137
+ res.json(
138
+ successResponse({
139
+ data,
140
+ message: "Media item tagged.",
141
+ readOnly: Boolean(data.connection?.readOnly),
142
+ })
143
+ );
144
+ })
145
+ );
146
+
147
+ router.get(
148
+ "/media-file",
149
+ route((req, res) => {
150
+ const rawPath = String(req.query.path ?? "").trim();
151
+ const resolvedPath = mediaTaggingService.resolveMediaFilePath(rawPath);
152
+
153
+ if (!resolvedPath || !fs.existsSync(resolvedPath)) {
154
+ throw new NotFoundError(`Media file not found: ${rawPath}`);
155
+ }
156
+
157
+ res.sendFile(resolvedPath);
158
+ })
159
+ );
160
+
161
+ return router;
162
+ }
163
+
164
+ module.exports = {
165
+ createMediaTaggingRouter,
166
+ };
@@ -74,6 +74,7 @@ function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
74
74
  : appStateStore.getRecentQueries({
75
75
  ...options,
76
76
  onlySaved: tab === "saved" ? true : options.onlySaved,
77
+ onlyUnsaved: tab === "unsaved",
77
78
  });
78
79
 
79
80
  res.json(
package/server/server.js CHANGED
@@ -12,6 +12,7 @@ const { ExportService } = require("./services/sqlite/exportService");
12
12
  const { StructureService } = require("./services/sqlite/structureService");
13
13
  const { DataBrowserService } = require("./services/sqlite/dataBrowserService");
14
14
  const { TableDesignerService } = require("./services/sqlite/tableDesignerService");
15
+ const { MediaTaggingService } = require("./services/sqlite/mediaTaggingService");
15
16
  const { createConnectionsRouter } = require("./routes/connections");
16
17
  const { createOverviewRouter } = require("./routes/overview");
17
18
  const { createSqlRouter } = require("./routes/sql");
@@ -19,6 +20,7 @@ const { createChartsRouter } = require("./routes/charts");
19
20
  const { createStructureRouter } = require("./routes/structure");
20
21
  const { createDataRouter } = require("./routes/data");
21
22
  const { createTableDesignerRouter } = require("./routes/tableDesigner");
23
+ const { createMediaTaggingRouter } = require("./routes/mediaTagging");
22
24
  const { createSettingsRouter } = require("./routes/settings");
23
25
  const { createExportRouter } = require("./routes/export");
24
26
 
@@ -50,6 +52,7 @@ const exportService = new ExportService({
50
52
  const structureService = new StructureService({ connectionManager, appStateStore });
51
53
  const dataBrowserService = new DataBrowserService({ connectionManager });
52
54
  const tableDesignerService = new TableDesignerService({ connectionManager });
55
+ const mediaTaggingService = new MediaTaggingService({ connectionManager, appStateStore });
53
56
 
54
57
  connectionManager.initialize();
55
58
 
@@ -84,6 +87,7 @@ app.use("/api/charts", createChartsRouter({ appStateStore, connectionManager, sq
84
87
  app.use("/api/structure", createStructureRouter({ structureService }));
85
88
  app.use("/api/data", createDataRouter({ dataBrowserService }));
86
89
  app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService }));
90
+ app.use("/api/media-tagging", createMediaTaggingRouter({ mediaTaggingService }));
87
91
  app.use("/api/settings", createSettingsRouter({ appStateStore }));
88
92
  app.use("/api/export", createExportRouter({ exportService }));
89
93
 
@@ -113,6 +113,7 @@ class DataBrowserService {
113
113
  pageCount: Math.max(1, Math.ceil((tableDetail.rowCount ?? rows.length) / limit)),
114
114
  columns,
115
115
  columnMeta: tableDetail.columns,
116
+ foreignKeys: tableDetail.foreignKeys,
116
117
  rows,
117
118
  identityStrategy: tableDetail.identityStrategy,
118
119
  notSafelyUpdatable: tableDetail.notSafelyUpdatable,
@@ -120,6 +121,30 @@ class DataBrowserService {
120
121
  };
121
122
  }
122
123
 
124
+ getTableRow(tableName, payload = {}) {
125
+ const db = this.connectionManager.getActiveDatabase();
126
+ const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
127
+ const identity = payload.identity ?? null;
128
+
129
+ if (tableDetail.notSafelyUpdatable) {
130
+ throw new ValidationError(
131
+ `Table ${tableName} has no stable row identity, so a specific row cannot be targeted.`
132
+ );
133
+ }
134
+
135
+ const where = this.buildWhereClause(tableDetail, identity);
136
+ const row = this.getRowByIdentity(db, tableDetail, where);
137
+
138
+ if (!row) {
139
+ throw new NotFoundError(`Row not found in table: ${tableName}`);
140
+ }
141
+
142
+ return {
143
+ tableName,
144
+ row,
145
+ };
146
+ }
147
+
123
148
  updateTableRow(tableName, payload = {}) {
124
149
  this.connectionManager.assertWritable();
125
150
 
@@ -3,6 +3,25 @@ const { serializeRows } = require("../../utils/sqliteTypes");
3
3
  const { rowsToCsv } = require("../../utils/csv");
4
4
  const { getTableDetail } = require("./introspection");
5
5
  const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
6
+ const {
7
+ buildAutoTitle,
8
+ detectQueryType,
9
+ detectTables,
10
+ } = require("../storage/queryHistoryUtils");
11
+
12
+ function sanitizeFilenameBase(value, fallback = "query-results") {
13
+ const sanitized = String(value ?? "")
14
+ .replace(/[<>:"/\\|?*\u0000-\u001f]/g, " ")
15
+ .replace(/\s+/g, " ")
16
+ .trim()
17
+ .replace(/[. ]+$/g, "");
18
+
19
+ if (!sanitized) {
20
+ return fallback;
21
+ }
22
+
23
+ return sanitized.slice(0, 120);
24
+ }
6
25
 
7
26
  class ExportService {
8
27
  constructor({ appStateStore, connectionManager, sqlExecutor }) {
@@ -16,13 +35,24 @@ class ExportService {
16
35
  }
17
36
 
18
37
  exportQuery(sql) {
38
+ const activeConnection = this.connectionManager.getActiveConnection();
39
+ const historyItem = activeConnection
40
+ ? this.appStateStore.findQueryHistoryItemBySql(activeConnection.id, sql)
41
+ : null;
42
+ const filenameBase = sanitizeFilenameBase(
43
+ historyItem?.displayTitle ||
44
+ buildAutoTitle(sql, {
45
+ queryType: detectQueryType(sql),
46
+ tablesDetected: detectTables(sql),
47
+ })
48
+ );
19
49
  const result = this.sqlExecutor.execute(sql, {
20
50
  persistHistory: false,
21
51
  requireReader: true,
22
52
  });
23
53
 
24
54
  return {
25
- filename: "query-results.csv",
55
+ filename: `${filenameBase}.csv`,
26
56
  csv: rowsToCsv({
27
57
  columns: result.columns,
28
58
  rows: result.rows,