sqlite-hub 0.11.1 → 0.16.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 +52 -13
- package/database.sqlite +0 -0
- package/frontend/js/api.js +64 -12
- package/frontend/js/app.js +723 -37
- package/frontend/js/components/emptyState.js +42 -46
- package/frontend/js/components/modal.js +526 -2
- package/frontend/js/components/queryEditor.js +12 -3
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +345 -12
- package/frontend/js/components/sidebar.js +106 -23
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/store.js +437 -26
- package/frontend/js/utils/copyColumnExport.js +117 -0
- package/frontend/js/utils/exportFilenames.js +32 -0
- package/frontend/js/utils/filePathPreview.js +315 -0
- package/frontend/js/utils/format.js +1 -1
- package/frontend/js/utils/rowEditorJson.js +65 -0
- package/frontend/js/utils/sqlFormatter.js +691 -0
- package/frontend/js/utils/tableDesigner.js +178 -6
- package/frontend/js/utils/textCellStats.js +20 -0
- package/frontend/js/utils/timestampPreview.js +264 -0
- package/frontend/js/views/charts.js +3 -1
- package/frontend/js/views/data.js +39 -4
- package/frontend/js/views/editor.js +50 -1
- package/frontend/js/views/settings.js +1 -4
- package/frontend/js/views/structure.js +154 -212
- package/frontend/styles/base.css +6 -0
- package/frontend/styles/components.css +463 -2
- package/frontend/styles/structure-graph.css +0 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +95 -95
- package/package.json +2 -3
- package/server/routes/export.js +97 -12
- package/server/services/sqlite/dataBrowserService.js +2 -68
- package/server/services/sqlite/exportService.js +74 -15
- package/server/services/sqlite/introspection.js +209 -1
- package/server/services/sqlite/sqlExecutor.js +31 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
- package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
- package/server/services/sqlite/tableDesigner/validation.js +60 -2
- package/server/services/sqlite/tableDesignerService.js +1 -1
- package/server/services/sqlite/tableFilter.js +75 -0
- package/server/utils/csv.js +30 -4
- package/tests/check-constraint-options.test.js +90 -0
- package/tests/export-filenames.test.js +34 -0
- package/tests/file-path-preview.test.js +165 -0
- package/tests/row-editor-json.test.js +82 -0
- package/tests/row-editor-timestamp-preview.test.js +192 -0
- package/tests/sql-formatter.test.js +173 -0
- package/tests/sql-highlight.test.js +38 -0
- package/tests/table-designer-v2-unique-constraints.test.js +78 -0
- package/tests/text-cell-stats.test.js +38 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeHtml } from '../utils/format.js';
|
|
1
|
+
import { escapeHtml, formatCompactDateTime, truncateMiddle } from '../utils/format.js';
|
|
2
2
|
import { renderConnectionLogo } from './connectionLogo.js';
|
|
3
3
|
|
|
4
4
|
const sidebarItems = [
|
|
@@ -6,16 +6,16 @@ const sidebarItems = [
|
|
|
6
6
|
{ label: 'Overview', href: '#/overview', key: 'overview', icon: 'dashboard' },
|
|
7
7
|
{ label: 'Data', href: '#/data', key: 'data', icon: 'table_rows' },
|
|
8
8
|
{ label: 'Structure', href: '#/structure', key: 'structure', icon: 'account_tree' },
|
|
9
|
-
{ label: '
|
|
9
|
+
{ label: 'SQL_Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
|
|
10
10
|
{ label: 'Charts', href: '#/charts', key: 'charts', icon: 'bar_chart' },
|
|
11
|
-
{ label: '
|
|
11
|
+
{ label: 'Table_Designer', href: '#/table-designer', key: 'tableDesigner', icon: 'table_chart' },
|
|
12
12
|
{
|
|
13
|
-
label: '
|
|
13
|
+
label: 'MEDIA_TAGGING',
|
|
14
14
|
key: 'mediaTagging',
|
|
15
15
|
icon: 'sell',
|
|
16
16
|
children: [
|
|
17
|
-
{ label: '
|
|
18
|
-
{ label: '
|
|
17
|
+
{ label: 'SETUP', href: '#/media-tagging', key: 'mediaTaggingSetup' },
|
|
18
|
+
{ label: 'TAGGING_QUEUE', href: '#/media-tagging/queue', key: 'mediaTaggingQueue' },
|
|
19
19
|
],
|
|
20
20
|
},
|
|
21
21
|
{ label: 'Settings', href: '#/settings', key: 'settings', icon: 'settings' },
|
|
@@ -37,9 +37,107 @@ function getActiveSidebarKey(routeName) {
|
|
|
37
37
|
return routeName;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function getConnectionTimeValue(connection) {
|
|
41
|
+
const value = connection?.lastOpenedAt ?? connection?.lastModifiedAt ?? 0;
|
|
42
|
+
const time = new Date(value).getTime();
|
|
43
|
+
return Number.isFinite(time) ? time : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getQuickPickConnections(state) {
|
|
47
|
+
const byId = new Map();
|
|
48
|
+
|
|
49
|
+
[state.connections.active, ...(state.connections.recent ?? [])].forEach(connection => {
|
|
50
|
+
if (!connection?.id || byId.has(connection.id)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
byId.set(connection.id, connection);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return [...byId.values()]
|
|
58
|
+
.sort((left, right) => getConnectionTimeValue(right) - getConnectionTimeValue(left))
|
|
59
|
+
.slice(0, 5);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderQuickPickCard(connection, activeConnectionId) {
|
|
63
|
+
const isActive = connection.id === activeConnectionId;
|
|
64
|
+
const label = connection.label || 'Untitled database';
|
|
65
|
+
const path = connection.path || '';
|
|
66
|
+
|
|
67
|
+
return `
|
|
68
|
+
<button
|
|
69
|
+
class="sidebar-db-picker-card ${isActive ? 'is-active' : ''}"
|
|
70
|
+
data-action="select-connection"
|
|
71
|
+
data-connection-id="${escapeHtml(connection.id)}"
|
|
72
|
+
type="button"
|
|
73
|
+
>
|
|
74
|
+
${renderConnectionLogo(connection, {
|
|
75
|
+
containerClass: 'sidebar-db-picker-card__mark overflow-hidden',
|
|
76
|
+
imageClassName: 'h-full w-full object-cover',
|
|
77
|
+
iconClassName: 'text-[15px]',
|
|
78
|
+
icon: 'database',
|
|
79
|
+
})}
|
|
80
|
+
<span class="sidebar-db-picker-card__body">
|
|
81
|
+
<span class="sidebar-db-picker-card__label" title="${escapeHtml(label)}">
|
|
82
|
+
${escapeHtml(label)}
|
|
83
|
+
</span>
|
|
84
|
+
<span class="sidebar-db-picker-card__path" title="${escapeHtml(path)}">
|
|
85
|
+
${escapeHtml(path ? truncateMiddle(path, 34) : 'path:n/a')}
|
|
86
|
+
</span>
|
|
87
|
+
<span class="sidebar-db-picker-card__meta">
|
|
88
|
+
${isActive ? 'ACTIVE' : escapeHtml(formatCompactDateTime(connection.lastOpenedAt))}
|
|
89
|
+
${connection.readOnly ? ' // READ_ONLY' : ''}
|
|
90
|
+
</span>
|
|
91
|
+
</span>
|
|
92
|
+
</button>
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderConnectionQuickPicker(state) {
|
|
97
|
+
const activeConnection = state.connections.active;
|
|
98
|
+
const quickPicks = getQuickPickConnections(state);
|
|
99
|
+
const hasQuickPicks = quickPicks.length > 0;
|
|
100
|
+
|
|
101
|
+
return `
|
|
102
|
+
<details class="sidebar-db-picker">
|
|
103
|
+
<summary class="sidebar-footer-card" title="Switch active database">
|
|
104
|
+
${renderConnectionLogo(activeConnection, {
|
|
105
|
+
containerClass: 'sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary',
|
|
106
|
+
imageClassName: 'h-full w-full object-cover',
|
|
107
|
+
iconClassName: 'text-[15px]',
|
|
108
|
+
icon: 'memory',
|
|
109
|
+
})}
|
|
110
|
+
<span class="min-w-0 flex-1">
|
|
111
|
+
<span class="block truncate text-[10px] font-bold text-on-surface">
|
|
112
|
+
${escapeHtml(activeConnection?.label ?? 'NO_ACTIVE_DATABASE')}
|
|
113
|
+
</span>
|
|
114
|
+
<span class="block text-[8px] text-on-surface-variant/60">
|
|
115
|
+
${activeConnection?.readOnly ? 'READ_ONLY' : activeConnection ? 'READ_WRITE' : 'SELECT_DATABASE'}
|
|
116
|
+
</span>
|
|
117
|
+
</span>
|
|
118
|
+
<span class="material-symbols-outlined sidebar-db-picker__chevron" aria-hidden="true">expand_less</span>
|
|
119
|
+
</summary>
|
|
120
|
+
<div class="sidebar-db-picker__panel">
|
|
121
|
+
<div class="sidebar-db-picker__header">
|
|
122
|
+
<span>Quick Picks</span>
|
|
123
|
+
<span>${escapeHtml(String(quickPicks.length))}/5</span>
|
|
124
|
+
</div>
|
|
125
|
+
${
|
|
126
|
+
hasQuickPicks
|
|
127
|
+
? `<div class="sidebar-db-picker__list">
|
|
128
|
+
${quickPicks
|
|
129
|
+
.map(connection => renderQuickPickCard(connection, activeConnection?.id))
|
|
130
|
+
.join('')}
|
|
131
|
+
</div>`
|
|
132
|
+
: `<div class="sidebar-db-picker__empty">No recent databases</div>`
|
|
133
|
+
}
|
|
134
|
+
</div>
|
|
135
|
+
</details>
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
|
|
40
139
|
export function renderSidebar(state) {
|
|
41
140
|
const activeKey = getActiveSidebarKey(state.route.name);
|
|
42
|
-
const activeConnection = state.connections.active;
|
|
43
141
|
const expandedKey = activeKey === 'mediaTagging' ? 'mediaTagging' : null;
|
|
44
142
|
|
|
45
143
|
return `
|
|
@@ -86,22 +184,7 @@ export function renderSidebar(state) {
|
|
|
86
184
|
.join('')}
|
|
87
185
|
</nav>
|
|
88
186
|
<div class="sidebar-footer">
|
|
89
|
-
|
|
90
|
-
${renderConnectionLogo(activeConnection, {
|
|
91
|
-
containerClass: 'sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary',
|
|
92
|
-
imageClassName: 'h-full w-full object-cover',
|
|
93
|
-
iconClassName: 'text-[15px]',
|
|
94
|
-
icon: 'memory',
|
|
95
|
-
})}
|
|
96
|
-
<div class="min-w-0">
|
|
97
|
-
<p class="truncate text-[10px] font-bold text-on-surface">
|
|
98
|
-
${escapeHtml(activeConnection?.label ?? 'NO_ACTIVE_DATABASE')}
|
|
99
|
-
</p>
|
|
100
|
-
<p class="text-[8px] text-on-surface-variant/60">
|
|
101
|
-
${activeConnection?.readOnly ? 'READ_ONLY' : 'READ_WRITE'}
|
|
102
|
-
</p>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
187
|
+
${renderConnectionQuickPicker(state)}
|
|
105
188
|
</div>
|
|
106
189
|
`;
|
|
107
190
|
}
|
|
@@ -81,6 +81,54 @@ export function renderTableDesignerReferenceColumnOptions(
|
|
|
81
81
|
].join("");
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function normalizeConstraintColumnName(name) {
|
|
85
|
+
return String(name ?? "").trim().toLowerCase();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function constraintIncludesColumn(constraint, columnName) {
|
|
89
|
+
const normalizedColumn = normalizeConstraintColumnName(columnName);
|
|
90
|
+
|
|
91
|
+
if (!normalizedColumn) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (constraint.columns ?? []).some(
|
|
96
|
+
(constraintColumn) => normalizeConstraintColumnName(constraintColumn.name) === normalizedColumn
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function countColumnCheckConstraints(column, draft) {
|
|
101
|
+
if (draft.mode !== "edit") {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (draft.checkConstraints ?? []).filter((constraint) =>
|
|
106
|
+
constraintIncludesColumn(constraint, column.name)
|
|
107
|
+
).length;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderColumnCheckAction(column, draft) {
|
|
111
|
+
const checkCount = countColumnCheckConstraints(column, draft);
|
|
112
|
+
const columnName = column.name || "Unnamed column";
|
|
113
|
+
|
|
114
|
+
return `
|
|
115
|
+
<button
|
|
116
|
+
aria-label="Checks for ${escapeHtml(columnName)}"
|
|
117
|
+
class="standard-button table-designer-row-check-button"
|
|
118
|
+
data-action="open-modal"
|
|
119
|
+
data-column-id="${escapeHtml(column.id)}"
|
|
120
|
+
data-column-name="${escapeHtml(column.name)}"
|
|
121
|
+
data-modal="table-designer-constraints"
|
|
122
|
+
title="Checks for ${escapeHtml(columnName)}"
|
|
123
|
+
type="button"
|
|
124
|
+
>
|
|
125
|
+
<span class="material-symbols-outlined text-base">fact_check</span>
|
|
126
|
+
<span>Checks</span>
|
|
127
|
+
${checkCount ? `<span class="status-badge status-badge--muted">${escapeHtml(String(checkCount))}</span>` : ""}
|
|
128
|
+
</button>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
84
132
|
function renderColumnRow(column, draft, catalogTables) {
|
|
85
133
|
const typeOptions = draft.supportedTypes
|
|
86
134
|
.map((type) =>
|
|
@@ -162,10 +210,13 @@ function renderColumnRow(column, draft, catalogTables) {
|
|
|
162
210
|
'" data-field="referencesColumn">',
|
|
163
211
|
referenceColumnOptions,
|
|
164
212
|
"</select>",
|
|
213
|
+
'<div class="table-designer-row-actions">',
|
|
214
|
+
renderColumnCheckAction(column, draft),
|
|
165
215
|
'<button class="delete-button" data-action="remove-table-designer-column" data-column-id="',
|
|
166
216
|
columnId,
|
|
167
217
|
'" type="button"><span class="material-symbols-outlined text-base">delete</span><span>Remove</span></button>',
|
|
168
218
|
"</div>",
|
|
219
|
+
"</div>",
|
|
169
220
|
].join("");
|
|
170
221
|
}
|
|
171
222
|
|
|
@@ -183,7 +234,7 @@ function renderColumnGrid(draft, catalogTables) {
|
|
|
183
234
|
<div>Default</div>
|
|
184
235
|
<div>FK Table</div>
|
|
185
236
|
<div>FK Column</div>
|
|
186
|
-
<div
|
|
237
|
+
<div>Actions</div>
|
|
187
238
|
</div>
|
|
188
239
|
${visibleColumns.map((column) => renderColumnRow(column, draft, catalogTables)).join("")}
|
|
189
240
|
</div>
|
|
@@ -225,6 +276,10 @@ export function renderTableDesignerFeedback(draft, saveError) {
|
|
|
225
276
|
title: message,
|
|
226
277
|
}));
|
|
227
278
|
const warningItems = draft?.warnings ?? [];
|
|
279
|
+
const schemaNotes = warningItems.filter(
|
|
280
|
+
(item) => item.tone === "muted" && item.code !== "COMPLEX_UNIQUE_CONSTRAINTS_PRESENT"
|
|
281
|
+
);
|
|
282
|
+
const alertWarnings = warningItems.filter((item) => item.tone !== "muted");
|
|
228
283
|
|
|
229
284
|
return [
|
|
230
285
|
saveError
|
|
@@ -233,10 +288,11 @@ export function renderTableDesignerFeedback(draft, saveError) {
|
|
|
233
288
|
<div class="table-designer-main__error-code">${escapeHtml(saveError.code)}</div>
|
|
234
289
|
<div class="table-designer-main__error-text">${escapeHtml(saveError.message)}</div>
|
|
235
290
|
</div>
|
|
236
|
-
|
|
291
|
+
`
|
|
237
292
|
: "",
|
|
238
293
|
renderWarningList(validationItems, "table-designer-banner is-validation", "Validation", "alert"),
|
|
239
|
-
renderWarningList(
|
|
294
|
+
renderWarningList(alertWarnings, "table-designer-banner is-warning", "Warnings", "alert"),
|
|
295
|
+
renderWarningList(schemaNotes, "table-designer-banner is-note", "Schema Notes", "muted"),
|
|
240
296
|
].join("");
|
|
241
297
|
}
|
|
242
298
|
|
|
@@ -305,7 +361,7 @@ export function renderTableDesignerEditor(state) {
|
|
|
305
361
|
<div class="table-designer-main__subtitle">
|
|
306
362
|
${escapeHtml(String(visibleColumns.length))} visible column${
|
|
307
363
|
visibleColumns.length === 1 ? "" : "s"
|
|
308
|
-
} // SQLite-safe operations only
|
|
364
|
+
} // Table Designer v2 // SQLite-safe operations only
|
|
309
365
|
</div>
|
|
310
366
|
</div>
|
|
311
367
|
<div class="table-designer-main__actions">
|
|
@@ -338,13 +394,15 @@ export function renderTableDesignerEditor(state) {
|
|
|
338
394
|
<div>
|
|
339
395
|
<div class="table-designer-main__section-title">Columns</div>
|
|
340
396
|
</div>
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
397
|
+
<div class="table-designer-main__section-actions">
|
|
398
|
+
<button
|
|
399
|
+
class="standard-button"
|
|
400
|
+
data-action="add-table-designer-column"
|
|
401
|
+
type="button"
|
|
402
|
+
>
|
|
403
|
+
+ Add Column
|
|
404
|
+
</button>
|
|
405
|
+
</div>
|
|
348
406
|
</div>
|
|
349
407
|
${renderColumnGrid(draft, catalogTables)}
|
|
350
408
|
</section>
|