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.
- package/changelog.md +25 -0
- package/docs/DESIGN_GUIDELINES.md +36 -0
- package/frontend/index.html +79 -0
- package/frontend/js/api.js +67 -0
- package/frontend/js/app.js +1401 -921
- package/frontend/js/components/connectionCard.js +3 -4
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/modal.js +239 -30
- package/frontend/js/components/queryEditor.js +11 -8
- package/frontend/js/components/queryHistoryDetail.js +27 -8
- package/frontend/js/components/queryHistoryPanel.js +6 -5
- package/frontend/js/components/rowEditorPanel.js +84 -58
- package/frontend/js/components/sidebar.js +76 -33
- package/frontend/js/components/structureGraph.js +629 -715
- package/frontend/js/components/tableDesignerEditor.js +9 -8
- package/frontend/js/components/tableDesignerSidebar.js +11 -10
- package/frontend/js/components/tableDesignerSqlPreview.js +61 -30
- package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
- package/frontend/js/router.js +81 -71
- package/frontend/js/store.js +3095 -2165
- package/frontend/js/views/charts.js +68 -40
- package/frontend/js/views/connections.js +5 -17
- package/frontend/js/views/data.js +40 -27
- package/frontend/js/views/editor.js +172 -177
- package/frontend/js/views/mediaTagging.js +861 -0
- package/frontend/js/views/overview.js +149 -10
- package/frontend/js/views/settings.js +2 -2
- package/frontend/js/views/structure.js +74 -70
- package/frontend/js/views/tableDesigner.js +7 -2
- package/frontend/styles/base.css +73 -1
- package/frontend/styles/components.css +105 -105
- package/frontend/styles/structure-graph.css +19 -82
- package/frontend/styles/tokens.css +2 -0
- package/frontend/styles/views.css +823 -30
- package/package.json +1 -1
- package/server/routes/charts.js +4 -1
- package/server/routes/data.js +14 -0
- package/server/routes/mediaTagging.js +166 -0
- package/server/routes/sql.js +1 -0
- package/server/server.js +4 -0
- package/server/services/sqlite/dataBrowserService.js +25 -0
- package/server/services/sqlite/exportService.js +31 -1
- package/server/services/sqlite/mediaTaggingService.js +1689 -0
- package/server/services/sqlite/overviewService.js +68 -0
- package/server/services/storage/appStateStore.js +321 -2
package/package.json
CHANGED
package/server/routes/charts.js
CHANGED
|
@@ -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.
|
|
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,
|
package/server/routes/data.js
CHANGED
|
@@ -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
|
+
};
|
package/server/routes/sql.js
CHANGED
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:
|
|
55
|
+
filename: `${filenameBase}.csv`,
|
|
26
56
|
csv: rowsToCsv({
|
|
27
57
|
columns: result.columns,
|
|
28
58
|
rows: result.rows,
|