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,5 +1,7 @@
|
|
|
1
1
|
import { escapeHtml } from "../utils/format.js";
|
|
2
2
|
|
|
3
|
+
const URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
|
|
4
|
+
|
|
3
5
|
function getJsonPreview(value) {
|
|
4
6
|
if (value === null || value === undefined) {
|
|
5
7
|
return null;
|
|
@@ -28,6 +30,114 @@ function getJsonPreview(value) {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function getUrlValue(value) {
|
|
34
|
+
const text = String(value ?? "").trim();
|
|
35
|
+
|
|
36
|
+
if (!URL_PATTERN.test(text)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(text);
|
|
42
|
+
|
|
43
|
+
return ["http:", "https:"].includes(url.protocol) ? url.href : null;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function withUrlBadge(badges = [], url) {
|
|
50
|
+
if (!url) {
|
|
51
|
+
return badges;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const hasUrlBadge = badges.some((badge) => {
|
|
55
|
+
const label = typeof badge === "object" ? badge.label : badge;
|
|
56
|
+
return String(label ?? "").toUpperCase() === "URL";
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return hasUrlBadge ? badges : [...badges, { label: "URL", tone: "url" }];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getAllowedValues(field) {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
|
|
65
|
+
return (Array.isArray(field.allowedValues) ? field.allowedValues : [])
|
|
66
|
+
.map((value) => String(value))
|
|
67
|
+
.filter((value) => {
|
|
68
|
+
if (seen.has(value)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
seen.add(value);
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function withCheckBadge(badges = [], allowedValues = []) {
|
|
78
|
+
if (!allowedValues.length) {
|
|
79
|
+
return badges;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const hasCheckBadge = badges.some((badge) => {
|
|
83
|
+
const label = typeof badge === "object" ? badge.label : badge;
|
|
84
|
+
return String(label ?? "").toUpperCase() === "CHECK";
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return hasCheckBadge ? badges : [...badges, { label: "CHECK", tone: "check" }];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderOpenUrlButton(url) {
|
|
91
|
+
if (!url) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return `
|
|
96
|
+
<div class="mt-2">
|
|
97
|
+
<button
|
|
98
|
+
class="standard-button"
|
|
99
|
+
data-action="open-row-editor-url"
|
|
100
|
+
data-url="${escapeHtml(url)}"
|
|
101
|
+
type="button"
|
|
102
|
+
>
|
|
103
|
+
<span class="material-symbols-outlined text-sm">open_in_new</span>
|
|
104
|
+
Open in tab
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderAllowedValuesSelect(field, allowedValues) {
|
|
111
|
+
const currentValue = String(field.value ?? "");
|
|
112
|
+
const hasCurrentAllowedValue = allowedValues.includes(currentValue);
|
|
113
|
+
const shouldRenderEmptyOption = field.notNull !== true;
|
|
114
|
+
const shouldRenderCurrentOption =
|
|
115
|
+
currentValue !== "" && !hasCurrentAllowedValue;
|
|
116
|
+
const options = [
|
|
117
|
+
shouldRenderEmptyOption
|
|
118
|
+
? `<option value="" ${currentValue === "" ? "selected" : ""}>NULL / empty</option>`
|
|
119
|
+
: "",
|
|
120
|
+
shouldRenderCurrentOption
|
|
121
|
+
? `<option value="${escapeHtml(currentValue)}" selected>${escapeHtml(currentValue)}</option>`
|
|
122
|
+
: "",
|
|
123
|
+
...allowedValues.map(
|
|
124
|
+
(value) =>
|
|
125
|
+
`<option value="${escapeHtml(value)}" ${
|
|
126
|
+
value === currentValue ? "selected" : ""
|
|
127
|
+
}>${escapeHtml(value)}</option>`
|
|
128
|
+
),
|
|
129
|
+
].join("");
|
|
130
|
+
|
|
131
|
+
return `
|
|
132
|
+
<select
|
|
133
|
+
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
134
|
+
name="field:${escapeHtml(field.name)}"
|
|
135
|
+
>
|
|
136
|
+
${options}
|
|
137
|
+
</select>
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
|
|
31
141
|
function renderJsonViewer(prettyJson, title = "JSON Viewer") {
|
|
32
142
|
return `
|
|
33
143
|
<div class="border border-outline-variant/10 bg-surface-container px-4 py-4">
|
|
@@ -42,12 +152,15 @@ function renderJsonViewer(prettyJson, title = "JSON Viewer") {
|
|
|
42
152
|
}
|
|
43
153
|
|
|
44
154
|
function renderReadonlyField(label, value) {
|
|
45
|
-
const
|
|
155
|
+
const url = getUrlValue(value);
|
|
156
|
+
const badges = withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url);
|
|
46
157
|
const displayLabel = typeof label === "object" ? label.label : label;
|
|
47
158
|
const jsonPreview = getJsonPreview(value);
|
|
48
159
|
|
|
49
160
|
return `
|
|
50
|
-
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3"
|
|
161
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3" ${
|
|
162
|
+
url ? "data-row-editor-url-field" : ""
|
|
163
|
+
}>
|
|
51
164
|
<div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
52
165
|
<span>${escapeHtml(displayLabel)}</span>
|
|
53
166
|
${badges.map((badge) => renderFieldBadge(badge)).join("")}
|
|
@@ -57,18 +170,22 @@ function renderReadonlyField(label, value) {
|
|
|
57
170
|
? `<div class="mt-2">${renderJsonViewer(jsonPreview)}</div>`
|
|
58
171
|
: `<div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>`
|
|
59
172
|
}
|
|
173
|
+
${renderOpenUrlButton(url)}
|
|
60
174
|
</div>
|
|
61
175
|
`;
|
|
62
176
|
}
|
|
63
177
|
|
|
64
178
|
function renderEditableField(field) {
|
|
65
|
-
const
|
|
179
|
+
const url = getUrlValue(field.value);
|
|
180
|
+
const allowedValues = getAllowedValues(field);
|
|
181
|
+
const baseBadges = withCheckBadge(Array.isArray(field.badges) ? field.badges : [], allowedValues);
|
|
182
|
+
const badges = withUrlBadge(baseBadges, url);
|
|
66
183
|
const jsonPreview = getJsonPreview(field.value);
|
|
67
184
|
const inputType = field.inputType === "number" ? "number" : "text";
|
|
68
185
|
const numberStep = field.numberStep === "1" ? "1" : "any";
|
|
69
186
|
|
|
70
187
|
return `
|
|
71
|
-
<
|
|
188
|
+
<div class="block space-y-2" ${url ? "data-row-editor-url-field" : ""}>
|
|
72
189
|
<span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
73
190
|
<span>${escapeHtml(field.label ?? field.name)}</span>
|
|
74
191
|
${badges.map((badge) => renderFieldBadge(badge)).join("")}
|
|
@@ -79,7 +196,9 @@ function renderEditableField(field) {
|
|
|
79
196
|
: ""
|
|
80
197
|
}
|
|
81
198
|
${
|
|
82
|
-
|
|
199
|
+
allowedValues.length && !jsonPreview
|
|
200
|
+
? renderAllowedValuesSelect(field, allowedValues)
|
|
201
|
+
: inputType === "number" && !jsonPreview
|
|
83
202
|
? `
|
|
84
203
|
<input
|
|
85
204
|
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
@@ -89,6 +208,18 @@ function renderEditableField(field) {
|
|
|
89
208
|
value="${escapeHtml(field.value ?? "")}"
|
|
90
209
|
/>
|
|
91
210
|
`
|
|
211
|
+
: url && !jsonPreview
|
|
212
|
+
? `
|
|
213
|
+
<input
|
|
214
|
+
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
215
|
+
name="field:${escapeHtml(field.name)}"
|
|
216
|
+
data-row-editor-url-input
|
|
217
|
+
spellcheck="false"
|
|
218
|
+
type="text"
|
|
219
|
+
value="${escapeHtml(field.value ?? "")}"
|
|
220
|
+
/>
|
|
221
|
+
${renderOpenUrlButton(url)}
|
|
222
|
+
`
|
|
92
223
|
: `
|
|
93
224
|
<textarea
|
|
94
225
|
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container ${
|
|
@@ -99,7 +230,7 @@ function renderEditableField(field) {
|
|
|
99
230
|
>${escapeHtml(field.value ?? "")}</textarea>
|
|
100
231
|
`
|
|
101
232
|
}
|
|
102
|
-
</
|
|
233
|
+
</div>
|
|
103
234
|
`;
|
|
104
235
|
}
|
|
105
236
|
|
|
@@ -112,6 +243,14 @@ function getFieldBadgeClassName(tone) {
|
|
|
112
243
|
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
113
244
|
}
|
|
114
245
|
|
|
246
|
+
if (tone === "url") {
|
|
247
|
+
return "border-primary-container/35 bg-primary-container/15 text-primary-container";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (tone === "check") {
|
|
251
|
+
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
252
|
+
}
|
|
253
|
+
|
|
115
254
|
return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
|
|
116
255
|
}
|
|
117
256
|
|
|
@@ -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' },
|