sqlite-hub 0.3.2 → 0.5.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 +5 -5
- package/changelog.md +14 -0
- package/{js → frontend/js}/api.js +99 -5
- package/{js → frontend/js}/app.js +162 -11
- package/{js → frontend/js}/components/connectionCard.js +8 -9
- package/frontend/js/components/connectionLogo.js +33 -0
- package/{js → frontend/js}/components/dataGrid.js +3 -3
- package/{js → frontend/js}/components/emptyState.js +25 -11
- package/{js → frontend/js}/components/modal.js +57 -0
- package/{js → frontend/js}/components/queryEditor.js +33 -55
- package/frontend/js/components/queryHistoryDetail.js +263 -0
- package/frontend/js/components/queryHistoryPanel.js +228 -0
- package/{js → frontend/js}/components/queryResults.js +32 -46
- package/{js → frontend/js}/components/rowEditorPanel.js +73 -14
- package/{js → frontend/js}/components/sidebar.js +8 -3
- package/{js → frontend/js}/store.js +577 -21
- package/{js → frontend/js}/utils/format.js +23 -0
- package/{js → frontend/js}/views/data.js +136 -10
- package/{js → frontend/js}/views/editor.js +34 -10
- package/{js → frontend/js}/views/overview.js +15 -0
- package/{js → frontend/js}/views/structure.js +10 -12
- package/{styles → frontend/styles}/components.css +106 -0
- package/{styles → frontend/styles}/structure-graph.css +5 -10
- package/package.json +2 -2
- package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
- package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
- package/server/data/db_logos/.gitkeep +0 -0
- package/server/routes/connections.js +2 -0
- package/server/routes/data.js +2 -0
- package/server/routes/export.js +4 -1
- package/server/routes/overview.js +12 -0
- package/server/routes/sql.js +163 -5
- package/server/server.js +8 -6
- package/server/services/sqlite/connectionManager.js +68 -33
- package/server/services/sqlite/dataBrowserService.js +4 -16
- package/server/services/sqlite/exportService.js +4 -16
- package/server/services/sqlite/overviewService.js +34 -0
- package/server/services/sqlite/sqlExecutor.js +83 -63
- package/server/services/sqlite/tableSort.js +63 -0
- package/server/services/storage/appStateStore.js +832 -20
- package/server/services/storage/queryHistoryUtils.js +169 -0
- package/server/utils/appPaths.js +42 -18
- package/{assets → frontend/assets}/images/logo.webp +0 -0
- package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
- package/{assets → frontend/assets}/images/logo_raw.png +0 -0
- package/{assets → frontend/assets}/images/logo_small.webp +0 -0
- package/{assets → frontend/assets}/mockups/connections.png +0 -0
- package/{assets → frontend/assets}/mockups/data.png +0 -0
- package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
- package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
- package/{assets → frontend/assets}/mockups/home.png +0 -0
- package/{assets → frontend/assets}/mockups/overview.png +0 -0
- package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
- package/{assets → frontend/assets}/mockups/structure.png +0 -0
- package/{index.html → frontend/index.html} +0 -0
- package/{js → frontend/js}/components/actionBar.js +0 -0
- package/{js → frontend/js}/components/appShell.js +0 -0
- package/{js → frontend/js}/components/badges.js +0 -0
- package/{js → frontend/js}/components/bottomTabs.js +1 -1
- /package/{js → frontend/js}/components/metricCard.js +0 -0
- /package/{js → frontend/js}/components/pageHeader.js +0 -0
- /package/{js → frontend/js}/components/statusBar.js +0 -0
- /package/{js → frontend/js}/components/structureGraph.js +0 -0
- /package/{js → frontend/js}/components/toast.js +0 -0
- /package/{js → frontend/js}/components/topNav.js +0 -0
- /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
- /package/{js → frontend/js}/router.js +0 -0
- /package/{js → frontend/js}/views/connections.js +0 -0
- /package/{js → frontend/js}/views/landing.js +0 -0
- /package/{js → frontend/js}/views/settings.js +0 -0
- /package/{styles → frontend/styles}/base.css +0 -0
- /package/{styles → frontend/styles}/layout.css +0 -0
- /package/{styles → frontend/styles}/tokens.css +0 -0
- /package/{styles → frontend/styles}/views.css +0 -0
- /package/{data → server/data}/.gitkeep +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
function stripLineComments(sql = "") {
|
|
2
|
+
return String(sql)
|
|
3
|
+
.split(/\r?\n/)
|
|
4
|
+
.map((line) => line.replace(/--.*$/g, ""))
|
|
5
|
+
.join("\n");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function stripBlockComments(sql = "") {
|
|
9
|
+
return String(sql).replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function compactWhitespace(value = "") {
|
|
13
|
+
return String(value).replace(/\s+/g, " ").trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function truncateText(value = "", maxLength = 80) {
|
|
17
|
+
const text = String(value ?? "").trim();
|
|
18
|
+
|
|
19
|
+
if (text.length <= maxLength) {
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return `${text.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeSql(sql = "") {
|
|
27
|
+
const withoutComments = stripBlockComments(stripLineComments(sql));
|
|
28
|
+
const compact = compactWhitespace(withoutComments).replace(/;+\s*$/g, "").trim();
|
|
29
|
+
|
|
30
|
+
return compact.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getLeadingKeywords(sql = "", limit = 3) {
|
|
34
|
+
const stripped = stripBlockComments(stripLineComments(sql));
|
|
35
|
+
const matches = stripped.match(/[A-Za-z]+/g) ?? [];
|
|
36
|
+
|
|
37
|
+
return matches.slice(0, limit).map((token) => token.toLowerCase());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveEffectiveKeyword(sql = "") {
|
|
41
|
+
const [firstKeyword, secondKeyword, thirdKeyword] = getLeadingKeywords(sql, 3);
|
|
42
|
+
|
|
43
|
+
if (firstKeyword === "with") {
|
|
44
|
+
return secondKeyword === "recursive" ? thirdKeyword ?? "with" : secondKeyword ?? "with";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (firstKeyword === "explain") {
|
|
48
|
+
return secondKeyword ?? "explain";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return firstKeyword ?? "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function detectQueryType(sql = "") {
|
|
55
|
+
const keyword = resolveEffectiveKeyword(sql);
|
|
56
|
+
|
|
57
|
+
if (keyword === "select") {
|
|
58
|
+
return "select";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (keyword === "insert") {
|
|
62
|
+
return "insert";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (keyword === "update") {
|
|
66
|
+
return "update";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (keyword === "delete") {
|
|
70
|
+
return "delete";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (keyword === "pragma") {
|
|
74
|
+
return "pragma";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (["create", "alter", "drop", "rename", "truncate"].includes(keyword)) {
|
|
78
|
+
return "ddl";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return "other";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isDestructiveQuery(sql = "") {
|
|
85
|
+
const keyword = resolveEffectiveKeyword(sql);
|
|
86
|
+
return ["insert", "update", "delete", "alter", "drop", "replace"].includes(keyword);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeTableName(candidate = "") {
|
|
90
|
+
const trimmed = String(candidate ?? "")
|
|
91
|
+
.trim()
|
|
92
|
+
.replace(/[;,)]*$/g, "");
|
|
93
|
+
|
|
94
|
+
if (!trimmed || trimmed.startsWith("(")) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const segments = trimmed.split(".").filter(Boolean);
|
|
99
|
+
const finalSegment = segments.at(-1) ?? trimmed;
|
|
100
|
+
const normalized = finalSegment.replace(/^["`[]|["`\]]$/g, "");
|
|
101
|
+
|
|
102
|
+
if (!normalized || ["select", "with", "pragma"].includes(normalized.toLowerCase())) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return normalized;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function collectTableMatches(sql = "", patterns = []) {
|
|
110
|
+
const text = stripBlockComments(stripLineComments(sql));
|
|
111
|
+
const tableNames = [];
|
|
112
|
+
|
|
113
|
+
patterns.forEach((pattern) => {
|
|
114
|
+
let match;
|
|
115
|
+
|
|
116
|
+
while ((match = pattern.exec(text))) {
|
|
117
|
+
const tableName = normalizeTableName(match[1]);
|
|
118
|
+
|
|
119
|
+
if (tableName) {
|
|
120
|
+
tableNames.push(tableName);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return Array.from(new Set(tableNames));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function detectTables(sql = "") {
|
|
129
|
+
return collectTableMatches(sql, [
|
|
130
|
+
/\bfrom\s+([^\s,;()]+)/gi,
|
|
131
|
+
/\bjoin\s+([^\s,;()]+)/gi,
|
|
132
|
+
/\bupdate(?:\s+or\s+\w+)?\s+([^\s,;()]+)/gi,
|
|
133
|
+
/\binto\s+([^\s,;()]+)/gi,
|
|
134
|
+
/\btable\s+(?:if\s+(?:not\s+)?exists\s+)?([^\s,;()]+)/gi,
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildAutoTitle(rawSql = "", { queryType = "other", tablesDetected = [] } = {}) {
|
|
139
|
+
const firstMeaningfulLine = String(rawSql)
|
|
140
|
+
.split(/\r?\n/)
|
|
141
|
+
.map((line) => line.replace(/--.*$/g, "").trim())
|
|
142
|
+
.find(Boolean);
|
|
143
|
+
|
|
144
|
+
if (firstMeaningfulLine) {
|
|
145
|
+
return truncateText(firstMeaningfulLine.replace(/;+\s*$/g, ""), 80);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const primaryTable = tablesDetected[0] ? ` on ${tablesDetected[0]}` : "";
|
|
149
|
+
|
|
150
|
+
if (queryType && queryType !== "other") {
|
|
151
|
+
return `${queryType.toUpperCase()}${primaryTable}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return primaryTable ? `Query${primaryTable}` : "SQL query";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildSqlPreview(rawSql = "", maxLength = 140) {
|
|
158
|
+
const compact = compactWhitespace(stripLineComments(rawSql));
|
|
159
|
+
return truncateText(compact, maxLength) || "SQL query";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
buildAutoTitle,
|
|
164
|
+
buildSqlPreview,
|
|
165
|
+
detectQueryType,
|
|
166
|
+
detectTables,
|
|
167
|
+
isDestructiveQuery,
|
|
168
|
+
normalizeSql,
|
|
169
|
+
};
|
package/server/utils/appPaths.js
CHANGED
|
@@ -6,6 +6,13 @@ const APP_NAME = "sqlite-hub";
|
|
|
6
6
|
const APP_STATE_DB_FILENAME = "sqlite-hub-state.db";
|
|
7
7
|
const LEGACY_STATE_FILENAME = "app-state.json";
|
|
8
8
|
|
|
9
|
+
function resolvePackagedDataDirectories(packageRoot) {
|
|
10
|
+
return [
|
|
11
|
+
path.resolve(packageRoot, "server", "data"),
|
|
12
|
+
path.resolve(packageRoot, "data"),
|
|
13
|
+
].filter((candidatePath, index, candidates) => candidates.indexOf(candidatePath) === index);
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
function resolveAppStateDirectory() {
|
|
10
17
|
const homeDirectory = os.homedir();
|
|
11
18
|
|
|
@@ -28,7 +35,7 @@ function resolveAppStateDirectory() {
|
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
function resolvePackagedDataDirectory(packageRoot) {
|
|
31
|
-
return
|
|
38
|
+
return resolvePackagedDataDirectories(packageRoot)[0];
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
function resolvePackagedAppStateDbPath(packageRoot) {
|
|
@@ -36,7 +43,11 @@ function resolvePackagedAppStateDbPath(packageRoot) {
|
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
function resolvePackagedLegacyStatePath(packageRoot) {
|
|
39
|
-
|
|
46
|
+
const candidates = resolvePackagedDataDirectories(packageRoot).map((directoryPath) =>
|
|
47
|
+
path.join(directoryPath, LEGACY_STATE_FILENAME)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return candidates.find((candidatePath) => fs.existsSync(candidatePath)) ?? candidates[0];
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
function resolveHomebrewCellarInfo(packageRoot) {
|
|
@@ -83,23 +94,34 @@ function collectHomebrewLegacyStateDbPaths(packageRoot) {
|
|
|
83
94
|
return fs
|
|
84
95
|
.readdirSync(cellarInfo.cellarRoot, { withFileTypes: true })
|
|
85
96
|
.filter((entry) => entry.isDirectory() && entry.name !== cellarInfo.currentVersion)
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
.flatMap((entry) =>
|
|
98
|
+
[
|
|
99
|
+
path.join(
|
|
100
|
+
cellarInfo.cellarRoot,
|
|
101
|
+
entry.name,
|
|
102
|
+
"libexec",
|
|
103
|
+
"lib",
|
|
104
|
+
"node_modules",
|
|
105
|
+
APP_NAME,
|
|
106
|
+
"server",
|
|
107
|
+
"data",
|
|
108
|
+
APP_STATE_DB_FILENAME
|
|
109
|
+
),
|
|
110
|
+
path.join(
|
|
111
|
+
cellarInfo.cellarRoot,
|
|
112
|
+
entry.name,
|
|
113
|
+
"libexec",
|
|
114
|
+
"lib",
|
|
115
|
+
"node_modules",
|
|
116
|
+
APP_NAME,
|
|
117
|
+
"data",
|
|
118
|
+
APP_STATE_DB_FILENAME
|
|
119
|
+
),
|
|
120
|
+
].map((candidatePath) => ({
|
|
99
121
|
path: candidatePath,
|
|
100
122
|
mtimeMs: safeStatMtimeMs(candidatePath),
|
|
101
|
-
}
|
|
102
|
-
|
|
123
|
+
}))
|
|
124
|
+
)
|
|
103
125
|
.filter((candidate) => candidate.mtimeMs >= 0)
|
|
104
126
|
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
|
105
127
|
.map((candidate) => candidate.path);
|
|
@@ -107,7 +129,9 @@ function collectHomebrewLegacyStateDbPaths(packageRoot) {
|
|
|
107
129
|
|
|
108
130
|
function collectLegacyDatabasePaths(packageRoot) {
|
|
109
131
|
return [
|
|
110
|
-
|
|
132
|
+
...resolvePackagedDataDirectories(packageRoot).map((directoryPath) =>
|
|
133
|
+
path.join(directoryPath, APP_STATE_DB_FILENAME)
|
|
134
|
+
),
|
|
111
135
|
...collectHomebrewLegacyStateDbPaths(packageRoot),
|
|
112
136
|
].filter((candidatePath, index, candidates) => candidates.indexOf(candidatePath) === index);
|
|
113
137
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -3,8 +3,8 @@ import { escapeHtml } from "../utils/format.js";
|
|
|
3
3
|
export function renderBottomTabs(activeTab, counts = {}) {
|
|
4
4
|
const tabs = [
|
|
5
5
|
{ key: "results", label: "results", meta: counts.resultRows ?? 0 },
|
|
6
|
-
{ key: "messages", label: "messages", meta: counts.messages ?? 0 },
|
|
7
6
|
{ key: "performance", label: "performance", meta: counts.statementCount ?? 0 },
|
|
7
|
+
{ key: "messages", label: "messages", meta: counts.messages ?? 0 },
|
|
8
8
|
];
|
|
9
9
|
|
|
10
10
|
return `
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|