sqlite-hub 0.6.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.
- package/README.md +107 -0
- package/bin/sqlite-hub.js +285 -4
- package/changelog.md +7 -0
- package/frontend/assets/mockups/sql_editor_croped.png +0 -0
- package/frontend/index.html +1 -0
- package/frontend/js/api.js +34 -0
- package/frontend/js/app.js +99 -2
- package/frontend/js/components/modal.js +314 -1
- package/frontend/js/components/queryChartRenderer.js +125 -0
- package/frontend/js/components/queryEditor.js +35 -6
- package/frontend/js/components/queryHistoryPanel.js +16 -5
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/components/tableDesignerSidebar.js +41 -38
- package/frontend/js/lib/queryChartOptions.js +283 -0
- package/frontend/js/lib/queryCharts.js +560 -0
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +618 -0
- package/frontend/js/views/charts.js +471 -0
- package/frontend/js/views/data.js +29 -15
- package/frontend/js/views/editor.js +19 -13
- package/frontend/js/views/structure.js +81 -39
- package/frontend/styles/components.css +5 -13
- package/frontend/styles/views.css +296 -66
- package/package.json +2 -1
- package/server/routes/charts.js +153 -0
- package/server/server.js +6 -0
- package/server/services/storage/appStateStore.js +400 -1
- package/server/services/storage/queryHistoryChartUtils.js +145 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const {
|
|
3
|
+
DatabaseRequiredError,
|
|
4
|
+
route,
|
|
5
|
+
successResponse,
|
|
6
|
+
} = require("../utils/errors");
|
|
7
|
+
|
|
8
|
+
function getActiveDatabaseKey(connectionManager) {
|
|
9
|
+
return connectionManager.getActiveConnection()?.id ?? null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function requireActiveDatabaseKey(connectionManager) {
|
|
13
|
+
const databaseKey = getActiveDatabaseKey(connectionManager);
|
|
14
|
+
|
|
15
|
+
if (!databaseKey) {
|
|
16
|
+
throw new DatabaseRequiredError();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return databaseKey;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createChartsRouter({ appStateStore, connectionManager, sqlExecutor }) {
|
|
23
|
+
const router = express.Router();
|
|
24
|
+
|
|
25
|
+
router.get(
|
|
26
|
+
"/query-history",
|
|
27
|
+
route((req, res) => {
|
|
28
|
+
const databaseKey = requireActiveDatabaseKey(connectionManager);
|
|
29
|
+
|
|
30
|
+
res.json(
|
|
31
|
+
successResponse({
|
|
32
|
+
data: appStateStore.getChartQueryHistoryList(databaseKey),
|
|
33
|
+
metadata: { databaseKey },
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
router.get(
|
|
40
|
+
"/query-history/:historyId",
|
|
41
|
+
route((req, res) => {
|
|
42
|
+
const databaseKey = requireActiveDatabaseKey(connectionManager);
|
|
43
|
+
|
|
44
|
+
res.json(
|
|
45
|
+
successResponse({
|
|
46
|
+
data: appStateStore.getQueryHistoryChartsDetail(req.params.historyId, databaseKey),
|
|
47
|
+
metadata: {
|
|
48
|
+
databaseKey,
|
|
49
|
+
historyId: Number(req.params.historyId),
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
router.post(
|
|
57
|
+
"/query-history/:historyId/execute",
|
|
58
|
+
route((req, res) => {
|
|
59
|
+
const databaseKey = requireActiveDatabaseKey(connectionManager);
|
|
60
|
+
const item = appStateStore.getQueryHistoryItemForDatabase(req.params.historyId, databaseKey);
|
|
61
|
+
const result = sqlExecutor.execute(item.rawSql, {
|
|
62
|
+
persistHistory: false,
|
|
63
|
+
requireReader: true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
res.json(
|
|
67
|
+
successResponse({
|
|
68
|
+
message: "Query results loaded for charts.",
|
|
69
|
+
data: {
|
|
70
|
+
...result,
|
|
71
|
+
queryHistoryId: item.id,
|
|
72
|
+
},
|
|
73
|
+
metadata: {
|
|
74
|
+
databaseKey,
|
|
75
|
+
historyId: item.id,
|
|
76
|
+
},
|
|
77
|
+
timingMs: result.timingMs,
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
router.post(
|
|
84
|
+
"/",
|
|
85
|
+
route((req, res) => {
|
|
86
|
+
const databaseKey = requireActiveDatabaseKey(connectionManager);
|
|
87
|
+
const chart = appStateStore.createQueryHistoryChart({
|
|
88
|
+
databaseKey,
|
|
89
|
+
queryHistoryId: req.body?.queryHistoryId,
|
|
90
|
+
name: req.body?.name,
|
|
91
|
+
chartType: req.body?.chartType,
|
|
92
|
+
config: req.body?.config,
|
|
93
|
+
resultColumns: req.body?.resultColumns,
|
|
94
|
+
tableVisible: req.body?.tableVisible,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
res.json(
|
|
98
|
+
successResponse({
|
|
99
|
+
message: "Chart created.",
|
|
100
|
+
data: chart,
|
|
101
|
+
metadata: { databaseKey },
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
router.patch(
|
|
108
|
+
"/:chartId",
|
|
109
|
+
route((req, res) => {
|
|
110
|
+
const databaseKey = requireActiveDatabaseKey(connectionManager);
|
|
111
|
+
const chart = appStateStore.updateQueryHistoryChart(req.params.chartId, {
|
|
112
|
+
databaseKey,
|
|
113
|
+
name: req.body?.name,
|
|
114
|
+
chartType: req.body?.chartType,
|
|
115
|
+
config: req.body?.config,
|
|
116
|
+
resultColumns: req.body?.resultColumns,
|
|
117
|
+
tableVisible: req.body?.tableVisible,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
res.json(
|
|
121
|
+
successResponse({
|
|
122
|
+
message: "Chart updated.",
|
|
123
|
+
data: chart,
|
|
124
|
+
metadata: { databaseKey },
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
router.delete(
|
|
131
|
+
"/:chartId",
|
|
132
|
+
route((req, res) => {
|
|
133
|
+
const databaseKey = requireActiveDatabaseKey(connectionManager);
|
|
134
|
+
appStateStore.deleteQueryHistoryChart(req.params.chartId, databaseKey);
|
|
135
|
+
|
|
136
|
+
res.json(
|
|
137
|
+
successResponse({
|
|
138
|
+
message: "Chart deleted.",
|
|
139
|
+
data: {
|
|
140
|
+
id: Number(req.params.chartId),
|
|
141
|
+
},
|
|
142
|
+
metadata: { databaseKey },
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return router;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
createChartsRouter,
|
|
153
|
+
};
|
package/server/server.js
CHANGED
|
@@ -15,6 +15,7 @@ const { TableDesignerService } = require("./services/sqlite/tableDesignerService
|
|
|
15
15
|
const { createConnectionsRouter } = require("./routes/connections");
|
|
16
16
|
const { createOverviewRouter } = require("./routes/overview");
|
|
17
17
|
const { createSqlRouter } = require("./routes/sql");
|
|
18
|
+
const { createChartsRouter } = require("./routes/charts");
|
|
18
19
|
const { createStructureRouter } = require("./routes/structure");
|
|
19
20
|
const { createDataRouter } = require("./routes/data");
|
|
20
21
|
const { createTableDesignerRouter } = require("./routes/tableDesigner");
|
|
@@ -79,6 +80,7 @@ app.use(
|
|
|
79
80
|
);
|
|
80
81
|
app.use("/api/db", createOverviewRouter({ overviewService }));
|
|
81
82
|
app.use("/api/sql", createSqlRouter({ appStateStore, connectionManager, sqlExecutor }));
|
|
83
|
+
app.use("/api/charts", createChartsRouter({ appStateStore, connectionManager, sqlExecutor }));
|
|
82
84
|
app.use("/api/structure", createStructureRouter({ structureService }));
|
|
83
85
|
app.use("/api/data", createDataRouter({ dataBrowserService }));
|
|
84
86
|
app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService }));
|
|
@@ -109,6 +111,10 @@ app.use(
|
|
|
109
111
|
"/vendor/elkjs",
|
|
110
112
|
express.static(path.resolve(__dirname, "..", "node_modules", "elkjs"))
|
|
111
113
|
);
|
|
114
|
+
app.use(
|
|
115
|
+
"/vendor/echarts",
|
|
116
|
+
express.static(path.resolve(__dirname, "..", "node_modules", "echarts"))
|
|
117
|
+
);
|
|
112
118
|
app.use(express.static(FRONTEND_ROOT));
|
|
113
119
|
app.use("/db_logos", express.static(path.join(APP_STATE_DIRECTORY, "db_logos")));
|
|
114
120
|
app.use(errorMiddleware);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
2
|
const path = require("node:path");
|
|
3
3
|
const Database = require("better-sqlite3");
|
|
4
|
-
const { NotFoundError, ValidationError } = require("../../utils/errors");
|
|
4
|
+
const { ConflictError, NotFoundError, ValidationError } = require("../../utils/errors");
|
|
5
5
|
const {
|
|
6
6
|
buildAutoTitle,
|
|
7
7
|
buildSqlPreview,
|
|
@@ -10,6 +10,13 @@ const {
|
|
|
10
10
|
isDestructiveQuery,
|
|
11
11
|
normalizeSql,
|
|
12
12
|
} = require("./queryHistoryUtils");
|
|
13
|
+
const {
|
|
14
|
+
buildDefaultChartName,
|
|
15
|
+
normalizeChartConfig,
|
|
16
|
+
normalizeChartName,
|
|
17
|
+
normalizeChartType,
|
|
18
|
+
normalizeResultColumns,
|
|
19
|
+
} = require("./queryHistoryChartUtils");
|
|
13
20
|
|
|
14
21
|
const DEFAULT_STATE = {
|
|
15
22
|
recentConnections: [],
|
|
@@ -144,6 +151,20 @@ class AppStateStore {
|
|
|
144
151
|
FOREIGN KEY (history_id) REFERENCES query_history(id) ON DELETE CASCADE
|
|
145
152
|
);
|
|
146
153
|
|
|
154
|
+
CREATE TABLE IF NOT EXISTS query_history_chart (
|
|
155
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
156
|
+
query_history_id INTEGER NOT NULL,
|
|
157
|
+
name TEXT NOT NULL,
|
|
158
|
+
chart_type TEXT NOT NULL CHECK (chart_type IN ('bar', 'line', 'pie', 'scatter')),
|
|
159
|
+
config_json TEXT NOT NULL,
|
|
160
|
+
result_columns_json TEXT NOT NULL DEFAULT '[]',
|
|
161
|
+
table_visible INTEGER NOT NULL DEFAULT 1,
|
|
162
|
+
created_at TEXT NOT NULL,
|
|
163
|
+
updated_at TEXT NOT NULL,
|
|
164
|
+
FOREIGN KEY (query_history_id) REFERENCES query_history(id) ON DELETE CASCADE,
|
|
165
|
+
UNIQUE(query_history_id, name)
|
|
166
|
+
);
|
|
167
|
+
|
|
147
168
|
CREATE INDEX IF NOT EXISTS idx_query_history_database_key
|
|
148
169
|
ON query_history(database_key);
|
|
149
170
|
|
|
@@ -170,6 +191,15 @@ class AppStateStore {
|
|
|
170
191
|
|
|
171
192
|
CREATE INDEX IF NOT EXISTS idx_query_runs_status
|
|
172
193
|
ON query_runs(status);
|
|
194
|
+
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_query_history_chart_query_history_id
|
|
196
|
+
ON query_history_chart(query_history_id);
|
|
197
|
+
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_query_history_chart_query_history_updated_at
|
|
199
|
+
ON query_history_chart(query_history_id, updated_at DESC);
|
|
200
|
+
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_query_history_chart_chart_type
|
|
202
|
+
ON query_history_chart(chart_type);
|
|
173
203
|
`);
|
|
174
204
|
|
|
175
205
|
const recentConnectionColumns = new Set(
|
|
@@ -528,6 +558,10 @@ class AppStateStore {
|
|
|
528
558
|
const queryType = row.query_type ?? row.queryType ?? "other";
|
|
529
559
|
const title = this.normalizeQueryHistoryText(row.title);
|
|
530
560
|
const notes = this.normalizeQueryHistoryText(row.notes);
|
|
561
|
+
const chartTypes = String(row.chart_types ?? row.chartTypes ?? "")
|
|
562
|
+
.split(",")
|
|
563
|
+
.map((entry) => String(entry ?? "").trim().toLowerCase())
|
|
564
|
+
.filter((entry, index, values) => entry && values.indexOf(entry) === index);
|
|
531
565
|
const lastRun =
|
|
532
566
|
row.last_run_id ?? row.lastRunId
|
|
533
567
|
? this.decorateQueryRun({
|
|
@@ -557,6 +591,8 @@ class AppStateStore {
|
|
|
557
591
|
useCount: Number(row.use_count ?? row.useCount ?? 0),
|
|
558
592
|
firstExecutedAt: row.first_executed_at ?? row.firstExecutedAt ?? null,
|
|
559
593
|
lastUsedAt: row.last_used_at ?? row.lastUsedAt ?? null,
|
|
594
|
+
chartCount: Number(row.chart_count ?? row.chartCount ?? 0),
|
|
595
|
+
chartTypes,
|
|
560
596
|
displayTitle:
|
|
561
597
|
title ||
|
|
562
598
|
buildAutoTitle(row.raw_sql ?? row.rawSql, {
|
|
@@ -568,6 +604,58 @@ class AppStateStore {
|
|
|
568
604
|
};
|
|
569
605
|
}
|
|
570
606
|
|
|
607
|
+
parseQueryHistoryChartConfig(value) {
|
|
608
|
+
if (!value) {
|
|
609
|
+
return {};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
613
|
+
return value;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const parsed = JSON.parse(value);
|
|
618
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
619
|
+
} catch {
|
|
620
|
+
return {};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
parseQueryHistoryChartResultColumns(value) {
|
|
625
|
+
if (Array.isArray(value)) {
|
|
626
|
+
return normalizeResultColumns(value);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!value) {
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
try {
|
|
634
|
+
return normalizeResultColumns(JSON.parse(value));
|
|
635
|
+
} catch {
|
|
636
|
+
return [];
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
decorateQueryHistoryChartRow(row = {}) {
|
|
641
|
+
return {
|
|
642
|
+
id: Number(row.id),
|
|
643
|
+
queryHistoryId: Number(row.query_history_id ?? row.queryHistoryId),
|
|
644
|
+
name: String(row.name ?? ""),
|
|
645
|
+
chartType: normalizeChartType(row.chart_type ?? row.chartType ?? "bar"),
|
|
646
|
+
config: normalizeChartConfig(
|
|
647
|
+
row.chart_type ?? row.chartType ?? "bar",
|
|
648
|
+
this.parseQueryHistoryChartConfig(row.config_json ?? row.configJson)
|
|
649
|
+
),
|
|
650
|
+
resultColumns: this.parseQueryHistoryChartResultColumns(
|
|
651
|
+
row.result_columns_json ?? row.resultColumnsJson
|
|
652
|
+
),
|
|
653
|
+
tableVisible: Boolean(row.table_visible ?? row.tableVisible),
|
|
654
|
+
createdAt: row.created_at ?? row.createdAt ?? null,
|
|
655
|
+
updatedAt: row.updated_at ?? row.updatedAt ?? null,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
571
659
|
buildQueryHistoryFilters({
|
|
572
660
|
databaseKey,
|
|
573
661
|
search,
|
|
@@ -952,6 +1040,65 @@ class AppStateStore {
|
|
|
952
1040
|
});
|
|
953
1041
|
}
|
|
954
1042
|
|
|
1043
|
+
getChartQueryHistoryList(databaseKey) {
|
|
1044
|
+
const normalizedDatabaseKey = this.normalizeQueryHistoryText(databaseKey);
|
|
1045
|
+
|
|
1046
|
+
if (!normalizedDatabaseKey) {
|
|
1047
|
+
return [];
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return this.db
|
|
1051
|
+
.prepare(`
|
|
1052
|
+
SELECT
|
|
1053
|
+
q.id,
|
|
1054
|
+
q.database_key,
|
|
1055
|
+
q.normalized_sql,
|
|
1056
|
+
q.raw_sql,
|
|
1057
|
+
q.title,
|
|
1058
|
+
q.notes,
|
|
1059
|
+
q.query_type,
|
|
1060
|
+
q.tables_detected,
|
|
1061
|
+
q.is_favorite,
|
|
1062
|
+
q.is_saved,
|
|
1063
|
+
q.is_destructive,
|
|
1064
|
+
q.use_count,
|
|
1065
|
+
q.first_executed_at,
|
|
1066
|
+
q.last_used_at,
|
|
1067
|
+
charts.chart_count,
|
|
1068
|
+
charts.chart_types,
|
|
1069
|
+
latest.id AS last_run_id,
|
|
1070
|
+
latest.executed_at AS last_run_executed_at,
|
|
1071
|
+
latest.duration_ms AS last_run_duration_ms,
|
|
1072
|
+
latest.row_count AS last_run_row_count,
|
|
1073
|
+
latest.status AS last_run_status,
|
|
1074
|
+
latest.error_message AS last_run_error_message,
|
|
1075
|
+
latest.affected_rows AS last_run_affected_rows
|
|
1076
|
+
FROM query_history q
|
|
1077
|
+
LEFT JOIN (
|
|
1078
|
+
SELECT
|
|
1079
|
+
query_history_id,
|
|
1080
|
+
COUNT(*) AS chart_count,
|
|
1081
|
+
GROUP_CONCAT(DISTINCT chart_type) AS chart_types
|
|
1082
|
+
FROM query_history_chart
|
|
1083
|
+
GROUP BY query_history_id
|
|
1084
|
+
) charts
|
|
1085
|
+
ON charts.query_history_id = q.id
|
|
1086
|
+
LEFT JOIN query_runs latest
|
|
1087
|
+
ON latest.id = (
|
|
1088
|
+
SELECT runs.id
|
|
1089
|
+
FROM query_runs runs
|
|
1090
|
+
WHERE runs.history_id = q.id
|
|
1091
|
+
ORDER BY runs.executed_at DESC, runs.id DESC
|
|
1092
|
+
LIMIT 1
|
|
1093
|
+
)
|
|
1094
|
+
WHERE q.database_key = ?
|
|
1095
|
+
AND COALESCE(latest.status, 'success') != 'error'
|
|
1096
|
+
ORDER BY q.last_used_at DESC, q.id DESC
|
|
1097
|
+
`)
|
|
1098
|
+
.all(normalizedDatabaseKey)
|
|
1099
|
+
.map((row) => this.decorateQueryHistoryRow(row));
|
|
1100
|
+
}
|
|
1101
|
+
|
|
955
1102
|
getQueryHistoryItemById(historyId) {
|
|
956
1103
|
const row = this.db
|
|
957
1104
|
.prepare(`
|
|
@@ -997,6 +1144,17 @@ class AppStateStore {
|
|
|
997
1144
|
return this.decorateQueryHistoryRow(row);
|
|
998
1145
|
}
|
|
999
1146
|
|
|
1147
|
+
getQueryHistoryItemForDatabase(historyId, databaseKey) {
|
|
1148
|
+
const item = this.getQueryHistoryItemById(historyId);
|
|
1149
|
+
const normalizedDatabaseKey = this.normalizeQueryHistoryText(databaseKey);
|
|
1150
|
+
|
|
1151
|
+
if (normalizedDatabaseKey && item.databaseKey !== normalizedDatabaseKey) {
|
|
1152
|
+
throw new NotFoundError(`Query history item not found: ${historyId}`);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return item;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1000
1158
|
getQueryRunsByHistoryId(historyId, limit = 8) {
|
|
1001
1159
|
const normalizedLimit = Math.max(1, Math.min(50, Number(limit) || 8));
|
|
1002
1160
|
|
|
@@ -1020,6 +1178,95 @@ class AppStateStore {
|
|
|
1020
1178
|
.map((row) => this.decorateQueryRun(row));
|
|
1021
1179
|
}
|
|
1022
1180
|
|
|
1181
|
+
getQueryHistoryChartsByHistoryId(historyId) {
|
|
1182
|
+
return this.db
|
|
1183
|
+
.prepare(`
|
|
1184
|
+
SELECT
|
|
1185
|
+
id,
|
|
1186
|
+
query_history_id,
|
|
1187
|
+
name,
|
|
1188
|
+
chart_type,
|
|
1189
|
+
config_json,
|
|
1190
|
+
result_columns_json,
|
|
1191
|
+
table_visible,
|
|
1192
|
+
created_at,
|
|
1193
|
+
updated_at
|
|
1194
|
+
FROM query_history_chart
|
|
1195
|
+
WHERE query_history_id = ?
|
|
1196
|
+
ORDER BY created_at ASC, id ASC
|
|
1197
|
+
`)
|
|
1198
|
+
.all(Number(historyId))
|
|
1199
|
+
.map((row) => this.decorateQueryHistoryChartRow(row));
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
getQueryHistoryChartById(chartId) {
|
|
1203
|
+
const row = this.db
|
|
1204
|
+
.prepare(`
|
|
1205
|
+
SELECT
|
|
1206
|
+
c.id,
|
|
1207
|
+
c.query_history_id,
|
|
1208
|
+
c.name,
|
|
1209
|
+
c.chart_type,
|
|
1210
|
+
c.config_json,
|
|
1211
|
+
c.result_columns_json,
|
|
1212
|
+
c.table_visible,
|
|
1213
|
+
c.created_at,
|
|
1214
|
+
c.updated_at
|
|
1215
|
+
FROM query_history_chart c
|
|
1216
|
+
WHERE c.id = ?
|
|
1217
|
+
`)
|
|
1218
|
+
.get(Number(chartId));
|
|
1219
|
+
|
|
1220
|
+
if (!row) {
|
|
1221
|
+
throw new NotFoundError(`Query history chart not found: ${chartId}`);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
return this.decorateQueryHistoryChartRow(row);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
getQueryHistoryChartForDatabase(chartId, databaseKey) {
|
|
1228
|
+
const row = this.db
|
|
1229
|
+
.prepare(`
|
|
1230
|
+
SELECT
|
|
1231
|
+
c.id,
|
|
1232
|
+
c.query_history_id,
|
|
1233
|
+
c.name,
|
|
1234
|
+
c.chart_type,
|
|
1235
|
+
c.config_json,
|
|
1236
|
+
c.result_columns_json,
|
|
1237
|
+
c.table_visible,
|
|
1238
|
+
c.created_at,
|
|
1239
|
+
c.updated_at,
|
|
1240
|
+
q.database_key
|
|
1241
|
+
FROM query_history_chart c
|
|
1242
|
+
INNER JOIN query_history q
|
|
1243
|
+
ON q.id = c.query_history_id
|
|
1244
|
+
WHERE c.id = ?
|
|
1245
|
+
`)
|
|
1246
|
+
.get(Number(chartId));
|
|
1247
|
+
|
|
1248
|
+
if (!row) {
|
|
1249
|
+
throw new NotFoundError(`Query history chart not found: ${chartId}`);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const normalizedDatabaseKey = this.normalizeQueryHistoryText(databaseKey);
|
|
1253
|
+
|
|
1254
|
+
if (normalizedDatabaseKey && row.database_key !== normalizedDatabaseKey) {
|
|
1255
|
+
throw new NotFoundError(`Query history chart not found: ${chartId}`);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
return this.decorateQueryHistoryChartRow(row);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
getQueryHistoryChartsDetail(historyId, databaseKey) {
|
|
1262
|
+
const item = this.getQueryHistoryItemForDatabase(historyId, databaseKey);
|
|
1263
|
+
|
|
1264
|
+
return {
|
|
1265
|
+
item,
|
|
1266
|
+
charts: this.getQueryHistoryChartsByHistoryId(item.id),
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1023
1270
|
updateQueryHistoryField(historyId, fieldName, value) {
|
|
1024
1271
|
const result = this.db
|
|
1025
1272
|
.prepare(`UPDATE query_history SET ${fieldName} = ? WHERE id = ?`)
|
|
@@ -1032,6 +1279,158 @@ class AppStateStore {
|
|
|
1032
1279
|
return this.getQueryHistoryItemById(historyId);
|
|
1033
1280
|
}
|
|
1034
1281
|
|
|
1282
|
+
resolveUniqueQueryHistoryChartName(queryHistoryId, candidateName, { excludeChartId = null } = {}) {
|
|
1283
|
+
const baseName = normalizeChartName(candidateName) || "Chart";
|
|
1284
|
+
let nextName = baseName;
|
|
1285
|
+
let suffix = 2;
|
|
1286
|
+
|
|
1287
|
+
while (true) {
|
|
1288
|
+
const row = this.db
|
|
1289
|
+
.prepare(`
|
|
1290
|
+
SELECT id
|
|
1291
|
+
FROM query_history_chart
|
|
1292
|
+
WHERE query_history_id = ?
|
|
1293
|
+
AND name = ?
|
|
1294
|
+
${excludeChartId ? "AND id != ?" : ""}
|
|
1295
|
+
LIMIT 1
|
|
1296
|
+
`)
|
|
1297
|
+
.get(
|
|
1298
|
+
Number(queryHistoryId),
|
|
1299
|
+
nextName,
|
|
1300
|
+
...(excludeChartId ? [Number(excludeChartId)] : [])
|
|
1301
|
+
);
|
|
1302
|
+
|
|
1303
|
+
if (!row) {
|
|
1304
|
+
return nextName;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
nextName = `${baseName}_${suffix}`;
|
|
1308
|
+
suffix += 1;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
createQueryHistoryChart({
|
|
1313
|
+
queryHistoryId,
|
|
1314
|
+
name,
|
|
1315
|
+
chartType,
|
|
1316
|
+
config,
|
|
1317
|
+
resultColumns = [],
|
|
1318
|
+
tableVisible = true,
|
|
1319
|
+
databaseKey = null,
|
|
1320
|
+
} = {}) {
|
|
1321
|
+
const item = this.getQueryHistoryItemForDatabase(queryHistoryId, databaseKey);
|
|
1322
|
+
const normalizedChartType = normalizeChartType(chartType);
|
|
1323
|
+
const normalizedConfig = normalizeChartConfig(normalizedChartType, config);
|
|
1324
|
+
const normalizedResultColumns = normalizeResultColumns(resultColumns);
|
|
1325
|
+
const requestedName =
|
|
1326
|
+
normalizeChartName(name) || buildDefaultChartName(normalizedChartType, item.displayTitle);
|
|
1327
|
+
const uniqueName = this.resolveUniqueQueryHistoryChartName(item.id, requestedName);
|
|
1328
|
+
const timestamp = new Date().toISOString();
|
|
1329
|
+
const insertResult = this.db
|
|
1330
|
+
.prepare(`
|
|
1331
|
+
INSERT INTO query_history_chart (
|
|
1332
|
+
query_history_id,
|
|
1333
|
+
name,
|
|
1334
|
+
chart_type,
|
|
1335
|
+
config_json,
|
|
1336
|
+
result_columns_json,
|
|
1337
|
+
table_visible,
|
|
1338
|
+
created_at,
|
|
1339
|
+
updated_at
|
|
1340
|
+
)
|
|
1341
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1342
|
+
`)
|
|
1343
|
+
.run(
|
|
1344
|
+
item.id,
|
|
1345
|
+
uniqueName,
|
|
1346
|
+
normalizedChartType,
|
|
1347
|
+
JSON.stringify(normalizedConfig),
|
|
1348
|
+
JSON.stringify(normalizedResultColumns),
|
|
1349
|
+
tableVisible ? 1 : 0,
|
|
1350
|
+
timestamp,
|
|
1351
|
+
timestamp
|
|
1352
|
+
);
|
|
1353
|
+
|
|
1354
|
+
return this.getQueryHistoryChartById(insertResult.lastInsertRowid);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
updateQueryHistoryChart(
|
|
1358
|
+
chartId,
|
|
1359
|
+
{ name, chartType, config, resultColumns, tableVisible, databaseKey = null } = {}
|
|
1360
|
+
) {
|
|
1361
|
+
const existing = this.getQueryHistoryChartForDatabase(chartId, databaseKey);
|
|
1362
|
+
const nextChartType = chartType ? normalizeChartType(chartType) : existing.chartType;
|
|
1363
|
+
const nextName =
|
|
1364
|
+
name === undefined ? existing.name : normalizeChartName(name);
|
|
1365
|
+
|
|
1366
|
+
if (!nextName) {
|
|
1367
|
+
throw new ValidationError("Chart name is required.");
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const conflicting = this.db
|
|
1371
|
+
.prepare(`
|
|
1372
|
+
SELECT id
|
|
1373
|
+
FROM query_history_chart
|
|
1374
|
+
WHERE query_history_id = ?
|
|
1375
|
+
AND name = ?
|
|
1376
|
+
AND id != ?
|
|
1377
|
+
LIMIT 1
|
|
1378
|
+
`)
|
|
1379
|
+
.get(existing.queryHistoryId, nextName, existing.id);
|
|
1380
|
+
|
|
1381
|
+
if (conflicting) {
|
|
1382
|
+
throw new ConflictError(`A chart named "${nextName}" already exists for this query.`);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const nextConfig = normalizeChartConfig(
|
|
1386
|
+
nextChartType,
|
|
1387
|
+
config === undefined ? existing.config : config
|
|
1388
|
+
);
|
|
1389
|
+
const nextResultColumns =
|
|
1390
|
+
resultColumns === undefined ? existing.resultColumns : normalizeResultColumns(resultColumns);
|
|
1391
|
+
const nextTableVisible =
|
|
1392
|
+
tableVisible === undefined ? existing.tableVisible : Boolean(tableVisible);
|
|
1393
|
+
const timestamp = new Date().toISOString();
|
|
1394
|
+
|
|
1395
|
+
this.db
|
|
1396
|
+
.prepare(`
|
|
1397
|
+
UPDATE query_history_chart
|
|
1398
|
+
SET
|
|
1399
|
+
name = ?,
|
|
1400
|
+
chart_type = ?,
|
|
1401
|
+
config_json = ?,
|
|
1402
|
+
result_columns_json = ?,
|
|
1403
|
+
table_visible = ?,
|
|
1404
|
+
updated_at = ?
|
|
1405
|
+
WHERE id = ?
|
|
1406
|
+
`)
|
|
1407
|
+
.run(
|
|
1408
|
+
nextName,
|
|
1409
|
+
nextChartType,
|
|
1410
|
+
JSON.stringify(nextConfig),
|
|
1411
|
+
JSON.stringify(nextResultColumns),
|
|
1412
|
+
nextTableVisible ? 1 : 0,
|
|
1413
|
+
timestamp,
|
|
1414
|
+
existing.id
|
|
1415
|
+
);
|
|
1416
|
+
|
|
1417
|
+
return this.getQueryHistoryChartById(existing.id);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
deleteQueryHistoryChart(chartId, databaseKey = null) {
|
|
1421
|
+
this.getQueryHistoryChartForDatabase(chartId, databaseKey);
|
|
1422
|
+
|
|
1423
|
+
const result = this.db
|
|
1424
|
+
.prepare("DELETE FROM query_history_chart WHERE id = ?")
|
|
1425
|
+
.run(Number(chartId));
|
|
1426
|
+
|
|
1427
|
+
if (!result.changes) {
|
|
1428
|
+
throw new NotFoundError(`Query history chart not found: ${chartId}`);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
return true;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1035
1434
|
toggleFavorite(historyId, nextValue) {
|
|
1036
1435
|
return this.updateQueryHistoryField(historyId, "is_favorite", nextValue ? 1 : 0);
|
|
1037
1436
|
}
|