sqlite-hub 0.9.3 → 0.9.6
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/.github/workflows/ci.yml +36 -0
- package/README.md +2 -2
- package/bin/sqlite-hub.js +1 -1
- package/frontend/index.html +2 -158
- package/frontend/js/app.js +41 -22
- package/frontend/js/components/connectionCard.js +62 -87
- package/frontend/js/components/emptyState.js +20 -23
- package/frontend/js/components/modal.js +145 -195
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryEditor.js +16 -30
- package/frontend/js/components/queryHistoryDetail.js +93 -164
- package/frontend/js/components/queryHistoryPanel.js +81 -99
- package/frontend/js/components/queryResults.js +3 -1
- package/frontend/js/components/rowEditorPanel.js +28 -31
- package/frontend/js/components/structureGraph.js +10 -9
- package/frontend/js/components/tableDesignerEditor.js +91 -116
- package/frontend/js/store.js +39 -3
- package/frontend/js/utils/dom.js +28 -0
- package/frontend/js/utils/tableDesigner.js +8 -2
- package/frontend/js/views/charts.js +23 -43
- package/frontend/js/views/data.js +116 -132
- package/frontend/js/views/mediaTagging.js +131 -164
- package/frontend/js/views/structure.js +52 -48
- package/frontend/styles/tailwind.css +80 -0
- package/frontend/styles/tailwind.generated.css +2 -0
- package/frontend/styles/tokens.css +3 -3
- package/package.json +19 -5
- package/server/routes/mediaTagging.js +2 -10
- package/server/routes/sql.js +35 -10
- package/server/server.js +24 -0
- package/server/services/sqlite/dataBrowserService.js +25 -5
- package/server/services/sqlite/exportService.js +4 -2
- package/server/services/sqlite/introspection.js +2 -2
- package/server/services/sqlite/mediaTaggingService.js +166 -53
- package/server/services/sqlite/structureService.js +2 -2
- package/server/services/sqlite/tableDesigner/sql.js +19 -3
- package/server/services/storage/appStateStore.js +227 -87
- package/server/utils/appPaths.js +55 -19
- package/server/utils/fileValidation.js +94 -8
- package/tailwind.config.cjs +73 -0
- package/tests/security-paths.test.js +84 -0
- package/tests/sql-identifier-safety.test.js +66 -0
- package/.npmingnore +0 -4
- package/changelog.md +0 -84
- package/docs/DESIGN_GUIDELINES.md +0 -36
- package/scripts/publish_brew.sh +0 -466
- package/scripts/publish_npm.sh +0 -241
- package/shortkeys.md +0 -5
|
@@ -97,28 +97,37 @@ function renderFileField({
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function renderSelectField({ label, name, value = "", options = [], bind = "" }) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
100
|
+
const attributes = [
|
|
101
|
+
bind ? ['data-bind="', escapeHtml(bind), '"'].join("") : "",
|
|
102
|
+
name ? ['name="', escapeHtml(name), '"'].join("") : "",
|
|
103
|
+
]
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.join(" ");
|
|
106
|
+
const optionMarkup = options
|
|
107
|
+
.map((option) =>
|
|
108
|
+
[
|
|
109
|
+
'<option value="',
|
|
110
|
+
escapeHtml(option.value),
|
|
111
|
+
'" ',
|
|
112
|
+
String(option.value) === String(value) ? "selected" : "",
|
|
113
|
+
">",
|
|
114
|
+
escapeHtml(option.label),
|
|
115
|
+
"</option>",
|
|
116
|
+
].join("")
|
|
117
|
+
)
|
|
118
|
+
.join("");
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
'<label class="block space-y-2">',
|
|
122
|
+
'<span class="block text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">',
|
|
123
|
+
escapeHtml(label),
|
|
124
|
+
"</span>",
|
|
125
|
+
'<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" ',
|
|
126
|
+
attributes,
|
|
127
|
+
">",
|
|
128
|
+
optionMarkup,
|
|
129
|
+
"</select></label>",
|
|
130
|
+
].join("");
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
function renderError(error) {
|
|
@@ -283,62 +292,55 @@ function renderCreateDatabaseForm(modal) {
|
|
|
283
292
|
|
|
284
293
|
function renderImportTargetOptions(state) {
|
|
285
294
|
const recentOptions = state.connections.recent
|
|
286
|
-
.map(
|
|
287
|
-
|
|
288
|
-
<option value="
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
295
|
+
.map((connection) =>
|
|
296
|
+
[
|
|
297
|
+
'<option value="',
|
|
298
|
+
escapeHtml(connection.id),
|
|
299
|
+
'">',
|
|
300
|
+
escapeHtml(connection.label),
|
|
301
|
+
" • ",
|
|
302
|
+
escapeHtml(truncateMiddle(connection.path, 42)),
|
|
303
|
+
"</option>",
|
|
304
|
+
].join("")
|
|
292
305
|
)
|
|
293
306
|
.join("");
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
<select
|
|
322
|
-
class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
323
|
-
name="targetConnectionId"
|
|
324
|
-
>
|
|
325
|
-
${recentOptions}
|
|
326
|
-
</select>
|
|
327
|
-
</label>
|
|
328
|
-
`
|
|
329
|
-
: ""
|
|
330
|
-
}
|
|
331
|
-
${renderField({
|
|
307
|
+
const activeOption = state.connections.active
|
|
308
|
+
? '<option value="active">Use active database</option>'
|
|
309
|
+
: "";
|
|
310
|
+
const recentModeOption = state.connections.recent.length
|
|
311
|
+
? '<option value="recent">Use recent connection</option>'
|
|
312
|
+
: "";
|
|
313
|
+
const recentConnectionSelect = state.connections.recent.length
|
|
314
|
+
? [
|
|
315
|
+
'<label class="block space-y-2">',
|
|
316
|
+
'<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Recent Connection</span>',
|
|
317
|
+
'<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" name="targetConnectionId">',
|
|
318
|
+
recentOptions,
|
|
319
|
+
"</select></label>",
|
|
320
|
+
].join("")
|
|
321
|
+
: "";
|
|
322
|
+
|
|
323
|
+
return [
|
|
324
|
+
'<label class="block space-y-2">',
|
|
325
|
+
'<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Import Target</span>',
|
|
326
|
+
'<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" name="targetMode">',
|
|
327
|
+
activeOption,
|
|
328
|
+
recentModeOption,
|
|
329
|
+
'<option value="create">Create new database from dump</option>',
|
|
330
|
+
'<option value="path">Open explicit target path</option>',
|
|
331
|
+
"</select></label>",
|
|
332
|
+
recentConnectionSelect,
|
|
333
|
+
renderField({
|
|
332
334
|
label: "Target Path",
|
|
333
335
|
name: "targetPath",
|
|
334
336
|
placeholder: "/absolute/path/to/target.sqlite",
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
+
}),
|
|
338
|
+
renderField({
|
|
337
339
|
label: "Target Label",
|
|
338
340
|
name: "label",
|
|
339
341
|
placeholder: "Optional display name",
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
+
}),
|
|
343
|
+
].join("");
|
|
342
344
|
}
|
|
343
345
|
|
|
344
346
|
function renderImportSqlForm(modal, state) {
|
|
@@ -376,71 +378,51 @@ function renderImportSqlForm(modal, state) {
|
|
|
376
378
|
|
|
377
379
|
function renderDeleteRowConfirmForm(modal) {
|
|
378
380
|
const rowPreview = modal.rowPreview ?? [];
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
<
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
</div>
|
|
426
|
-
${renderError(modal.error)}
|
|
427
|
-
<div class="flex items-center justify-end gap-3 pt-2">
|
|
428
|
-
<button
|
|
429
|
-
class="standard-button"
|
|
430
|
-
data-action="close-modal"
|
|
431
|
-
type="button"
|
|
432
|
-
>
|
|
433
|
-
Cancel
|
|
434
|
-
</button>
|
|
435
|
-
<button
|
|
436
|
-
class="delete-button"
|
|
437
|
-
type="submit"
|
|
438
|
-
>
|
|
439
|
-
${modal.submitting ? "Deleting..." : "Delete Row"}
|
|
440
|
-
</button>
|
|
441
|
-
</div>
|
|
442
|
-
</form>
|
|
443
|
-
`;
|
|
381
|
+
const rowLabelMarkup = modal.rowLabel
|
|
382
|
+
? [
|
|
383
|
+
'<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
|
|
384
|
+
escapeHtml(modal.rowLabel),
|
|
385
|
+
"</div>",
|
|
386
|
+
].join("")
|
|
387
|
+
: "";
|
|
388
|
+
const rowPreviewMarkup = rowPreview.length
|
|
389
|
+
? [
|
|
390
|
+
'<div class="grid grid-cols-1 gap-3 md:grid-cols-2">',
|
|
391
|
+
rowPreview
|
|
392
|
+
.map((field) =>
|
|
393
|
+
[
|
|
394
|
+
'<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">',
|
|
395
|
+
'<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
|
|
396
|
+
escapeHtml(field.label),
|
|
397
|
+
"</div>",
|
|
398
|
+
'<div class="mt-2 text-sm text-on-surface" title="',
|
|
399
|
+
escapeHtml(field.fullValue ?? field.value ?? ""),
|
|
400
|
+
'">',
|
|
401
|
+
escapeHtml(field.value ?? ""),
|
|
402
|
+
"</div></div>",
|
|
403
|
+
].join("")
|
|
404
|
+
)
|
|
405
|
+
.join(""),
|
|
406
|
+
"</div>",
|
|
407
|
+
].join("")
|
|
408
|
+
: "";
|
|
409
|
+
|
|
410
|
+
return [
|
|
411
|
+
'<form class="space-y-5" data-form="delete-row-confirm"><div class="space-y-3">',
|
|
412
|
+
'<p class="text-sm leading-7 text-on-surface">Delete this row from <span class="font-bold text-primary-container">',
|
|
413
|
+
escapeHtml(modal.tableName ?? "the current table"),
|
|
414
|
+
"</span>?</p>",
|
|
415
|
+
'<p class="text-sm leading-7 text-on-surface-variant/65">This action cannot be undone.</p>',
|
|
416
|
+
rowLabelMarkup,
|
|
417
|
+
rowPreviewMarkup,
|
|
418
|
+
"</div>",
|
|
419
|
+
renderError(modal.error),
|
|
420
|
+
'<div class="flex items-center justify-end gap-3 pt-2">',
|
|
421
|
+
'<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
|
|
422
|
+
'<button class="delete-button" type="submit">',
|
|
423
|
+
modal.submitting ? "Deleting..." : "Delete Row",
|
|
424
|
+
"</button></div></form>",
|
|
425
|
+
].join("");
|
|
444
426
|
}
|
|
445
427
|
|
|
446
428
|
function renderChartColumnOptions(analysis, { allowEmpty = false, includeNumericHint = false } = {}) {
|
|
@@ -693,69 +675,37 @@ function renderChartEditorForm(modal, state) {
|
|
|
693
675
|
}
|
|
694
676
|
|
|
695
677
|
function renderDeleteChartForm(modal) {
|
|
696
|
-
return
|
|
697
|
-
<form class="space-y-5" data-form="delete-query-chart">
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
<button
|
|
711
|
-
class="standard-button"
|
|
712
|
-
data-action="close-modal"
|
|
713
|
-
type="button"
|
|
714
|
-
>
|
|
715
|
-
Cancel
|
|
716
|
-
</button>
|
|
717
|
-
<button
|
|
718
|
-
class="delete-button"
|
|
719
|
-
type="submit"
|
|
720
|
-
>
|
|
721
|
-
${modal.submitting ? "Deleting..." : "Delete Chart"}
|
|
722
|
-
</button>
|
|
723
|
-
</div>
|
|
724
|
-
</form>
|
|
725
|
-
`;
|
|
678
|
+
return [
|
|
679
|
+
'<form class="space-y-5" data-form="delete-query-chart"><div class="space-y-3">',
|
|
680
|
+
'<p class="text-sm leading-7 text-on-surface">Delete chart <span class="font-bold text-primary-container">',
|
|
681
|
+
escapeHtml(modal.chartName ?? "Chart"),
|
|
682
|
+
"</span>?</p>",
|
|
683
|
+
'<p class="text-sm leading-7 text-on-surface-variant/65">The linked query-history entry stays intact. Only this chart definition is removed.</p>',
|
|
684
|
+
"</div>",
|
|
685
|
+
renderError(modal.error),
|
|
686
|
+
'<div class="flex items-center justify-end gap-3 pt-2">',
|
|
687
|
+
'<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
|
|
688
|
+
'<button class="delete-button" type="submit">',
|
|
689
|
+
modal.submitting ? "Deleting..." : "Delete Chart",
|
|
690
|
+
"</button></div></form>",
|
|
691
|
+
].join("");
|
|
726
692
|
}
|
|
727
693
|
|
|
728
694
|
function renderDeleteQueryHistoryForm(modal) {
|
|
729
|
-
return
|
|
730
|
-
<form class="space-y-5" data-form="delete-query-history-confirm">
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
<button
|
|
744
|
-
class="standard-button"
|
|
745
|
-
data-action="close-modal"
|
|
746
|
-
type="button"
|
|
747
|
-
>
|
|
748
|
-
Cancel
|
|
749
|
-
</button>
|
|
750
|
-
<button
|
|
751
|
-
class="delete-button"
|
|
752
|
-
type="submit"
|
|
753
|
-
>
|
|
754
|
-
${modal.submitting ? "Deleting..." : "Delete Query"}
|
|
755
|
-
</button>
|
|
756
|
-
</div>
|
|
757
|
-
</form>
|
|
758
|
-
`;
|
|
695
|
+
return [
|
|
696
|
+
'<form class="space-y-5" data-form="delete-query-history-confirm"><div class="space-y-3">',
|
|
697
|
+
'<p class="text-sm leading-7 text-on-surface">Delete query <span class="font-bold text-primary-container">',
|
|
698
|
+
escapeHtml(modal.queryTitle ?? "SQL query"),
|
|
699
|
+
"</span>?</p>",
|
|
700
|
+
'<p class="text-sm leading-7 text-on-surface-variant/65">This removes the query-history entry and all recorded runs linked to it.</p>',
|
|
701
|
+
"</div>",
|
|
702
|
+
renderError(modal.error),
|
|
703
|
+
'<div class="flex items-center justify-end gap-3 pt-2">',
|
|
704
|
+
'<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
|
|
705
|
+
'<button class="delete-button" type="submit">',
|
|
706
|
+
modal.submitting ? "Deleting..." : "Delete Query",
|
|
707
|
+
"</button></div></form>",
|
|
708
|
+
].join("");
|
|
759
709
|
}
|
|
760
710
|
|
|
761
711
|
function renderCreateMediaTaggingMappingTableForm(modal, state) {
|
|
@@ -16,7 +16,7 @@ export function renderPageHeader({ eyebrow = "", title, subtitle = "", actions =
|
|
|
16
16
|
`
|
|
17
17
|
: ""
|
|
18
18
|
}
|
|
19
|
-
<h1 class="text-5xl font-
|
|
19
|
+
<h1 class="text-5xl font-headline font-bold text-[#FCE300] tracking-tighter uppercase">${escapeHtml(
|
|
20
20
|
title
|
|
21
21
|
)}</h1>
|
|
22
22
|
${
|
|
@@ -18,36 +18,22 @@ function renderHighlightedQuery(query) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function renderEditorSurface({ query }) {
|
|
21
|
-
return
|
|
22
|
-
<div class="query-editor-shell flex min-h-0 flex-1 overflow-hidden">
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
aria-hidden="true"
|
|
38
|
-
class="query-editor-highlight"
|
|
39
|
-
data-query-editor-highlight
|
|
40
|
-
>${renderHighlightedQuery(query)}</div>
|
|
41
|
-
<textarea
|
|
42
|
-
class="query-editor-input custom-scrollbar relative z-10 h-full min-h-[140px] w-full resize-none border-none focus:ring-0"
|
|
43
|
-
data-bind="current-query"
|
|
44
|
-
placeholder="SELECT name FROM sqlite_master WHERE type = 'table';"
|
|
45
|
-
spellcheck="false"
|
|
46
|
-
>${escapeHtml(query)}</textarea>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
`;
|
|
21
|
+
return [
|
|
22
|
+
'<div class="query-editor-shell flex min-h-0 flex-1 overflow-hidden">',
|
|
23
|
+
'<div class="flex w-12 min-h-0 overflow-hidden bg-surface-container-lowest py-4 font-mono text-xs select-none text-outline-variant/30">',
|
|
24
|
+
'<div class="query-editor-gutter-track flex w-full flex-col items-center" data-query-editor-gutter>',
|
|
25
|
+
renderLineNumbers(query),
|
|
26
|
+
"</div></div>",
|
|
27
|
+
'<div class="relative min-h-0 flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">',
|
|
28
|
+
'<div class="pointer-events-none absolute right-0 top-0 p-4 opacity-5"><span class="material-symbols-outlined text-[120px] font-thin">terminal</span></div>',
|
|
29
|
+
'<div class="query-editor-layer relative z-10 h-full min-h-[140px]">',
|
|
30
|
+
'<div aria-hidden="true" class="query-editor-highlight" data-query-editor-highlight>',
|
|
31
|
+
renderHighlightedQuery(query),
|
|
32
|
+
"</div>",
|
|
33
|
+
'<textarea class="query-editor-input custom-scrollbar relative z-10 h-full min-h-[140px] w-full resize-none border-none focus:ring-0" data-bind="current-query" placeholder="SELECT name FROM sqlite_master WHERE type = \'table\';" spellcheck="false">',
|
|
34
|
+
escapeHtml(query),
|
|
35
|
+
"</textarea></div></div></div>",
|
|
36
|
+
].join("");
|
|
51
37
|
}
|
|
52
38
|
|
|
53
39
|
export function renderQueryEditor({
|