sqlite-hub 2.0.1 → 2.2.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 +21 -9
- package/docs/MCP.md +8 -0
- package/docs/changelog.md +16 -0
- package/docs/guidelines/DESIGN.md +64 -0
- package/docs/todo.md +2 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/logs_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +18 -0
- package/frontend/js/app.js +567 -15
- package/frontend/js/components/badges.js +16 -0
- package/frontend/js/components/connectionCard.js +35 -0
- package/frontend/js/components/modal.js +268 -247
- package/frontend/js/components/queryHistoryDetail.js +25 -45
- package/frontend/js/components/rowEditorPanel.js +7 -60
- package/frontend/js/components/structureGraph.js +292 -13
- package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
- package/frontend/js/components/tableDesignerEditor.js +51 -16
- package/frontend/js/components/tableDesignerSidebar.js +9 -5
- package/frontend/js/store.js +1847 -190
- package/frontend/js/utils/connectionRegistry.js +76 -0
- package/frontend/js/utils/markdownDocuments.js +21 -4
- package/frontend/js/utils/tableDesigner.js +431 -62
- package/frontend/js/views/backups.js +2 -5
- package/frontend/js/views/charts.js +20 -10
- package/frontend/js/views/connections.js +101 -6
- package/frontend/js/views/data.js +52 -9
- package/frontend/js/views/documents.js +153 -59
- package/frontend/js/views/structure.js +28 -5
- package/frontend/js/views/tableAdvisor.js +14 -6
- package/frontend/js/views/tableDesigner.js +44 -9
- package/frontend/styles/base.css +9 -0
- package/frontend/styles/components.css +676 -79
- package/frontend/styles/structure-graph.css +49 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +12 -0
- package/package.json +1 -1
- package/server/routes/backups.js +37 -1
- package/server/routes/charts.js +28 -0
- package/server/routes/connections.js +12 -0
- package/server/routes/data.js +30 -2
- package/server/routes/documents.js +61 -5
- package/server/routes/settings.js +32 -0
- package/server/routes/tableDesigner.js +35 -2
- package/server/server.js +3 -3
- package/server/services/mcpToolService.js +30 -0
- package/server/services/sqlite/connectionManager.js +25 -2
- package/server/services/sqlite/dataBrowserService.js +21 -0
- package/server/services/sqlite/introspection.js +39 -1
- package/server/services/sqlite/overviewService.js +1 -0
- package/server/services/sqlite/structureService.js +66 -11
- package/server/services/sqlite/tableAdvisor.js +6 -0
- package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
- package/server/services/sqlite/tableDesigner/sql.js +31 -2
- package/server/services/sqlite/tableDesigner/validation.js +69 -9
- package/server/services/sqlite/tableDesignerService.js +110 -5
- package/server/services/storage/appStateStore.js +590 -10
- package/server/utils/fileValidation.js +5 -0
- package/server/utils/userActionLog.js +49 -0
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
2
|
const path = require("node:path");
|
|
3
3
|
const { DatabaseRequiredError, route, successResponse } = require("../utils/errors");
|
|
4
|
+
const { recordUserAction } = require("../utils/userActionLog");
|
|
4
5
|
|
|
5
6
|
function getActiveDatabaseKey(connectionManager) {
|
|
6
7
|
return connectionManager.getActiveConnection()?.id ?? null;
|
|
@@ -27,20 +28,23 @@ function resolveActiveDatabaseDocumentName(connection) {
|
|
|
27
28
|
return basename || "Database";
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function buildInitialDocumentPayload(connection) {
|
|
31
|
+
function buildInitialDocumentPayload(connection, options = {}) {
|
|
31
32
|
const databaseName = resolveActiveDatabaseDocumentName(connection);
|
|
33
|
+
const timestamp = new Date().toISOString();
|
|
34
|
+
const createdAt = String(options.createdAt ?? connection?.createdAt ?? "").trim() || timestamp;
|
|
35
|
+
const lastModifiedAt = String(options.lastModifiedAt ?? "").trim() || timestamp;
|
|
32
36
|
|
|
33
37
|
return {
|
|
34
38
|
title: databaseName,
|
|
35
39
|
filename: databaseName,
|
|
36
|
-
content: `# ${databaseName}\n`,
|
|
40
|
+
content: `# ${databaseName}\n\n- created at: ${createdAt}\n- last modified: ${lastModifiedAt}\n\n## Purpose\n`,
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
function pickDocumentPatch(body = {}) {
|
|
41
45
|
const patch = {};
|
|
42
46
|
|
|
43
|
-
for (const field of ["title", "filename", "content"]) {
|
|
47
|
+
for (const field of ["title", "filename", "content", "folderId"]) {
|
|
44
48
|
if (Object.prototype.hasOwnProperty.call(body, field)) {
|
|
45
49
|
patch[field] = body[field];
|
|
46
50
|
}
|
|
@@ -58,7 +62,9 @@ function ensureDatabaseDocuments({ appStateStore, connectionManager, databaseKey
|
|
|
58
62
|
|
|
59
63
|
appStateStore.createDatabaseDocument(
|
|
60
64
|
databaseKey,
|
|
61
|
-
buildInitialDocumentPayload(connectionManager.getActiveConnection()
|
|
65
|
+
buildInitialDocumentPayload(connectionManager.getActiveConnection(), {
|
|
66
|
+
lastModifiedAt: appStateStore.getLatestActivityLogTimestamp?.(databaseKey),
|
|
67
|
+
})
|
|
62
68
|
);
|
|
63
69
|
|
|
64
70
|
return appStateStore.listDatabaseDocuments(databaseKey);
|
|
@@ -76,6 +82,7 @@ function createDocumentsRouter({ appStateStore, connectionManager }) {
|
|
|
76
82
|
successResponse({
|
|
77
83
|
data: {
|
|
78
84
|
items: ensureDatabaseDocuments({ appStateStore, connectionManager, databaseKey }),
|
|
85
|
+
folders: appStateStore.listDatabaseDocumentFolders(databaseKey),
|
|
79
86
|
},
|
|
80
87
|
metadata: { databaseKey },
|
|
81
88
|
})
|
|
@@ -83,6 +90,27 @@ function createDocumentsRouter({ appStateStore, connectionManager }) {
|
|
|
83
90
|
})
|
|
84
91
|
);
|
|
85
92
|
|
|
93
|
+
router.post(
|
|
94
|
+
"/folders",
|
|
95
|
+
route((req, res) => {
|
|
96
|
+
const databaseKey = requireActiveDatabaseKey(connectionManager);
|
|
97
|
+
const folder = appStateStore.createDatabaseDocumentFolder(databaseKey, {
|
|
98
|
+
name: req.body?.name,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
res.status(201).json(
|
|
102
|
+
successResponse({
|
|
103
|
+
message: "Document folder created.",
|
|
104
|
+
data: folder,
|
|
105
|
+
metadata: {
|
|
106
|
+
databaseKey,
|
|
107
|
+
folders: appStateStore.listDatabaseDocumentFolders(databaseKey),
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
|
|
86
114
|
router.post(
|
|
87
115
|
"/",
|
|
88
116
|
route((req, res) => {
|
|
@@ -91,6 +119,19 @@ function createDocumentsRouter({ appStateStore, connectionManager }) {
|
|
|
91
119
|
title: req.body?.title,
|
|
92
120
|
filename: req.body?.filename,
|
|
93
121
|
content: req.body?.content,
|
|
122
|
+
folderId: req.body?.folderId,
|
|
123
|
+
});
|
|
124
|
+
recordUserAction({
|
|
125
|
+
appStateStore,
|
|
126
|
+
connectionManager,
|
|
127
|
+
action: "document.create",
|
|
128
|
+
databaseKey,
|
|
129
|
+
targetType: "document",
|
|
130
|
+
targetName: document.filename ?? document.id,
|
|
131
|
+
metadata: {
|
|
132
|
+
documentId: document.id,
|
|
133
|
+
title: document.title ?? null,
|
|
134
|
+
},
|
|
94
135
|
});
|
|
95
136
|
|
|
96
137
|
res.status(201).json(
|
|
@@ -140,11 +181,26 @@ function createDocumentsRouter({ appStateStore, connectionManager }) {
|
|
|
140
181
|
"/:documentId",
|
|
141
182
|
route((req, res) => {
|
|
142
183
|
const databaseKey = requireActiveDatabaseKey(connectionManager);
|
|
184
|
+
const document = appStateStore.getDatabaseDocument(databaseKey, req.params.documentId);
|
|
185
|
+
const deleted = appStateStore.deleteDatabaseDocument(databaseKey, req.params.documentId);
|
|
186
|
+
|
|
187
|
+
recordUserAction({
|
|
188
|
+
appStateStore,
|
|
189
|
+
connectionManager,
|
|
190
|
+
action: "document.delete",
|
|
191
|
+
databaseKey,
|
|
192
|
+
targetType: "document",
|
|
193
|
+
targetName: document.filename ?? req.params.documentId,
|
|
194
|
+
metadata: {
|
|
195
|
+
documentId: req.params.documentId,
|
|
196
|
+
title: document.title ?? null,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
143
199
|
|
|
144
200
|
res.json(
|
|
145
201
|
successResponse({
|
|
146
202
|
message: "Document deleted.",
|
|
147
|
-
data:
|
|
203
|
+
data: deleted,
|
|
148
204
|
metadata: { databaseKey },
|
|
149
205
|
})
|
|
150
206
|
);
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
} = require("../services/appInfoService");
|
|
11
11
|
const { McpStatusService } = require("../services/mcpStatusService");
|
|
12
12
|
const { MCP_TOOL_DEFINITIONS } = require("../services/mcpToolService");
|
|
13
|
+
const { recordUserAction } = require("../utils/userActionLog");
|
|
13
14
|
|
|
14
15
|
function getActiveTokenContext({ connectionManager, tokenService }) {
|
|
15
16
|
const activeDatabase = connectionManager?.getActiveConnection?.() ?? null;
|
|
@@ -161,6 +162,20 @@ function createSettingsRouter({ appStateStore, connectionManager, tokenService,
|
|
|
161
162
|
const activeDatabase = requireActiveDatabase(connectionManager);
|
|
162
163
|
const token = tokenService.createToken(activeDatabase.id, req.body?.name);
|
|
163
164
|
|
|
165
|
+
recordUserAction({
|
|
166
|
+
appStateStore,
|
|
167
|
+
connectionManager,
|
|
168
|
+
action: "settings.api-token.create",
|
|
169
|
+
targetType: "api-token",
|
|
170
|
+
targetName: token.name ?? token.id,
|
|
171
|
+
databaseKey: activeDatabase.id,
|
|
172
|
+
metadata: {
|
|
173
|
+
apiTokenId: token.id,
|
|
174
|
+
apiTokenName: token.name ?? null,
|
|
175
|
+
tokenPrefix: token.tokenPrefix ?? null,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
164
179
|
res.status(201).json(
|
|
165
180
|
successResponse({
|
|
166
181
|
message: "API token created. It will only be shown once.",
|
|
@@ -175,8 +190,25 @@ function createSettingsRouter({ appStateStore, connectionManager, tokenService,
|
|
|
175
190
|
"/api-tokens/:tokenId",
|
|
176
191
|
route((req, res) => {
|
|
177
192
|
const activeDatabase = requireActiveDatabase(connectionManager);
|
|
193
|
+
const token = tokenService
|
|
194
|
+
.listTokens(activeDatabase.id)
|
|
195
|
+
.find((candidate) => candidate.id === req.params.tokenId);
|
|
178
196
|
const result = tokenService.deleteToken(activeDatabase.id, req.params.tokenId);
|
|
179
197
|
|
|
198
|
+
recordUserAction({
|
|
199
|
+
appStateStore,
|
|
200
|
+
connectionManager,
|
|
201
|
+
action: "settings.api-token.delete",
|
|
202
|
+
targetType: "api-token",
|
|
203
|
+
targetName: token?.name ?? req.params.tokenId,
|
|
204
|
+
databaseKey: activeDatabase.id,
|
|
205
|
+
metadata: {
|
|
206
|
+
apiTokenId: req.params.tokenId,
|
|
207
|
+
apiTokenName: token?.name ?? null,
|
|
208
|
+
tokenPrefix: token?.tokenPrefix ?? null,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
180
212
|
res.json(
|
|
181
213
|
successResponse({
|
|
182
214
|
message: "API token deleted.",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
2
|
const { route, successResponse } = require("../utils/errors");
|
|
3
|
+
const { recordUserAction } = require("../utils/userActionLog");
|
|
3
4
|
|
|
4
|
-
function createTableDesignerRouter({ tableDesignerService }) {
|
|
5
|
+
function createTableDesignerRouter({ tableDesignerService, appStateStore = null, connectionManager = null }) {
|
|
5
6
|
const router = express.Router();
|
|
6
7
|
|
|
7
8
|
router.get(
|
|
@@ -18,6 +19,20 @@ function createTableDesignerRouter({ tableDesignerService }) {
|
|
|
18
19
|
})
|
|
19
20
|
);
|
|
20
21
|
|
|
22
|
+
router.post(
|
|
23
|
+
"/validate-check",
|
|
24
|
+
route((req, res) => {
|
|
25
|
+
const data = tableDesignerService.validateCheckExpression(req.body ?? {});
|
|
26
|
+
|
|
27
|
+
res.json(
|
|
28
|
+
successResponse({
|
|
29
|
+
data,
|
|
30
|
+
message: "CHECK expression validated.",
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
|
|
21
36
|
router.get(
|
|
22
37
|
"/:tableName",
|
|
23
38
|
route((req, res) => {
|
|
@@ -38,12 +53,30 @@ function createTableDesignerRouter({ tableDesignerService }) {
|
|
|
38
53
|
const data = tableDesignerService.saveDraft(req.body ?? {});
|
|
39
54
|
const isCreate = String(req.body?.draft?.mode ?? req.body?.mode ?? "").trim() !== "edit";
|
|
40
55
|
const fillsImportedRows = Boolean(req.body?.draft?.fillImportedRows ?? req.body?.fillImportedRows);
|
|
56
|
+
const importedRows = req.body?.draft?.importRows ?? req.body?.draft?.importedCsvRows ?? [];
|
|
57
|
+
const executedSqlCount = Array.isArray(data.executedSql) ? data.executedSql.length : 0;
|
|
58
|
+
|
|
59
|
+
if (isCreate || executedSqlCount > 0) {
|
|
60
|
+
recordUserAction({
|
|
61
|
+
appStateStore,
|
|
62
|
+
connectionManager,
|
|
63
|
+
action: isCreate ? "table-designer.table.create" : "table-designer.table.update",
|
|
64
|
+
targetType: "table",
|
|
65
|
+
targetName: data.savedTableName ?? req.body?.draft?.tableName ?? req.body?.tableName,
|
|
66
|
+
metadata: {
|
|
67
|
+
mode: isCreate ? "create" : "edit",
|
|
68
|
+
executedSqlCount,
|
|
69
|
+
fillsImportedRows,
|
|
70
|
+
importedRowCount: fillsImportedRows && Array.isArray(importedRows) ? importedRows.length : 0,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
41
74
|
|
|
42
75
|
res.json(
|
|
43
76
|
successResponse({
|
|
44
77
|
message: isCreate
|
|
45
78
|
? fillsImportedRows
|
|
46
|
-
? "Table created and
|
|
79
|
+
? "Table created and data imported."
|
|
47
80
|
: "Table created."
|
|
48
81
|
: "Table schema updated.",
|
|
49
82
|
data,
|
package/server/server.js
CHANGED
|
@@ -129,13 +129,13 @@ app.use(
|
|
|
129
129
|
nativeFileDialogService,
|
|
130
130
|
})
|
|
131
131
|
);
|
|
132
|
-
app.use("/api/backups", createBackupsRouter({ backupService }));
|
|
132
|
+
app.use("/api/backups", createBackupsRouter({ backupService, appStateStore, connectionManager }));
|
|
133
133
|
app.use("/api/db", createOverviewRouter({ overviewService }));
|
|
134
134
|
app.use("/api/sql", createSqlRouter({ appStateStore, connectionManager, sqlExecutor }));
|
|
135
135
|
app.use("/api/charts", createChartsRouter({ appStateStore, connectionManager, sqlExecutor }));
|
|
136
136
|
app.use("/api/structure", createStructureRouter({ structureService }));
|
|
137
|
-
app.use("/api/data", createDataRouter({ dataBrowserService }));
|
|
138
|
-
app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService }));
|
|
137
|
+
app.use("/api/data", createDataRouter({ dataBrowserService, appStateStore, connectionManager }));
|
|
138
|
+
app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService, appStateStore, connectionManager }));
|
|
139
139
|
app.use("/api/media-tagging", createMediaTaggingRouter({ mediaTaggingService }));
|
|
140
140
|
app.use(
|
|
141
141
|
"/api/settings",
|
|
@@ -76,6 +76,28 @@ const MCP_TOOL_DEFINITIONS = [
|
|
|
76
76
|
["databaseId", "sql"]
|
|
77
77
|
),
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
name: "get_saved_queries",
|
|
81
|
+
description: "List saved SQL Editor queries for a database. This is the MCP equivalent of `sqlite-hub --database:name --queries`.",
|
|
82
|
+
inputSchema: objectSchema({
|
|
83
|
+
databaseId: databaseIdProperty(),
|
|
84
|
+
limit: { type: "integer", minimum: 1, maximum: 500, default: 100 },
|
|
85
|
+
}, ["databaseId"]),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "execute_stored_query",
|
|
89
|
+
description: "Execute a saved SQL Editor query by id, title, display title, or SQL fragment. This is the MCP equivalent of `sqlite-hub --database:name --execute:\"query\"`.",
|
|
90
|
+
inputSchema: objectSchema(
|
|
91
|
+
{
|
|
92
|
+
databaseId: databaseIdProperty(),
|
|
93
|
+
queryName: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "Saved query id, title, display title, or SQL fragment.",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
["databaseId", "queryName"]
|
|
99
|
+
),
|
|
100
|
+
},
|
|
79
101
|
{
|
|
80
102
|
name: "explain_query_plan",
|
|
81
103
|
description: "Run SQLite EXPLAIN QUERY PLAN for a read-only query and return structured plan rows.",
|
|
@@ -142,6 +164,7 @@ function redactConnection(connection = {}) {
|
|
|
142
164
|
label: connection.label,
|
|
143
165
|
readOnly: Boolean(connection.readOnly),
|
|
144
166
|
sizeBytes: connection.sizeBytes ?? null,
|
|
167
|
+
createdAt: connection.createdAt ?? null,
|
|
145
168
|
lastOpenedAt: connection.lastOpenedAt ?? null,
|
|
146
169
|
lastModifiedAt: connection.lastModifiedAt ?? null,
|
|
147
170
|
};
|
|
@@ -154,6 +177,7 @@ function redactOverview(overview = {}) {
|
|
|
154
177
|
file: {
|
|
155
178
|
filename: overview.file?.filename ?? overview.connection?.label ?? null,
|
|
156
179
|
sizeBytes: overview.file?.sizeBytes ?? null,
|
|
180
|
+
createdAt: overview.file?.createdAt ?? null,
|
|
157
181
|
lastModifiedAt: overview.file?.lastModifiedAt ?? null,
|
|
158
182
|
},
|
|
159
183
|
};
|
|
@@ -227,6 +251,12 @@ class McpToolService {
|
|
|
227
251
|
executedBy: "mcp",
|
|
228
252
|
maxRows: args.maxRows,
|
|
229
253
|
});
|
|
254
|
+
case "get_saved_queries":
|
|
255
|
+
return this.databaseService.listSavedQueries(args.databaseId, args.limit);
|
|
256
|
+
case "execute_stored_query":
|
|
257
|
+
return this.databaseService.executeSavedQuery(args.databaseId, args.queryName, {
|
|
258
|
+
executedBy: "mcp",
|
|
259
|
+
});
|
|
230
260
|
case "explain_query_plan":
|
|
231
261
|
return this.databaseService.explainQueryPlan(args.databaseId, args.sql);
|
|
232
262
|
case "read_documents":
|
|
@@ -70,6 +70,7 @@ class ConnectionManager {
|
|
|
70
70
|
label: options.label?.trim() || path.basename(filePath),
|
|
71
71
|
path: filePath,
|
|
72
72
|
lastOpenedAt: new Date().toISOString(),
|
|
73
|
+
createdAt: metadata.createdAt,
|
|
73
74
|
lastModifiedAt: metadata.lastModifiedAt,
|
|
74
75
|
sizeBytes: metadata.sizeBytes,
|
|
75
76
|
readOnly: options.readOnly ?? !isWritable(filePath),
|
|
@@ -216,7 +217,7 @@ class ConnectionManager {
|
|
|
216
217
|
return recentConnections;
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
updateRecentConnection(id, { filePath, label, readOnly = false, logoUpload = null, clearLogo = false }) {
|
|
220
|
+
updateRecentConnection(id, { filePath, label, readOnly = false, logoUpload = null, clearLogo = false, tags = null }) {
|
|
220
221
|
const recentConnections = this.appStateStore.getRecentConnections();
|
|
221
222
|
const existing = recentConnections.find((connection) => connection.id === id);
|
|
222
223
|
|
|
@@ -245,7 +246,7 @@ class ConnectionManager {
|
|
|
245
246
|
|
|
246
247
|
try {
|
|
247
248
|
if (this.current?.id === id) {
|
|
248
|
-
|
|
249
|
+
const connection = this.openConnection({
|
|
249
250
|
filePath: resolvedPath,
|
|
250
251
|
label: normalizedLabel,
|
|
251
252
|
id,
|
|
@@ -253,6 +254,15 @@ class ConnectionManager {
|
|
|
253
254
|
readOnly: normalizedReadOnly,
|
|
254
255
|
logoPath: nextLogoPath,
|
|
255
256
|
});
|
|
257
|
+
|
|
258
|
+
if (Array.isArray(tags)) {
|
|
259
|
+
this.appStateStore.setConnectionTags(id, tags);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
...connection,
|
|
264
|
+
tags: this.appStateStore.getConnectionTags(id),
|
|
265
|
+
};
|
|
256
266
|
}
|
|
257
267
|
|
|
258
268
|
const db = this.openRawDatabase(resolvedPath, {
|
|
@@ -267,6 +277,7 @@ class ConnectionManager {
|
|
|
267
277
|
...existing,
|
|
268
278
|
label: normalizedLabel,
|
|
269
279
|
path: resolvedPath,
|
|
280
|
+
createdAt: metadata.createdAt,
|
|
270
281
|
lastModifiedAt: metadata.lastModifiedAt,
|
|
271
282
|
sizeBytes: metadata.sizeBytes,
|
|
272
283
|
readOnly: normalizedReadOnly,
|
|
@@ -275,9 +286,14 @@ class ConnectionManager {
|
|
|
275
286
|
|
|
276
287
|
this.appStateStore.updateRecentConnection(id, () => nextConnection);
|
|
277
288
|
|
|
289
|
+
if (Array.isArray(tags)) {
|
|
290
|
+
this.appStateStore.setConnectionTags(id, tags);
|
|
291
|
+
}
|
|
292
|
+
|
|
278
293
|
return {
|
|
279
294
|
...nextConnection,
|
|
280
295
|
logoUrl: this.appStateStore.getConnectionLogoUrl(nextLogoPath),
|
|
296
|
+
tags: this.appStateStore.getConnectionTags(id),
|
|
281
297
|
isActive: false,
|
|
282
298
|
};
|
|
283
299
|
} catch (error) {
|
|
@@ -295,8 +311,11 @@ class ConnectionManager {
|
|
|
295
311
|
}
|
|
296
312
|
|
|
297
313
|
const { db, ...connection } = this.current;
|
|
314
|
+
const storedConnection = this.appStateStore.getRecentConnection(connection.id);
|
|
315
|
+
|
|
298
316
|
return {
|
|
299
317
|
...connection,
|
|
318
|
+
tags: storedConnection?.tags ?? [],
|
|
300
319
|
isActive: true,
|
|
301
320
|
};
|
|
302
321
|
}
|
|
@@ -334,6 +353,10 @@ class ConnectionManager {
|
|
|
334
353
|
}));
|
|
335
354
|
}
|
|
336
355
|
|
|
356
|
+
listConnectionTags() {
|
|
357
|
+
return this.appStateStore.listConnectionTags();
|
|
358
|
+
}
|
|
359
|
+
|
|
337
360
|
getStatus() {
|
|
338
361
|
const active = this.getActiveConnection();
|
|
339
362
|
|
|
@@ -83,6 +83,14 @@ function isUnchangedSubmittedValue(currentValue, submittedValue) {
|
|
|
83
83
|
return formatPreviewValue(currentValue) === formatPreviewValue(submittedValue);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function assertDataTableWritable(tableDetail) {
|
|
87
|
+
if (tableDetail.isShadow) {
|
|
88
|
+
throw new ValidationError(
|
|
89
|
+
`Shadow table ${tableDetail.name} is read-only in Data.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
class DataBrowserService {
|
|
87
95
|
constructor({ connectionManager }) {
|
|
88
96
|
this.connectionManager = connectionManager;
|
|
@@ -102,6 +110,9 @@ class DataBrowserService {
|
|
|
102
110
|
return {
|
|
103
111
|
name: entry.name,
|
|
104
112
|
columnCount,
|
|
113
|
+
tableKind: entry.tableKind ?? "table",
|
|
114
|
+
isVirtual: Boolean(entry.isVirtual),
|
|
115
|
+
isShadow: Boolean(entry.isShadow),
|
|
105
116
|
};
|
|
106
117
|
});
|
|
107
118
|
}
|
|
@@ -161,6 +172,10 @@ class DataBrowserService {
|
|
|
161
172
|
return {
|
|
162
173
|
name: tableDetail.name,
|
|
163
174
|
type: tableDetail.type,
|
|
175
|
+
tableKind: tableDetail.tableKind,
|
|
176
|
+
isVirtual: Boolean(tableDetail.isVirtual),
|
|
177
|
+
isShadow: Boolean(tableDetail.isShadow),
|
|
178
|
+
readOnly: Boolean(tableDetail.isShadow),
|
|
164
179
|
rowCount,
|
|
165
180
|
limit,
|
|
166
181
|
offset,
|
|
@@ -253,6 +268,8 @@ class DataBrowserService {
|
|
|
253
268
|
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
254
269
|
const identity = payload.identity ?? null;
|
|
255
270
|
|
|
271
|
+
assertDataTableWritable(tableDetail);
|
|
272
|
+
|
|
256
273
|
if (tableDetail.notSafelyUpdatable) {
|
|
257
274
|
throw new ValidationError(
|
|
258
275
|
`Table ${tableName} cannot be safely updated because it has no stable row identity.`
|
|
@@ -299,6 +316,8 @@ class DataBrowserService {
|
|
|
299
316
|
const db = this.connectionManager.getActiveDatabase();
|
|
300
317
|
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
301
318
|
|
|
319
|
+
assertDataTableWritable(tableDetail);
|
|
320
|
+
|
|
302
321
|
return insertSyntheticRows(db, tableDetail, payload);
|
|
303
322
|
}
|
|
304
323
|
|
|
@@ -351,6 +370,8 @@ class DataBrowserService {
|
|
|
351
370
|
const values = payload.values ?? {};
|
|
352
371
|
const identity = payload.identity ?? null;
|
|
353
372
|
|
|
373
|
+
assertDataTableWritable(tableDetail);
|
|
374
|
+
|
|
354
375
|
if (tableDetail.notSafelyUpdatable) {
|
|
355
376
|
throw new ValidationError(
|
|
356
377
|
`Table ${tableName} cannot be safely updated because it has no stable row identity.`
|
|
@@ -19,6 +19,37 @@ function getTableListMap(db) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function normalizeTableKind(entry = {}, tableListEntry = null) {
|
|
23
|
+
const pragmaType = String(tableListEntry?.type ?? "")
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase();
|
|
26
|
+
|
|
27
|
+
if (pragmaType) {
|
|
28
|
+
return pragmaType;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (entry.type === "table" && /^\s*CREATE\s+VIRTUAL\s+TABLE\b/i.test(entry.sql || "")) {
|
|
32
|
+
return "virtual";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return entry.type;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decorateStructureEntry(entry, tableListMap = null) {
|
|
39
|
+
if (entry.type !== "table") {
|
|
40
|
+
return entry;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const tableKind = normalizeTableKind(entry, tableListMap?.get(entry.name));
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...entry,
|
|
47
|
+
tableKind,
|
|
48
|
+
isVirtual: tableKind === "virtual",
|
|
49
|
+
isShadow: tableKind === "shadow",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
22
53
|
function getMasterEntry(db, type, name) {
|
|
23
54
|
const entry = db
|
|
24
55
|
.prepare(
|
|
@@ -34,6 +65,8 @@ function getMasterEntry(db, type, name) {
|
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
function getRawStructureEntries(db) {
|
|
68
|
+
const tableListMap = getTableListMap(db);
|
|
69
|
+
|
|
37
70
|
return db
|
|
38
71
|
.prepare(
|
|
39
72
|
[
|
|
@@ -43,7 +76,8 @@ function getRawStructureEntries(db) {
|
|
|
43
76
|
"ORDER BY type ASC, name ASC",
|
|
44
77
|
].join(" ")
|
|
45
78
|
)
|
|
46
|
-
.all()
|
|
79
|
+
.all()
|
|
80
|
+
.map((entry) => decorateStructureEntry(entry, tableListMap));
|
|
47
81
|
}
|
|
48
82
|
|
|
49
83
|
function normalizeColumn(column, visibleSet) {
|
|
@@ -607,11 +641,15 @@ function getTableDetail(db, tableName, options = {}) {
|
|
|
607
641
|
typeof tableListEntry?.wr === "number"
|
|
608
642
|
? Boolean(tableListEntry.wr)
|
|
609
643
|
: /WITHOUT\s+ROWID/i.test(entry.sql || "");
|
|
644
|
+
const tableKind = normalizeTableKind(entry, tableListEntry);
|
|
610
645
|
|
|
611
646
|
const tableDetail = {
|
|
612
647
|
type: entry.type,
|
|
613
648
|
name: entry.name,
|
|
614
649
|
ddl: entry.sql,
|
|
650
|
+
tableKind,
|
|
651
|
+
isVirtual: tableKind === "virtual",
|
|
652
|
+
isShadow: tableKind === "shadow",
|
|
615
653
|
withoutRowId,
|
|
616
654
|
strict: Boolean(tableListEntry?.strict),
|
|
617
655
|
columns,
|
|
@@ -3,6 +3,65 @@ const { quoteIdentifier } = require("../../utils/identifier");
|
|
|
3
3
|
const { serializeRows } = require("../../utils/sqliteTypes");
|
|
4
4
|
const { TypeGenerationService } = require("../typeGenerationService");
|
|
5
5
|
|
|
6
|
+
function resolveShadowOwnerTableName(shadowTable, tables) {
|
|
7
|
+
if (!shadowTable?.isShadow) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
tables
|
|
13
|
+
.filter((table) => table.isVirtual && shadowTable.name.startsWith(`${table.name}_`))
|
|
14
|
+
.sort((left, right) => right.name.length - left.name.length)[0]?.name ?? null
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function unquoteSqlIdentifier(identifier) {
|
|
19
|
+
const value = String(identifier ?? "").trim();
|
|
20
|
+
|
|
21
|
+
if (value.length >= 2 && value[0] === '"' && value.at(-1) === '"') {
|
|
22
|
+
return value.slice(1, -1).replace(/""/g, '"');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (value.length >= 2 && value[0] === "`" && value.at(-1) === "`") {
|
|
26
|
+
return value.slice(1, -1).replace(/``/g, "`");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (value.length >= 2 && value[0] === "[" && value.at(-1) === "]") {
|
|
30
|
+
return value.slice(1, -1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractVirtualTableModule(ddl = "") {
|
|
37
|
+
const match = String(ddl ?? "").match(
|
|
38
|
+
/\bUSING\s+("[^"]+(?:""[^"]*)*"|`[^`]+(?:``[^`]*)*`|\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_]*)/i
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return match ? unquoteSqlIdentifier(match[1]) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function serializeGraphTable(table, shadowOwnerTableName = null) {
|
|
45
|
+
const isVirtual = Boolean(table.isVirtual);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
type: table.type,
|
|
49
|
+
name: table.name,
|
|
50
|
+
ddl: table.ddl,
|
|
51
|
+
tableKind: table.tableKind,
|
|
52
|
+
isVirtual,
|
|
53
|
+
isShadow: Boolean(table.isShadow),
|
|
54
|
+
virtualModule: isVirtual ? extractVirtualTableModule(table.ddl) : null,
|
|
55
|
+
shadowOwnerTable: shadowOwnerTableName,
|
|
56
|
+
withoutRowId: table.withoutRowId,
|
|
57
|
+
strict: table.strict,
|
|
58
|
+
columns: table.columns,
|
|
59
|
+
foreignKeys: table.foreignKeys,
|
|
60
|
+
identityStrategy: table.identityStrategy,
|
|
61
|
+
notSafelyUpdatable: table.notSafelyUpdatable,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
6
65
|
class StructureService {
|
|
7
66
|
constructor({ connectionManager, appStateStore }) {
|
|
8
67
|
this.connectionManager = connectionManager;
|
|
@@ -35,17 +94,9 @@ class StructureService {
|
|
|
35
94
|
triggers: entries.filter((entry) => entry.type === "trigger"),
|
|
36
95
|
},
|
|
37
96
|
graph: {
|
|
38
|
-
tables: tables.map((table) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
ddl: table.ddl,
|
|
42
|
-
withoutRowId: table.withoutRowId,
|
|
43
|
-
strict: table.strict,
|
|
44
|
-
columns: table.columns,
|
|
45
|
-
foreignKeys: table.foreignKeys,
|
|
46
|
-
identityStrategy: table.identityStrategy,
|
|
47
|
-
notSafelyUpdatable: table.notSafelyUpdatable,
|
|
48
|
-
})),
|
|
97
|
+
tables: tables.map((table) =>
|
|
98
|
+
serializeGraphTable(table, resolveShadowOwnerTableName(table, tables))
|
|
99
|
+
),
|
|
49
100
|
relationshipCount,
|
|
50
101
|
},
|
|
51
102
|
};
|
|
@@ -65,6 +116,10 @@ class StructureService {
|
|
|
65
116
|
type: table.type,
|
|
66
117
|
name: table.name,
|
|
67
118
|
ddl: table.ddl,
|
|
119
|
+
tableKind: table.tableKind,
|
|
120
|
+
isVirtual: Boolean(table.isVirtual),
|
|
121
|
+
isShadow: Boolean(table.isShadow),
|
|
122
|
+
virtualModule: table.isVirtual ? extractVirtualTableModule(table.ddl) : null,
|
|
68
123
|
withoutRowId: table.withoutRowId,
|
|
69
124
|
strict: table.strict,
|
|
70
125
|
columns: table.columns,
|
|
@@ -763,12 +763,18 @@ function analyzeTable(db, tableName) {
|
|
|
763
763
|
score: calculateScore(issues),
|
|
764
764
|
issueCount: issues.length,
|
|
765
765
|
rowCount: Number(tableDetail.rowCount ?? 0),
|
|
766
|
+
tableKind: tableDetail.tableKind,
|
|
767
|
+
isVirtual: Boolean(tableDetail.isVirtual),
|
|
768
|
+
isShadow: Boolean(tableDetail.isShadow),
|
|
766
769
|
issues,
|
|
767
770
|
columnProfiles,
|
|
768
771
|
table: {
|
|
769
772
|
columnCount: (tableDetail.columns ?? []).filter((column) => column.visible && !column.generated).length,
|
|
770
773
|
indexCount: (tableDetail.indexes ?? []).length,
|
|
771
774
|
foreignKeyCount: (tableDetail.foreignKeys ?? []).length,
|
|
775
|
+
tableKind: tableDetail.tableKind,
|
|
776
|
+
isVirtual: Boolean(tableDetail.isVirtual),
|
|
777
|
+
isShadow: Boolean(tableDetail.isShadow),
|
|
772
778
|
},
|
|
773
779
|
analyzedAt: new Date().toISOString(),
|
|
774
780
|
};
|