sqlite-hub 0.10.0 → 0.12.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 +44 -13
- package/database.sqlite +0 -0
- package/fill.js +526 -0
- package/frontend/js/api.js +70 -11
- package/frontend/js/app.js +104 -32
- package/frontend/js/components/emptyState.js +42 -46
- package/frontend/js/components/modal.js +95 -0
- package/frontend/js/components/queryEditor.js +3 -2
- package/frontend/js/components/rowEditorPanel.js +145 -6
- package/frontend/js/components/sidebar.js +5 -5
- package/frontend/js/store.js +303 -34
- package/frontend/js/views/data.js +94 -70
- package/frontend/js/views/editor.js +2 -0
- package/frontend/js/views/settings.js +1 -1
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +95 -95
- package/package.json +1 -1
- package/server/routes/data.js +3 -0
- package/server/routes/export.js +97 -12
- package/server/services/sqlite/dataBrowserService.js +25 -4
- package/server/services/sqlite/exportService.js +74 -15
- package/server/services/sqlite/introspection.js +199 -1
- package/server/services/sqlite/sqlExecutor.js +2 -0
- package/server/services/sqlite/tableFilter.js +75 -0
- package/server/utils/csv.js +30 -4
- package/tests/check-constraint-options.test.js +76 -0
- package/tests/sql-identifier-safety.test.js +59 -0
|
@@ -1,97 +1,97 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
2
|
+
--rgb-primary: 252 227 0;
|
|
3
|
+
--rgb-outline: 75 71 50;
|
|
4
|
+
--rgb-on-surface: 229 226 225;
|
|
5
|
+
--rgb-on-surface-variant: 205 199 171;
|
|
6
|
+
--rgb-background: 19 19 19;
|
|
7
|
+
--rgb-surface: 19 19 19;
|
|
8
|
+
--rgb-surface-lowest: 14 14 14;
|
|
9
|
+
--rgb-surface-low: 28 27 27;
|
|
10
|
+
--rgb-surface-container: 32 31 31;
|
|
11
|
+
--rgb-surface-high: 42 42 42;
|
|
12
|
+
--rgb-surface-highest: 53 53 52;
|
|
13
|
+
--rgb-surface-bright: 58 57 57;
|
|
14
|
+
--rgb-error: 255 180 171;
|
|
15
|
+
--rgb-error-container: 147 0 10;
|
|
16
|
+
--rgb-tertiary: 0 220 225;
|
|
17
|
+
--rgb-success: 34 197 94;
|
|
18
|
+
--color-background: #131313;
|
|
19
|
+
--color-surface: #131313;
|
|
20
|
+
--color-surface-lowest: #0e0e0e;
|
|
21
|
+
--color-surface-container-lowest: var(--color-surface-lowest);
|
|
22
|
+
--color-surface-low: #1c1b1b;
|
|
23
|
+
--color-surface-container-low: var(--color-surface-low);
|
|
24
|
+
--color-surface-container: #201f1f;
|
|
25
|
+
--color-surface-high: #2a2a2a;
|
|
26
|
+
--color-surface-container-high: var(--color-surface-high);
|
|
27
|
+
--color-surface-highest: #353534;
|
|
28
|
+
--color-surface-container-highest: var(--color-surface-highest);
|
|
29
|
+
--color-surface-bright: #3a3939;
|
|
30
|
+
--color-primary-container: #fce300;
|
|
31
|
+
--color-primary-fixed: #fde403;
|
|
32
|
+
--color-on-primary: #373100;
|
|
33
|
+
--color-on-surface: #e5e2e1;
|
|
34
|
+
--color-on-surface-variant: #cdc7ab;
|
|
35
|
+
--color-outline-variant: #4b4732;
|
|
36
|
+
--color-error: #ffb4ab;
|
|
37
|
+
--color-error-container: #93000a;
|
|
38
|
+
--color-tertiary-dim: #00dce1;
|
|
39
|
+
--color-grid-dot: rgb(var(--rgb-primary) / 0.03);
|
|
40
|
+
--color-success: #22c55e;
|
|
41
|
+
--border-subtle: rgb(var(--rgb-outline) / 0.12);
|
|
42
|
+
--border-medium: rgb(var(--rgb-outline) / 0.18);
|
|
43
|
+
--border-strong: rgb(var(--rgb-outline) / 0.24);
|
|
44
|
+
--text-muted: rgb(var(--rgb-on-surface) / 0.6);
|
|
45
|
+
--text-subtle: rgb(var(--rgb-on-surface) / 0.45);
|
|
46
|
+
--text-faint: rgb(var(--rgb-on-surface) / 0.35);
|
|
47
|
+
--text-variant-muted: rgb(var(--rgb-on-surface-variant) / 0.55);
|
|
48
|
+
--text-variant-subtle: rgb(var(--rgb-on-surface-variant) / 0.45);
|
|
49
|
+
--primary-alpha-03: rgb(var(--rgb-primary) / 0.03);
|
|
50
|
+
--primary-alpha-05: rgb(var(--rgb-primary) / 0.05);
|
|
51
|
+
--primary-alpha-06: rgb(var(--rgb-primary) / 0.06);
|
|
52
|
+
--primary-alpha-08: rgb(var(--rgb-primary) / 0.08);
|
|
53
|
+
--primary-alpha-10: rgb(var(--rgb-primary) / 0.1);
|
|
54
|
+
--primary-alpha-12: rgb(var(--rgb-primary) / 0.12);
|
|
55
|
+
--primary-alpha-15: rgb(var(--rgb-primary) / 0.15);
|
|
56
|
+
--primary-alpha-18: rgb(var(--rgb-primary) / 0.18);
|
|
57
|
+
--primary-alpha-20: rgb(var(--rgb-primary) / 0.2);
|
|
58
|
+
--primary-alpha-22: rgb(var(--rgb-primary) / 0.22);
|
|
59
|
+
--primary-alpha-24: rgb(var(--rgb-primary) / 0.24);
|
|
60
|
+
--primary-alpha-30: rgb(var(--rgb-primary) / 0.3);
|
|
61
|
+
--primary-alpha-35: rgb(var(--rgb-primary) / 0.35);
|
|
62
|
+
--focus-ring: 0 0 0 2px var(--color-surface), 0 0 0 4px var(--primary-alpha-30);
|
|
63
|
+
--focus-ring-inset: inset 0 0 0 1px var(--primary-alpha-30), 0 0 0 2px var(--primary-alpha-12);
|
|
64
|
+
--font-family-headline: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
65
|
+
--font-family-body: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
66
|
+
--font-family-mono: ui-monospace, 'SFMono-Regular', 'SF Mono', Consolas, 'Liberation Mono', monospace;
|
|
67
|
+
--font-size-status: 10px;
|
|
68
|
+
--font-size-micro: 9px;
|
|
69
|
+
--font-size-label: 11px;
|
|
70
|
+
--font-size-icon: 18px;
|
|
71
|
+
--control-height: 36px;
|
|
72
|
+
--control-padding-inline: 12px;
|
|
73
|
+
--button-height: 32px;
|
|
74
|
+
--button-padding-inline: 10px;
|
|
75
|
+
--button-corner-size: 10px;
|
|
76
|
+
--spacing-1: 4px;
|
|
77
|
+
--spacing-2: 8px;
|
|
78
|
+
--spacing-3: 12px;
|
|
79
|
+
--spacing-4: 16px;
|
|
80
|
+
--spacing-5: 20px;
|
|
81
|
+
--spacing-6: 24px;
|
|
82
|
+
--spacing-8: 32px;
|
|
83
|
+
--spacing-10: 40px;
|
|
84
|
+
--radius-none: 0;
|
|
85
|
+
--top-nav-height: 56px;
|
|
86
|
+
--status-bar-height: 24px;
|
|
87
|
+
--sidebar-width: 256px;
|
|
88
|
+
--panel-width: 600px;
|
|
89
|
+
--shadow-sidebar: 4px 0 0 0 #131313;
|
|
90
|
+
--shadow-panel: -20px 0 60px rgb(0 0 0 / 0.8);
|
|
91
|
+
--shadow-subtle-glow: 0 0 15px var(--primary-alpha-30);
|
|
92
|
+
--shadow-strong-glow: 0 0 20px var(--primary-alpha-30);
|
|
93
|
+
--transition-fast: 150ms ease;
|
|
94
|
+
--transition-standard: 300ms ease;
|
|
95
|
+
--duration-cursor-blink: 1s;
|
|
96
|
+
--scrollbar-size: 4px;
|
|
97
97
|
}
|
package/package.json
CHANGED
package/server/routes/data.js
CHANGED
|
@@ -28,6 +28,9 @@ function createDataRouter({ dataBrowserService }) {
|
|
|
28
28
|
offset: req.query.offset,
|
|
29
29
|
sortColumn: req.query.sortColumn,
|
|
30
30
|
sortDirection: req.query.sortDirection,
|
|
31
|
+
filterColumn: req.query.filterColumn,
|
|
32
|
+
filterOperator: req.query.filterOperator,
|
|
33
|
+
filterValue: req.query.filterValue,
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
res.json(
|
package/server/routes/export.js
CHANGED
|
@@ -1,34 +1,119 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
|
-
const { route } = require("../utils/errors");
|
|
2
|
+
const { route, successResponse } = require("../utils/errors");
|
|
3
3
|
|
|
4
4
|
function createExportRouter({ exportService }) {
|
|
5
5
|
const router = express.Router();
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
res.setHeader("Content-Type", "text/
|
|
7
|
+
function sendExport(res, result) {
|
|
8
|
+
res.setHeader("Content-Type", result.mimeType || "text/plain; charset=utf-8");
|
|
9
9
|
res.setHeader(
|
|
10
10
|
"Content-Disposition",
|
|
11
11
|
`attachment; filename="${result.filename}"`
|
|
12
12
|
);
|
|
13
|
-
res.send(result.csv);
|
|
13
|
+
res.send(result.content ?? result.csv ?? "");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function sendQueryExport(res, sql, format) {
|
|
17
|
+
const result = exportService.exportQuery(sql, { format });
|
|
18
|
+
sendExport(res, result);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function exportTableFromBody(body, format) {
|
|
22
|
+
return exportService.exportTable(body?.tableName, {
|
|
23
|
+
sortColumn: body?.sortColumn,
|
|
24
|
+
sortDirection: body?.sortDirection,
|
|
25
|
+
filterColumn: body?.filterColumn,
|
|
26
|
+
filterOperator: body?.filterOperator,
|
|
27
|
+
filterValue: body?.filterValue,
|
|
28
|
+
format,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sendTableExport(res, body, format) {
|
|
33
|
+
sendExport(res, exportTableFromBody(body, format));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
router.post(
|
|
37
|
+
"/query",
|
|
38
|
+
route((req, res) => {
|
|
39
|
+
const result = exportService.exportQuery(req.body?.sql, {
|
|
40
|
+
format: req.body?.format || "csv",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
res.json(
|
|
44
|
+
successResponse({
|
|
45
|
+
data: {
|
|
46
|
+
filename: result.filename,
|
|
47
|
+
content: result.content ?? result.csv ?? "",
|
|
48
|
+
format: result.format,
|
|
49
|
+
mimeType: result.mimeType,
|
|
50
|
+
columns: result.columns,
|
|
51
|
+
rowCount: result.rowCount,
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
|
|
16
58
|
router.post(
|
|
17
59
|
"/query.csv",
|
|
18
60
|
route((req, res) => {
|
|
19
|
-
|
|
20
|
-
|
|
61
|
+
sendQueryExport(res, req.body?.sql, "csv");
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
router.post(
|
|
66
|
+
"/query.tsv",
|
|
67
|
+
route((req, res) => {
|
|
68
|
+
sendQueryExport(res, req.body?.sql, "tsv");
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
router.post(
|
|
73
|
+
"/query.md",
|
|
74
|
+
route((req, res) => {
|
|
75
|
+
sendQueryExport(res, req.body?.sql, "md");
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
router.post(
|
|
80
|
+
"/table",
|
|
81
|
+
route((req, res) => {
|
|
82
|
+
const result = exportTableFromBody(req.body, req.body?.format || "csv");
|
|
83
|
+
|
|
84
|
+
res.json(
|
|
85
|
+
successResponse({
|
|
86
|
+
data: {
|
|
87
|
+
filename: result.filename,
|
|
88
|
+
content: result.content ?? result.csv ?? "",
|
|
89
|
+
format: result.format,
|
|
90
|
+
mimeType: result.mimeType,
|
|
91
|
+
columns: result.columns,
|
|
92
|
+
rowCount: result.rowCount,
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
);
|
|
21
96
|
})
|
|
22
97
|
);
|
|
23
98
|
|
|
24
99
|
router.post(
|
|
25
100
|
"/table.csv",
|
|
26
101
|
route((req, res) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
102
|
+
sendTableExport(res, req.body, "csv");
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
router.post(
|
|
107
|
+
"/table.tsv",
|
|
108
|
+
route((req, res) => {
|
|
109
|
+
sendTableExport(res, req.body, "tsv");
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
router.post(
|
|
114
|
+
"/table.md",
|
|
115
|
+
route((req, res) => {
|
|
116
|
+
sendTableExport(res, req.body, "md");
|
|
32
117
|
})
|
|
33
118
|
);
|
|
34
119
|
|
|
@@ -36,7 +121,7 @@ function createExportRouter({ exportService }) {
|
|
|
36
121
|
"/table/:tableName.csv",
|
|
37
122
|
route((req, res) => {
|
|
38
123
|
const result = exportService.exportTable(req.params.tableName);
|
|
39
|
-
|
|
124
|
+
sendExport(res, result);
|
|
40
125
|
})
|
|
41
126
|
);
|
|
42
127
|
|
|
@@ -6,10 +6,11 @@ const {
|
|
|
6
6
|
serializeRows,
|
|
7
7
|
} = require("../../utils/sqliteTypes");
|
|
8
8
|
const { getRawStructureEntries, getTableDetail } = require("./introspection");
|
|
9
|
+
const { normalizeTableFilter } = require("./tableFilter");
|
|
9
10
|
const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
|
|
10
11
|
|
|
11
12
|
const DEFAULT_LIMIT = 50;
|
|
12
|
-
const MAX_LIMIT =
|
|
13
|
+
const MAX_LIMIT = 250;
|
|
13
14
|
|
|
14
15
|
function buildRowIdentity(tableDetail, row) {
|
|
15
16
|
if (tableDetail.identityStrategy?.type === "rowid") {
|
|
@@ -100,15 +101,18 @@ class DataBrowserService {
|
|
|
100
101
|
const tableDetail = getTableDetail(db, tableName);
|
|
101
102
|
const { limit, offset } = normalizePaginationOptions(options);
|
|
102
103
|
const sort = normalizeTableSort(tableDetail, options);
|
|
104
|
+
const filter = normalizeTableFilter(tableDetail, options);
|
|
103
105
|
const selectExpression =
|
|
104
106
|
tableDetail.identityStrategy?.type === "rowid" ? "rowid AS __rowid__, *" : "*";
|
|
105
107
|
const orderClause = buildTableOrderClause(tableDetail, sort);
|
|
108
|
+
const whereClause = filter ? `WHERE ${filter.clause}` : "";
|
|
106
109
|
const statement = db.prepare(
|
|
107
110
|
[
|
|
108
111
|
"SELECT",
|
|
109
112
|
selectExpression,
|
|
110
113
|
"FROM",
|
|
111
114
|
quoteIdentifier(tableName),
|
|
115
|
+
whereClause,
|
|
112
116
|
orderClause ? "ORDER BY" : "",
|
|
113
117
|
orderClause,
|
|
114
118
|
"LIMIT ? OFFSET ?",
|
|
@@ -116,11 +120,20 @@ class DataBrowserService {
|
|
|
116
120
|
.filter(Boolean)
|
|
117
121
|
.join(" ")
|
|
118
122
|
);
|
|
119
|
-
const rawRows = statement.all(limit, offset);
|
|
123
|
+
const rawRows = statement.all(...(filter?.params ?? []), limit, offset);
|
|
120
124
|
const columns = statement
|
|
121
125
|
.columns()
|
|
122
126
|
.map((column) => column.name)
|
|
123
127
|
.filter((columnName) => columnName !== "__rowid__");
|
|
128
|
+
const rowCount = filter
|
|
129
|
+
? db
|
|
130
|
+
.prepare(
|
|
131
|
+
["SELECT COUNT(*) AS count FROM", quoteIdentifier(tableName), whereClause]
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join(" ")
|
|
134
|
+
)
|
|
135
|
+
.get(...filter.params).count
|
|
136
|
+
: tableDetail.rowCount ?? rawRows.length;
|
|
124
137
|
|
|
125
138
|
const rows = serializeRows(rawRows).map((row) => {
|
|
126
139
|
const identity = buildRowIdentity(tableDetail, row);
|
|
@@ -138,17 +151,25 @@ class DataBrowserService {
|
|
|
138
151
|
return {
|
|
139
152
|
name: tableDetail.name,
|
|
140
153
|
type: tableDetail.type,
|
|
141
|
-
rowCount
|
|
154
|
+
rowCount,
|
|
142
155
|
limit,
|
|
143
156
|
offset,
|
|
144
157
|
page: Math.floor(offset / limit) + 1,
|
|
145
|
-
pageCount: Math.max(1, Math.ceil(
|
|
158
|
+
pageCount: Math.max(1, Math.ceil(rowCount / limit)),
|
|
146
159
|
columns,
|
|
147
160
|
columnMeta: tableDetail.columns,
|
|
148
161
|
foreignKeys: tableDetail.foreignKeys,
|
|
149
162
|
rows,
|
|
150
163
|
identityStrategy: tableDetail.identityStrategy,
|
|
151
164
|
notSafelyUpdatable: tableDetail.notSafelyUpdatable,
|
|
165
|
+
filter: filter
|
|
166
|
+
? {
|
|
167
|
+
column: filter.column,
|
|
168
|
+
operator: filter.operator,
|
|
169
|
+
value: filter.value,
|
|
170
|
+
matchMode: filter.matchMode,
|
|
171
|
+
}
|
|
172
|
+
: null,
|
|
152
173
|
sort,
|
|
153
174
|
};
|
|
154
175
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
const { quoteIdentifier } = require("../../utils/identifier");
|
|
2
2
|
const { serializeRows } = require("../../utils/sqliteTypes");
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
rowsToCsv,
|
|
5
|
+
rowsToDelimitedText,
|
|
6
|
+
rowsToMarkdownTable,
|
|
7
|
+
} = require("../../utils/csv");
|
|
4
8
|
const { getTableDetail } = require("./introspection");
|
|
9
|
+
const { normalizeTableFilter } = require("./tableFilter");
|
|
5
10
|
const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
|
|
6
11
|
const {
|
|
7
12
|
buildAutoTitle,
|
|
@@ -23,6 +28,43 @@ function sanitizeFilenameBase(value, fallback = "query-results") {
|
|
|
23
28
|
return sanitized.slice(0, 120);
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
const EXPORT_FORMATS = {
|
|
32
|
+
csv: {
|
|
33
|
+
extension: "csv",
|
|
34
|
+
mimeType: "text/csv; charset=utf-8",
|
|
35
|
+
},
|
|
36
|
+
tsv: {
|
|
37
|
+
extension: "tsv",
|
|
38
|
+
mimeType: "text/tab-separated-values; charset=utf-8",
|
|
39
|
+
},
|
|
40
|
+
md: {
|
|
41
|
+
extension: "md",
|
|
42
|
+
mimeType: "text/markdown; charset=utf-8",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function normalizeExportFormat(format) {
|
|
47
|
+
const normalized = String(format ?? "csv").toLowerCase();
|
|
48
|
+
|
|
49
|
+
if (!EXPORT_FORMATS[normalized]) {
|
|
50
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderExportContent({ columns, rows, format, csvDelimiter }) {
|
|
57
|
+
if (format === "tsv") {
|
|
58
|
+
return rowsToDelimitedText({ columns, rows, delimiter: "\t" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (format === "md") {
|
|
62
|
+
return rowsToMarkdownTable({ columns, rows });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return rowsToCsv({ columns, rows, delimiter: csvDelimiter });
|
|
66
|
+
}
|
|
67
|
+
|
|
26
68
|
class ExportService {
|
|
27
69
|
constructor({ appStateStore, connectionManager, sqlExecutor }) {
|
|
28
70
|
this.appStateStore = appStateStore;
|
|
@@ -34,7 +76,9 @@ class ExportService {
|
|
|
34
76
|
return this.appStateStore.getSettings().csvDelimiter || ",";
|
|
35
77
|
}
|
|
36
78
|
|
|
37
|
-
exportQuery(sql) {
|
|
79
|
+
exportQuery(sql, options = {}) {
|
|
80
|
+
const format = normalizeExportFormat(options.format);
|
|
81
|
+
const formatConfig = EXPORT_FORMATS[format];
|
|
38
82
|
const activeConnection = this.connectionManager.getActiveConnection();
|
|
39
83
|
const historyItem = activeConnection
|
|
40
84
|
? this.appStateStore.findQueryHistoryItemBySql(activeConnection.id, sql)
|
|
@@ -50,44 +94,59 @@ class ExportService {
|
|
|
50
94
|
persistHistory: false,
|
|
51
95
|
requireReader: true,
|
|
52
96
|
});
|
|
97
|
+
const content = renderExportContent({
|
|
98
|
+
columns: result.columns,
|
|
99
|
+
rows: result.rows,
|
|
100
|
+
format,
|
|
101
|
+
csvDelimiter: this.getDelimiter(),
|
|
102
|
+
});
|
|
53
103
|
|
|
54
104
|
return {
|
|
55
|
-
filename: `${filenameBase}.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}),
|
|
105
|
+
filename: `${filenameBase}.${formatConfig.extension}`,
|
|
106
|
+
content,
|
|
107
|
+
csv: format === "csv" ? content : undefined,
|
|
108
|
+
format,
|
|
109
|
+
mimeType: formatConfig.mimeType,
|
|
61
110
|
columns: result.columns,
|
|
62
111
|
rowCount: result.rows.length,
|
|
63
112
|
};
|
|
64
113
|
}
|
|
65
114
|
|
|
66
115
|
exportTable(tableName, options = {}) {
|
|
116
|
+
const format = normalizeExportFormat(options.format);
|
|
117
|
+
const formatConfig = EXPORT_FORMATS[format];
|
|
67
118
|
const db = this.connectionManager.getActiveDatabase();
|
|
68
119
|
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
69
120
|
const sort = normalizeTableSort(tableDetail, options);
|
|
121
|
+
const filter = normalizeTableFilter(tableDetail, options);
|
|
70
122
|
const orderClause = buildTableOrderClause(tableDetail, sort);
|
|
123
|
+
const whereClause = filter ? `WHERE ${filter.clause}` : "";
|
|
71
124
|
const statement = db.prepare(
|
|
72
125
|
[
|
|
73
126
|
"SELECT * FROM",
|
|
74
127
|
quoteIdentifier(tableName),
|
|
128
|
+
whereClause,
|
|
75
129
|
orderClause ? "ORDER BY" : "",
|
|
76
130
|
orderClause,
|
|
77
131
|
]
|
|
78
132
|
.filter(Boolean)
|
|
79
133
|
.join(" ")
|
|
80
134
|
);
|
|
81
|
-
const rows = serializeRows(statement.all());
|
|
135
|
+
const rows = serializeRows(statement.all(...(filter?.params ?? [])));
|
|
82
136
|
const columns = statement.columns().map((column) => column.name);
|
|
137
|
+
const content = renderExportContent({
|
|
138
|
+
columns,
|
|
139
|
+
rows,
|
|
140
|
+
format,
|
|
141
|
+
csvDelimiter: this.getDelimiter(),
|
|
142
|
+
});
|
|
83
143
|
|
|
84
144
|
return {
|
|
85
|
-
filename: `${tableName}.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}),
|
|
145
|
+
filename: `${tableName}.${formatConfig.extension}`,
|
|
146
|
+
content,
|
|
147
|
+
csv: format === "csv" ? content : undefined,
|
|
148
|
+
format,
|
|
149
|
+
mimeType: formatConfig.mimeType,
|
|
91
150
|
columns,
|
|
92
151
|
rowCount: rows.length,
|
|
93
152
|
};
|