sqlite-hub 0.4.0 → 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/changelog.md +7 -0
- package/frontend/js/api.js +99 -5
- package/frontend/js/app.js +90 -11
- package/frontend/js/components/bottomTabs.js +1 -1
- package/frontend/js/components/dataGrid.js +3 -3
- package/frontend/js/components/queryEditor.js +33 -55
- package/frontend/js/components/queryHistoryDetail.js +263 -0
- package/frontend/js/components/queryHistoryPanel.js +228 -0
- package/frontend/js/components/queryResults.js +32 -46
- package/frontend/js/components/rowEditorPanel.js +73 -14
- package/frontend/js/store.js +545 -21
- package/frontend/js/utils/format.js +23 -0
- package/frontend/js/views/data.js +34 -1
- package/frontend/js/views/editor.js +34 -10
- package/frontend/js/views/overview.js +15 -0
- package/frontend/styles/components.css +106 -0
- package/package.json +1 -1
- 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 +1 -1
- 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 +674 -1
- package/server/services/storage/queryHistoryUtils.js +169 -0
|
@@ -5,6 +5,10 @@ const DATE_TIME_FORMATTER = new Intl.DateTimeFormat("en-GB", {
|
|
|
5
5
|
dateStyle: "medium",
|
|
6
6
|
timeStyle: "short",
|
|
7
7
|
});
|
|
8
|
+
const COMPACT_DATE_TIME_FORMATTER = new Intl.DateTimeFormat("en-GB", {
|
|
9
|
+
dateStyle: "short",
|
|
10
|
+
timeStyle: "short",
|
|
11
|
+
});
|
|
8
12
|
|
|
9
13
|
export function escapeHtml(value = "") {
|
|
10
14
|
return String(value)
|
|
@@ -52,6 +56,25 @@ export function formatDateTime(value) {
|
|
|
52
56
|
return Number.isNaN(date.getTime()) ? String(value) : DATE_TIME_FORMATTER.format(date);
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
export function formatCompactDateTime(value) {
|
|
60
|
+
if (!value) {
|
|
61
|
+
return "N/A";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const date = new Date(value);
|
|
65
|
+
return Number.isNaN(date.getTime()) ? String(value) : COMPACT_DATE_TIME_FORMATTER.format(date);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatDurationMs(value) {
|
|
69
|
+
const numericValue = Number(value);
|
|
70
|
+
|
|
71
|
+
if (!Number.isFinite(numericValue) || numericValue < 0) {
|
|
72
|
+
return "N/A";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `${numericValue.toLocaleString("en-US")} ms`;
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
export function formatRelativeBoolean(value) {
|
|
56
79
|
return value ? "ENABLED" : "DISABLED";
|
|
57
80
|
}
|
|
@@ -168,6 +168,36 @@ function getCellWidthClass(columnName) {
|
|
|
168
168
|
return "max-w-[12rem]";
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
function getSortIcon(columnName, sortColumn, sortDirection) {
|
|
172
|
+
if (columnName !== sortColumn) {
|
|
173
|
+
return "unfold_more";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return sortDirection === "desc" ? "south" : "north";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderSortableHeader(columnName, sortColumn, sortDirection, action) {
|
|
180
|
+
const isActive = columnName === sortColumn;
|
|
181
|
+
|
|
182
|
+
return `
|
|
183
|
+
<button
|
|
184
|
+
class="flex w-full items-center justify-between gap-2 text-left transition-colors ${
|
|
185
|
+
isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
|
|
186
|
+
}"
|
|
187
|
+
data-action="${action}"
|
|
188
|
+
data-column-name="${escapeHtml(columnName)}"
|
|
189
|
+
type="button"
|
|
190
|
+
>
|
|
191
|
+
<span class="truncate">${escapeHtml(columnName)}</span>
|
|
192
|
+
<span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
|
|
193
|
+
columnName,
|
|
194
|
+
sortColumn,
|
|
195
|
+
sortDirection
|
|
196
|
+
)}</span>
|
|
197
|
+
</button>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
|
|
171
201
|
function getFilteredTableRows(table, state) {
|
|
172
202
|
const allRows = table?.rows ?? [];
|
|
173
203
|
const availableColumns = table?.columns ?? [];
|
|
@@ -272,10 +302,13 @@ function renderTableSurface(state) {
|
|
|
272
302
|
}
|
|
273
303
|
|
|
274
304
|
const { activeColumn, filteredRows, searchQuery } = getFilteredTableRows(table, state);
|
|
305
|
+
const sortColumn = state.dataBrowser.sortColumn;
|
|
306
|
+
const sortDirection = state.dataBrowser.sortDirection;
|
|
275
307
|
const columns = (table.columns ?? []).map((columnName) => ({
|
|
276
|
-
label: escapeHtml(columnName),
|
|
277
308
|
headerClassName:
|
|
278
309
|
"border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold tracking-[0.08em] text-primary-container",
|
|
310
|
+
renderHeader: () =>
|
|
311
|
+
renderSortableHeader(columnName, sortColumn, sortDirection, "sort-data-column"),
|
|
279
312
|
cellClassName: "px-4 py-2 align-top text-[11px] text-on-surface",
|
|
280
313
|
render: (row) => {
|
|
281
314
|
const value = formatCellValue(row[columnName]);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { renderBottomTabs } from "../components/bottomTabs.js";
|
|
2
2
|
import { renderQueryEditor } from "../components/queryEditor.js";
|
|
3
|
+
import { renderQueryHistoryDetail } from "../components/queryHistoryDetail.js";
|
|
4
|
+
import { renderQueryHistoryPanel } from "../components/queryHistoryPanel.js";
|
|
3
5
|
import { renderRowEditorPanel } from "../components/rowEditorPanel.js";
|
|
4
6
|
import { renderQueryResultsPane } from "../components/queryResults.js";
|
|
5
7
|
import { getCurrentConnection, getQueryMessages, getQueryPerformance } from "../store.js";
|
|
@@ -216,17 +218,17 @@ function renderResultsSurface(state, isResultsRoute) {
|
|
|
216
218
|
|
|
217
219
|
content = state.connections.active
|
|
218
220
|
? renderQueryResultsPane(state.editor.result, {
|
|
219
|
-
exporting: state.editor.exportLoading,
|
|
220
221
|
selectedRowIndex: state.editor.selectedRowIndex,
|
|
221
222
|
editable: editingState.enabled,
|
|
222
|
-
|
|
223
|
+
sortColumn: state.editor.resultSortColumn,
|
|
224
|
+
sortDirection: state.editor.resultSortDirection,
|
|
223
225
|
})
|
|
224
226
|
: renderMissingDatabase();
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
return `
|
|
228
230
|
<div class="flex h-full min-h-0 flex-col border-t border-outline-variant/10 bg-surface-container-lowest">
|
|
229
|
-
${renderBottomTabs(activeTab, counts)}
|
|
231
|
+
${renderBottomTabs(state.editor.activeTab, counts)}
|
|
230
232
|
<div class="min-h-0 flex-1">${content}</div>
|
|
231
233
|
</div>
|
|
232
234
|
`;
|
|
@@ -234,13 +236,13 @@ function renderResultsSurface(state, isResultsRoute) {
|
|
|
234
236
|
|
|
235
237
|
export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
236
238
|
const connection = getCurrentConnection(state);
|
|
237
|
-
const editorSectionClass =
|
|
238
|
-
const resultsSectionClass =
|
|
239
|
+
const editorSectionClass = "min-h-[27.5%]";
|
|
240
|
+
const resultsSectionClass = "flex-1";
|
|
239
241
|
|
|
240
242
|
return {
|
|
241
243
|
main: `
|
|
242
|
-
<section class="view-surface flex h-full min-h-0 flex-col overflow-hidden">
|
|
243
|
-
<div class="flex h-
|
|
244
|
+
<section class="view-surface flex h-full min-h-0 flex-col overflow-hidden xl:flex-row">
|
|
245
|
+
<div class="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
|
244
246
|
<section class="${editorSectionClass} flex min-h-0 flex-col ${
|
|
245
247
|
isResultsRoute ? "border-b-4 border-background" : ""
|
|
246
248
|
}">
|
|
@@ -248,17 +250,39 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
|
248
250
|
query: state.editor.sqlText,
|
|
249
251
|
executing: state.editor.executing,
|
|
250
252
|
exporting: state.editor.exportLoading,
|
|
251
|
-
history: state.editor.history,
|
|
252
253
|
historyLoading: state.editor.historyLoading,
|
|
254
|
+
historyTotal: state.editor.historyTotal,
|
|
253
255
|
title: connection?.label ?? "SQLite Query Workspace",
|
|
254
256
|
})}
|
|
255
257
|
</section>
|
|
256
|
-
<section class="${resultsSectionClass} flex min-h-0 flex-col overflow-hidden">
|
|
258
|
+
<section class="${resultsSectionClass} flex min-h-0 min-w-0 flex-col overflow-hidden">
|
|
257
259
|
${renderResultsSurface(state, isResultsRoute)}
|
|
258
260
|
</section>
|
|
259
261
|
</div>
|
|
262
|
+
${renderQueryHistoryPanel({
|
|
263
|
+
items: state.editor.history,
|
|
264
|
+
loading: state.editor.historyLoading,
|
|
265
|
+
loadingMore: state.editor.historyLoadingMore,
|
|
266
|
+
error: state.editor.historyError,
|
|
267
|
+
activeTab: state.editor.historyTab,
|
|
268
|
+
search: state.editor.historySearchInput,
|
|
269
|
+
total: state.editor.historyTotal,
|
|
270
|
+
hasMore: state.editor.historyHasMore,
|
|
271
|
+
activeHistoryId: state.editor.historyActiveId,
|
|
272
|
+
selectedHistoryId: state.editor.historySelectedId,
|
|
273
|
+
})}
|
|
260
274
|
</section>
|
|
261
275
|
`,
|
|
262
|
-
panel:
|
|
276
|
+
panel:
|
|
277
|
+
isResultsRoute && state.editor.selectedRowIndex !== null
|
|
278
|
+
? renderEditorRowPanel(state)
|
|
279
|
+
: state.editor.historySelectedId
|
|
280
|
+
? renderQueryHistoryDetail({
|
|
281
|
+
item: state.editor.historyDetail,
|
|
282
|
+
runs: state.editor.historyRuns,
|
|
283
|
+
loading: state.editor.historyDetailLoading,
|
|
284
|
+
error: state.editor.historyDetailError,
|
|
285
|
+
})
|
|
286
|
+
: "",
|
|
263
287
|
};
|
|
264
288
|
}
|
|
@@ -126,6 +126,21 @@ function renderOperationalSurface(overview) {
|
|
|
126
126
|
)
|
|
127
127
|
.join("")}
|
|
128
128
|
</div>
|
|
129
|
+
${
|
|
130
|
+
overview.file?.path
|
|
131
|
+
? `
|
|
132
|
+
<div class="border-t border-outline-variant/10 px-4 py-3">
|
|
133
|
+
<button
|
|
134
|
+
class="toolbar-button border border-outline-variant/20 bg-surface-container px-3 py-1.5 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
|
|
135
|
+
data-action="open-overview-in-finder"
|
|
136
|
+
type="button"
|
|
137
|
+
>
|
|
138
|
+
Open In Finder
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
`
|
|
142
|
+
: ""
|
|
143
|
+
}
|
|
129
144
|
</section>
|
|
130
145
|
`;
|
|
131
146
|
}
|
|
@@ -316,6 +316,112 @@
|
|
|
316
316
|
padding: var(--spacing-3) var(--spacing-4);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
.query-history-panel {
|
|
320
|
+
display: flex;
|
|
321
|
+
flex: 0 0 360px;
|
|
322
|
+
flex-direction: column;
|
|
323
|
+
min-height: 0;
|
|
324
|
+
width: 360px;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.query-history-item {
|
|
328
|
+
align-items: stretch;
|
|
329
|
+
background: var(--color-surface-low);
|
|
330
|
+
border: 1px solid rgba(75, 71, 50, 0.16);
|
|
331
|
+
border-left: 2px solid transparent;
|
|
332
|
+
display: flex;
|
|
333
|
+
flex-direction: column;
|
|
334
|
+
min-width: 0;
|
|
335
|
+
transition: background-color var(--transition-fast), border-color var(--transition-fast),
|
|
336
|
+
box-shadow var(--transition-fast);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.query-history-item:hover {
|
|
340
|
+
background: var(--color-surface-high);
|
|
341
|
+
border-color: rgba(252, 227, 0, 0.18);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.query-history-item.is-active {
|
|
345
|
+
border-left-color: var(--color-primary-container);
|
|
346
|
+
box-shadow: 0 0 24px rgba(252, 227, 0, 0.06);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.query-history-item.is-error {
|
|
350
|
+
border-left-color: var(--color-error);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.query-history-item-hit {
|
|
354
|
+
background: transparent;
|
|
355
|
+
border: 0;
|
|
356
|
+
color: inherit;
|
|
357
|
+
display: block;
|
|
358
|
+
min-width: 0;
|
|
359
|
+
padding: var(--spacing-3);
|
|
360
|
+
text-align: left;
|
|
361
|
+
transition: background-color var(--transition-fast);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.query-history-item-hit.is-active {
|
|
365
|
+
background: rgba(252, 227, 0, 0.08);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.query-history-icon-button {
|
|
369
|
+
align-items: center;
|
|
370
|
+
background: transparent;
|
|
371
|
+
border: 0;
|
|
372
|
+
color: rgba(229, 226, 225, 0.6);
|
|
373
|
+
display: inline-flex;
|
|
374
|
+
height: 2rem;
|
|
375
|
+
justify-content: center;
|
|
376
|
+
transition: color var(--transition-fast), background-color var(--transition-fast);
|
|
377
|
+
width: 2rem;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.query-history-icon-button:hover,
|
|
381
|
+
.query-history-icon-button.is-active {
|
|
382
|
+
background: var(--color-surface-highest);
|
|
383
|
+
color: var(--color-primary-container);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.query-history-tab {
|
|
387
|
+
background: transparent;
|
|
388
|
+
border: 0;
|
|
389
|
+
border-bottom: 1px solid transparent;
|
|
390
|
+
color: rgba(205, 199, 171, 0.45);
|
|
391
|
+
font-family: var(--font-family-mono);
|
|
392
|
+
font-size: var(--font-size-status);
|
|
393
|
+
letter-spacing: 0.16em;
|
|
394
|
+
padding: 0.35rem 0;
|
|
395
|
+
text-transform: uppercase;
|
|
396
|
+
transition: border-color var(--transition-fast), color var(--transition-fast);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.query-history-tab.is-active {
|
|
400
|
+
border-color: var(--color-primary-container);
|
|
401
|
+
color: var(--color-primary-container);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.query-history-sql-preview {
|
|
405
|
+
display: -webkit-box;
|
|
406
|
+
-webkit-box-orient: vertical;
|
|
407
|
+
-webkit-line-clamp: 2;
|
|
408
|
+
overflow: hidden;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.query-history-detail-sql {
|
|
412
|
+
max-height: 240px;
|
|
413
|
+
white-space: pre-wrap;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
@media (max-width: 1279px) {
|
|
417
|
+
.query-history-panel {
|
|
418
|
+
border-left: 0;
|
|
419
|
+
border-top: 1px solid rgba(75, 71, 50, 0.16);
|
|
420
|
+
flex: 0 0 auto;
|
|
421
|
+
width: 100%;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
319
425
|
@media (min-width: 768px) {
|
|
320
426
|
.top-nav-links {
|
|
321
427
|
display: flex;
|
package/package.json
CHANGED
package/server/routes/data.js
CHANGED
|
@@ -26,6 +26,8 @@ function createDataRouter({ dataBrowserService }) {
|
|
|
26
26
|
const data = dataBrowserService.getTableData(req.params.tableName, {
|
|
27
27
|
limit: req.query.limit,
|
|
28
28
|
offset: req.query.offset,
|
|
29
|
+
sortColumn: req.query.sortColumn,
|
|
30
|
+
sortDirection: req.query.sortDirection,
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
res.json(
|
package/server/routes/export.js
CHANGED
|
@@ -24,7 +24,10 @@ function createExportRouter({ exportService }) {
|
|
|
24
24
|
router.post(
|
|
25
25
|
"/table.csv",
|
|
26
26
|
route((req, res) => {
|
|
27
|
-
const result = exportService.exportTable(req.body?.tableName
|
|
27
|
+
const result = exportService.exportTable(req.body?.tableName, {
|
|
28
|
+
sortColumn: req.body?.sortColumn,
|
|
29
|
+
sortDirection: req.body?.sortDirection,
|
|
30
|
+
});
|
|
28
31
|
sendCsv(res, result);
|
|
29
32
|
})
|
|
30
33
|
);
|
|
@@ -31,6 +31,18 @@ function createOverviewRouter({ overviewService }) {
|
|
|
31
31
|
})
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
router.post(
|
|
35
|
+
"/overview/open-in-finder",
|
|
36
|
+
route(async (req, res) => {
|
|
37
|
+
await overviewService.revealActiveDatabaseInFinder();
|
|
38
|
+
res.json(
|
|
39
|
+
successResponse({
|
|
40
|
+
message: "Database file revealed in Finder.",
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
|
|
34
46
|
return router;
|
|
35
47
|
}
|
|
36
48
|
|
package/server/routes/sql.js
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
2
|
const { route, successResponse } = require("../utils/errors");
|
|
3
3
|
|
|
4
|
-
function
|
|
4
|
+
function parseBooleanFlag(value) {
|
|
5
|
+
if (typeof value === "boolean") {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const normalized = String(value ?? "")
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase();
|
|
12
|
+
|
|
13
|
+
return ["1", "true", "yes", "on"].includes(normalized);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseListLimit(value, fallback = 30, max = 100) {
|
|
17
|
+
const numericValue = Number(value);
|
|
18
|
+
|
|
19
|
+
if (!Number.isFinite(numericValue) || numericValue < 1) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return Math.min(max, Math.round(numericValue));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseOffset(value) {
|
|
27
|
+
const numericValue = Number(value);
|
|
28
|
+
|
|
29
|
+
if (!Number.isFinite(numericValue) || numericValue < 0) {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return Math.round(numericValue);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getActiveDatabaseKey(connectionManager) {
|
|
37
|
+
return connectionManager.getActiveConnection()?.id ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
|
|
5
41
|
const router = express.Router();
|
|
6
42
|
|
|
7
43
|
router.post(
|
|
@@ -21,9 +57,32 @@ function createSqlRouter({ appStateStore, sqlExecutor }) {
|
|
|
21
57
|
router.get(
|
|
22
58
|
"/history",
|
|
23
59
|
route((req, res) => {
|
|
60
|
+
const databaseKey = getActiveDatabaseKey(connectionManager);
|
|
61
|
+
const tab = String(req.query.tab ?? "recent").trim().toLowerCase();
|
|
62
|
+
const options = {
|
|
63
|
+
databaseKey,
|
|
64
|
+
limit: parseListLimit(req.query.limit, 30, 100),
|
|
65
|
+
offset: parseOffset(req.query.offset),
|
|
66
|
+
search: String(req.query.search ?? ""),
|
|
67
|
+
queryType: String(req.query.queryType ?? "").trim() || null,
|
|
68
|
+
onlySaved: parseBooleanFlag(req.query.onlySaved),
|
|
69
|
+
onlyFavorites: parseBooleanFlag(req.query.onlyFavorites),
|
|
70
|
+
};
|
|
71
|
+
const result =
|
|
72
|
+
tab === "failed"
|
|
73
|
+
? appStateStore.getFailedQueries(options)
|
|
74
|
+
: appStateStore.getRecentQueries({
|
|
75
|
+
...options,
|
|
76
|
+
onlySaved: tab === "saved" ? true : options.onlySaved,
|
|
77
|
+
});
|
|
78
|
+
|
|
24
79
|
res.json(
|
|
25
80
|
successResponse({
|
|
26
|
-
data:
|
|
81
|
+
data: result,
|
|
82
|
+
metadata: {
|
|
83
|
+
databaseKey,
|
|
84
|
+
tab,
|
|
85
|
+
},
|
|
27
86
|
})
|
|
28
87
|
);
|
|
29
88
|
})
|
|
@@ -32,11 +91,110 @@ function createSqlRouter({ appStateStore, sqlExecutor }) {
|
|
|
32
91
|
router.delete(
|
|
33
92
|
"/history",
|
|
34
93
|
route((req, res) => {
|
|
35
|
-
|
|
94
|
+
const databaseKey = getActiveDatabaseKey(connectionManager);
|
|
95
|
+
const deletedCount = appStateStore.clearQueryHistoryForDatabase(databaseKey);
|
|
96
|
+
|
|
97
|
+
res.json(
|
|
98
|
+
successResponse({
|
|
99
|
+
message: deletedCount
|
|
100
|
+
? "Query history cleared for the active database."
|
|
101
|
+
: "No query history was found for the active database.",
|
|
102
|
+
data: {
|
|
103
|
+
deletedCount,
|
|
104
|
+
},
|
|
105
|
+
metadata: {
|
|
106
|
+
databaseKey,
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
router.get(
|
|
114
|
+
"/history/:historyId/runs",
|
|
115
|
+
route((req, res) => {
|
|
116
|
+
res.json(
|
|
117
|
+
successResponse({
|
|
118
|
+
data: appStateStore.getQueryRunsByHistoryId(
|
|
119
|
+
req.params.historyId,
|
|
120
|
+
parseListLimit(req.query.limit, 8, 50)
|
|
121
|
+
),
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
router.patch(
|
|
128
|
+
"/history/:historyId/favorite",
|
|
129
|
+
route((req, res) => {
|
|
130
|
+
const nextValue = parseBooleanFlag(req.body?.value);
|
|
131
|
+
res.json(
|
|
132
|
+
successResponse({
|
|
133
|
+
message: nextValue ? "Query favorited." : "Query removed from favorites.",
|
|
134
|
+
data: appStateStore.toggleFavorite(req.params.historyId, nextValue),
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
router.patch(
|
|
141
|
+
"/history/:historyId/saved",
|
|
142
|
+
route((req, res) => {
|
|
143
|
+
const nextValue = parseBooleanFlag(req.body?.value);
|
|
144
|
+
res.json(
|
|
145
|
+
successResponse({
|
|
146
|
+
message: nextValue ? "Query saved." : "Query removed from saved queries.",
|
|
147
|
+
data: appStateStore.toggleSaved(req.params.historyId, nextValue),
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
router.patch(
|
|
154
|
+
"/history/:historyId/title",
|
|
155
|
+
route((req, res) => {
|
|
156
|
+
res.json(
|
|
157
|
+
successResponse({
|
|
158
|
+
message: "Query title updated.",
|
|
159
|
+
data: appStateStore.renameQuery(req.params.historyId, req.body?.title),
|
|
160
|
+
})
|
|
161
|
+
);
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
router.patch(
|
|
166
|
+
"/history/:historyId/notes",
|
|
167
|
+
route((req, res) => {
|
|
168
|
+
res.json(
|
|
169
|
+
successResponse({
|
|
170
|
+
message: "Query notes updated.",
|
|
171
|
+
data: appStateStore.updateQueryNotes(req.params.historyId, req.body?.notes),
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
router.delete(
|
|
178
|
+
"/history/:historyId",
|
|
179
|
+
route((req, res) => {
|
|
180
|
+
appStateStore.deleteQueryHistoryItem(req.params.historyId);
|
|
181
|
+
res.json(
|
|
182
|
+
successResponse({
|
|
183
|
+
message: "Query history item deleted.",
|
|
184
|
+
data: {
|
|
185
|
+
id: Number(req.params.historyId),
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
router.get(
|
|
193
|
+
"/history/:historyId",
|
|
194
|
+
route((req, res) => {
|
|
36
195
|
res.json(
|
|
37
196
|
successResponse({
|
|
38
|
-
|
|
39
|
-
data: [],
|
|
197
|
+
data: appStateStore.getQueryHistoryItemById(req.params.historyId),
|
|
40
198
|
})
|
|
41
199
|
);
|
|
42
200
|
})
|
package/server/server.js
CHANGED
|
@@ -75,7 +75,7 @@ app.use(
|
|
|
75
75
|
})
|
|
76
76
|
);
|
|
77
77
|
app.use("/api/db", createOverviewRouter({ overviewService }));
|
|
78
|
-
app.use("/api/sql", createSqlRouter({ appStateStore, sqlExecutor }));
|
|
78
|
+
app.use("/api/sql", createSqlRouter({ appStateStore, connectionManager, sqlExecutor }));
|
|
79
79
|
app.use("/api/structure", createStructureRouter({ structureService }));
|
|
80
80
|
app.use("/api/data", createDataRouter({ dataBrowserService }));
|
|
81
81
|
app.use("/api/settings", createSettingsRouter({ appStateStore }));
|
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
serializeRows,
|
|
7
7
|
} = require("../../utils/sqliteTypes");
|
|
8
8
|
const { getRawStructureEntries, getTableDetail } = require("./introspection");
|
|
9
|
+
const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
|
|
9
10
|
|
|
10
11
|
const DEFAULT_LIMIT = 50;
|
|
11
12
|
const MAX_LIMIT = 100;
|
|
@@ -70,9 +71,10 @@ class DataBrowserService {
|
|
|
70
71
|
const db = this.connectionManager.getActiveDatabase();
|
|
71
72
|
const tableDetail = getTableDetail(db, tableName);
|
|
72
73
|
const { limit, offset } = normalizePaginationOptions(options);
|
|
74
|
+
const sort = normalizeTableSort(tableDetail, options);
|
|
73
75
|
const selectExpression =
|
|
74
76
|
tableDetail.identityStrategy?.type === "rowid" ? "rowid AS __rowid__, *" : "*";
|
|
75
|
-
const orderClause =
|
|
77
|
+
const orderClause = buildTableOrderClause(tableDetail, sort);
|
|
76
78
|
const statement = db.prepare(
|
|
77
79
|
[
|
|
78
80
|
`SELECT ${selectExpression} FROM ${quoteIdentifier(tableName)}`,
|
|
@@ -114,6 +116,7 @@ class DataBrowserService {
|
|
|
114
116
|
rows,
|
|
115
117
|
identityStrategy: tableDetail.identityStrategy,
|
|
116
118
|
notSafelyUpdatable: tableDetail.notSafelyUpdatable,
|
|
119
|
+
sort,
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -238,21 +241,6 @@ class DataBrowserService {
|
|
|
238
241
|
`Table ${tableDetail.name} cannot be updated because it has no stable row identity.`
|
|
239
242
|
);
|
|
240
243
|
}
|
|
241
|
-
|
|
242
|
-
buildOrderClause(tableDetail) {
|
|
243
|
-
if (tableDetail.identityStrategy?.type === "rowid") {
|
|
244
|
-
return "rowid ASC";
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (tableDetail.identityStrategy?.type === "primaryKey") {
|
|
248
|
-
return tableDetail.identityStrategy.columns
|
|
249
|
-
.map((columnName) => `${quoteIdentifier(columnName)} ASC`)
|
|
250
|
-
.join(", ");
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return "";
|
|
254
|
-
}
|
|
255
|
-
|
|
256
244
|
getRowByIdentity(db, tableDetail, where) {
|
|
257
245
|
const selectExpression =
|
|
258
246
|
tableDetail.identityStrategy?.type === "rowid" ? "rowid AS __rowid__, *" : "*";
|
|
@@ -2,6 +2,7 @@ const { quoteIdentifier } = require("../../utils/identifier");
|
|
|
2
2
|
const { serializeRows } = require("../../utils/sqliteTypes");
|
|
3
3
|
const { rowsToCsv } = require("../../utils/csv");
|
|
4
4
|
const { getTableDetail } = require("./introspection");
|
|
5
|
+
const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
|
|
5
6
|
|
|
6
7
|
class ExportService {
|
|
7
8
|
constructor({ appStateStore, connectionManager, sqlExecutor }) {
|
|
@@ -32,10 +33,11 @@ class ExportService {
|
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
exportTable(tableName) {
|
|
36
|
+
exportTable(tableName, options = {}) {
|
|
36
37
|
const db = this.connectionManager.getActiveDatabase();
|
|
37
38
|
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
38
|
-
const
|
|
39
|
+
const sort = normalizeTableSort(tableDetail, options);
|
|
40
|
+
const orderClause = buildTableOrderClause(tableDetail, sort);
|
|
39
41
|
const statement = db.prepare(
|
|
40
42
|
[
|
|
41
43
|
`SELECT * FROM ${quoteIdentifier(tableName)}`,
|
|
@@ -58,20 +60,6 @@ class ExportService {
|
|
|
58
60
|
rowCount: rows.length,
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
|
-
|
|
62
|
-
buildOrderClause(tableDetail) {
|
|
63
|
-
if (tableDetail.identityStrategy?.type === "rowid") {
|
|
64
|
-
return "rowid ASC";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (tableDetail.identityStrategy?.type === "primaryKey") {
|
|
68
|
-
return tableDetail.identityStrategy.columns
|
|
69
|
-
.map((columnName) => `${quoteIdentifier(columnName)} ASC`)
|
|
70
|
-
.join(", ");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return "";
|
|
74
|
-
}
|
|
75
63
|
}
|
|
76
64
|
|
|
77
65
|
module.exports = {
|