sqlite-hub 0.9.1 → 0.9.4

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.
Files changed (56) hide show
  1. package/.github/workflows/ci.yml +36 -0
  2. package/README.md +2 -2
  3. package/bin/sqlite-hub.js +1 -1
  4. package/frontend/index.html +3 -158
  5. package/frontend/js/app.js +231 -5
  6. package/frontend/js/components/connectionCard.js +62 -87
  7. package/frontend/js/components/emptyState.js +20 -23
  8. package/frontend/js/components/modal.js +158 -199
  9. package/frontend/js/components/pageHeader.js +1 -1
  10. package/frontend/js/components/queryEditor.js +16 -30
  11. package/frontend/js/components/queryHistoryDetail.js +93 -164
  12. package/frontend/js/components/queryHistoryPanel.js +90 -100
  13. package/frontend/js/components/queryResults.js +3 -1
  14. package/frontend/js/components/rowEditorPanel.js +76 -56
  15. package/frontend/js/components/tableDesignerEditor.js +91 -116
  16. package/frontend/js/lib/queryChartOptions.js +5 -1
  17. package/frontend/js/lib/queryCharts.js +50 -2
  18. package/frontend/js/store.js +161 -23
  19. package/frontend/js/utils/tableDesigner.js +8 -2
  20. package/frontend/js/views/charts.js +126 -73
  21. package/frontend/js/views/data.js +147 -133
  22. package/frontend/js/views/editor.js +1 -0
  23. package/frontend/js/views/mediaTagging.js +131 -164
  24. package/frontend/js/views/structure.js +52 -48
  25. package/frontend/styles/base.css +57 -13
  26. package/frontend/styles/components.css +166 -96
  27. package/frontend/styles/layout.css +1 -1
  28. package/frontend/styles/structure-graph.css +11 -11
  29. package/frontend/styles/tailwind.css +81 -0
  30. package/frontend/styles/tailwind.generated.css +1 -0
  31. package/frontend/styles/tokens.css +50 -7
  32. package/frontend/styles/utilities.css +21 -0
  33. package/frontend/styles/views.css +94 -86
  34. package/package.json +16 -3
  35. package/server/routes/mediaTagging.js +2 -10
  36. package/server/routes/sql.js +35 -10
  37. package/server/server.js +24 -0
  38. package/server/services/sqlite/dataBrowserService.js +25 -5
  39. package/server/services/sqlite/exportService.js +4 -2
  40. package/server/services/sqlite/introspection.js +2 -2
  41. package/server/services/sqlite/mediaTaggingService.js +166 -53
  42. package/server/services/sqlite/structureService.js +2 -2
  43. package/server/services/sqlite/tableDesigner/sql.js +19 -3
  44. package/server/services/storage/appStateStore.js +227 -87
  45. package/server/services/storage/queryHistoryChartUtils.js +19 -2
  46. package/server/utils/appPaths.js +55 -19
  47. package/server/utils/fileValidation.js +94 -8
  48. package/tailwind.config.cjs +73 -0
  49. package/tests/security-paths.test.js +84 -0
  50. package/tests/sql-identifier-safety.test.js +66 -0
  51. package/.npmingnore +0 -4
  52. package/changelog.md +0 -77
  53. package/docs/DESIGN_GUIDELINES.md +0 -36
  54. package/scripts/publish_brew.sh +0 -466
  55. package/scripts/publish_npm.sh +0 -241
  56. package/shortkeys.md +0 -5
@@ -90,7 +90,7 @@
90
90
  }
91
91
 
92
92
  .landing-accent {
93
- background: rgba(75, 71, 50, 0.2);
93
+ background: rgb(var(--rgb-outline) / 0.2);
94
94
  height: 1px;
95
95
  position: absolute;
96
96
  }
@@ -110,8 +110,8 @@
110
110
  }
111
111
 
112
112
  .landing-accent--c {
113
- border-bottom: 1px solid rgba(75, 71, 50, 0.12);
114
- border-right: 1px solid rgba(75, 71, 50, 0.12);
113
+ border-bottom: 1px solid var(--border-subtle);
114
+ border-right: 1px solid var(--border-subtle);
115
115
  height: 4rem;
116
116
  left: 0;
117
117
  top: 50%;
@@ -126,7 +126,7 @@
126
126
 
127
127
  .system-placeholder-card {
128
128
  background: var(--color-surface-low);
129
- border-bottom: 2px solid rgba(49, 48, 48, 1);
129
+ box-shadow: inset 0 -2px 0 rgb(49 48 48);
130
130
  display: flex;
131
131
  flex-direction: column;
132
132
  gap: var(--spacing-2);
@@ -148,16 +148,16 @@
148
148
 
149
149
  .charts-view__sidebar {
150
150
  background: var(--color-surface-low);
151
- border-right: 1px solid rgba(75, 71, 50, 0.18);
151
+ border-right: 1px solid var(--border-medium);
152
152
  display: flex;
153
- flex: 0 0 23rem;
153
+ flex: 0 0 clamp(20rem, 28vw, 23rem);
154
154
  flex-direction: column;
155
155
  min-height: 0;
156
- width: 23rem;
156
+ width: clamp(20rem, 28vw, 23rem);
157
157
  }
158
158
 
159
159
  .charts-view__sidebar-header {
160
- border-bottom: 1px solid rgba(75, 71, 50, 0.18);
160
+ border-bottom: 1px solid var(--border-medium);
161
161
  padding: var(--spacing-5);
162
162
  }
163
163
 
@@ -171,16 +171,16 @@
171
171
 
172
172
  .structure-view__sidebar {
173
173
  background: var(--color-surface-low);
174
- border-right: 1px solid rgba(75, 71, 50, 0.18);
174
+ border-right: 1px solid var(--border-medium);
175
175
  display: flex;
176
- flex: 0 0 18.5rem;
176
+ flex: 0 0 clamp(16rem, 22vw, 18.5rem);
177
177
  flex-direction: column;
178
178
  min-height: 0;
179
- width: 18.5rem;
179
+ width: clamp(16rem, 22vw, 18.5rem);
180
180
  }
181
181
 
182
182
  .structure-view__sidebar-header {
183
- border-bottom: 1px solid rgba(75, 71, 50, 0.18);
183
+ border-bottom: 1px solid var(--border-medium);
184
184
  padding: var(--spacing-5);
185
185
  }
186
186
 
@@ -200,7 +200,7 @@
200
200
  }
201
201
 
202
202
  .structure-view__header {
203
- border-bottom: 1px solid rgba(75, 71, 50, 0.18);
203
+ border-bottom: 1px solid var(--border-medium);
204
204
  padding: 1.25rem 1.5rem;
205
205
  }
206
206
 
@@ -262,7 +262,7 @@
262
262
 
263
263
  .charts-height-toggle {
264
264
  background: var(--color-surface-container-low);
265
- border: 1px solid rgba(75, 71, 50, 0.18);
265
+ border: 1px solid var(--border-medium);
266
266
  display: inline-flex;
267
267
  gap: 0;
268
268
  height: var(--control-height);
@@ -287,7 +287,7 @@
287
287
  }
288
288
 
289
289
  .charts-height-toggle__button + .charts-height-toggle__button {
290
- border-left: 1px solid rgba(75, 71, 50, 0.18);
290
+ border-left: 1px solid var(--border-medium);
291
291
  }
292
292
 
293
293
  .charts-height-toggle__button.is-active {
@@ -299,7 +299,7 @@
299
299
  .query-chart-card {
300
300
  --query-chart-height: 450px;
301
301
  background: var(--color-surface-low);
302
- border: 1px solid rgba(75, 71, 50, 0.12);
302
+ border: 1px solid var(--border-subtle);
303
303
  overflow: hidden;
304
304
  }
305
305
 
@@ -327,7 +327,7 @@
327
327
 
328
328
  .query-chart-card__body {
329
329
  background:
330
- radial-gradient(circle at top right, rgba(252, 227, 0, 0.08), transparent 28%),
330
+ radial-gradient(circle at top right, var(--primary-alpha-08), transparent 28%),
331
331
  linear-gradient(180deg, rgba(32, 31, 31, 0.92), rgba(14, 14, 14, 1));
332
332
  min-height: var(--query-chart-height);
333
333
  padding: 1rem;
@@ -360,25 +360,6 @@
360
360
  padding-top: var(--spacing-4);
361
361
  }
362
362
 
363
- .media-tagging-created-tags-shell {
364
- display: flex;
365
- flex: 1;
366
- flex-direction: column;
367
- min-height: 0;
368
- }
369
-
370
- .media-tagging-created-tags {
371
- flex: 1;
372
- max-height: none;
373
- min-height: 0;
374
- }
375
-
376
- .media-tagging-mapping-list {
377
- flex: 0 0 auto;
378
- min-height: auto;
379
- overflow-y: auto;
380
- }
381
-
382
363
  .media-tagging-shell {
383
364
  display: flex;
384
365
  flex: 1;
@@ -416,7 +397,7 @@
416
397
 
417
398
  .media-tagging-card__header {
418
399
  align-items: start;
419
- border-bottom: 1px solid rgba(75, 71, 50, 0.14);
400
+ border-bottom: 1px solid rgb(var(--rgb-outline) / 0.14);
420
401
  display: flex;
421
402
  flex-wrap: wrap;
422
403
  gap: var(--spacing-4);
@@ -501,18 +482,23 @@
501
482
  }
502
483
 
503
484
  .media-tagging-field__hint {
504
- color: rgba(229, 226, 225, 0.45);
485
+ color: var(--text-subtle);
505
486
  font-family: var(--font-family-mono);
506
487
  font-size: 0.65rem;
507
488
  letter-spacing: 0.06em;
508
489
  text-transform: uppercase;
509
490
  }
510
491
 
511
- .media-tagging-static-field {
492
+ .media-tagging-static-field,
493
+ .media-tagging-card .control-input,
494
+ .media-tagging-card .control-select {
512
495
  align-items: center;
513
- background: var(--color-surface-container-lowest);
514
- border: 1px solid rgba(75, 71, 50, 0.18);
496
+ background: var(--color-surface-container);
497
+ border: 1px solid var(--border-medium);
515
498
  color: var(--color-on-surface);
499
+ }
500
+
501
+ .media-tagging-static-field {
516
502
  display: flex;
517
503
  font-family: var(--font-family-mono);
518
504
  font-size: 0.8rem;
@@ -521,9 +507,12 @@
521
507
  }
522
508
 
523
509
  .media-tagging-textarea {
524
- background: var(--color-surface-container-lowest);
525
- border: 1px solid rgba(75, 71, 50, 0.18);
510
+ background: var(--color-surface-container);
511
+ border: 1px solid var(--border-medium);
526
512
  color: var(--color-on-surface);
513
+ }
514
+
515
+ .media-tagging-textarea {
527
516
  font-family: var(--font-family-mono);
528
517
  font-size: 0.8rem;
529
518
  min-height: 13rem;
@@ -531,9 +520,16 @@
531
520
  resize: vertical;
532
521
  }
533
522
 
523
+ .media-tagging-textarea:focus-visible,
524
+ .media-tagging-static-field:focus-within {
525
+ border-color: var(--primary-alpha-35);
526
+ box-shadow: var(--focus-ring-inset);
527
+ outline: none;
528
+ }
529
+
534
530
  .sql-highlight-shell {
535
531
  background: var(--color-surface-container-lowest);
536
- border: 1px solid rgba(75, 71, 50, 0.18);
532
+ border: 1px solid var(--border-medium);
537
533
  min-height: 13rem;
538
534
  overflow: hidden;
539
535
  }
@@ -582,7 +578,7 @@
582
578
  }
583
579
 
584
580
  .media-tagging-status__label {
585
- color: rgba(229, 226, 225, 0.5);
581
+ color: rgb(var(--rgb-on-surface) / 0.5);
586
582
  font-family: var(--font-family-mono);
587
583
  font-size: 0.625rem;
588
584
  letter-spacing: 0.14em;
@@ -605,18 +601,21 @@
605
601
 
606
602
  .media-tagging-mapping-list {
607
603
  display: grid;
604
+ flex: 0 0 auto;
608
605
  gap: var(--spacing-3);
609
606
  max-height: 18rem;
607
+ min-height: auto;
608
+ overflow-y: auto;
610
609
  }
611
610
 
612
611
  .media-tagging-mapping-card {
613
- background: var(--color-surface-container-lowest);
614
- border: 1px solid rgba(75, 71, 50, 0.16);
612
+ background: var(--color-surface-container);
613
+ border: 1px solid rgb(var(--rgb-outline) / 0.16);
615
614
  padding: var(--spacing-4);
616
615
  }
617
616
 
618
617
  .media-tagging-mapping-card.is-selected {
619
- border-color: rgba(252, 227, 0, 0.45);
618
+ border-color: rgb(var(--rgb-primary) / 0.45);
620
619
  }
621
620
 
622
621
  .media-tagging-preview {
@@ -637,9 +636,9 @@
637
636
  .media-tagging-preview__media {
638
637
  align-items: stretch;
639
638
  background:
640
- radial-gradient(circle at top right, rgba(252, 227, 0, 0.1), transparent 30%),
639
+ radial-gradient(circle at top right, var(--primary-alpha-10), transparent 30%),
641
640
  linear-gradient(180deg, rgba(32, 31, 31, 0.95), rgba(14, 14, 14, 1));
642
- border: 1px solid rgba(75, 71, 50, 0.16);
641
+ border: 1px solid rgb(var(--rgb-outline) / 0.16);
643
642
  display: flex;
644
643
  flex-direction: column;
645
644
  justify-content: flex-start;
@@ -659,7 +658,7 @@
659
658
 
660
659
  .media-tagging-preview__media-toolbar {
661
660
  align-items: center;
662
- border-bottom: 1px solid rgba(75, 71, 50, 0.18);
661
+ border-bottom: 1px solid var(--border-medium);
663
662
  display: flex;
664
663
  flex: 0 0 auto;
665
664
  flex-wrap: wrap;
@@ -728,8 +727,8 @@
728
727
  align-self: center;
729
728
  background:
730
729
  linear-gradient(180deg, rgba(32, 31, 31, 0.96), rgba(14, 14, 14, 0.98)),
731
- radial-gradient(circle at top left, rgba(252, 227, 0, 0.1), transparent 35%);
732
- border: 1px solid rgba(75, 71, 50, 0.2);
730
+ radial-gradient(circle at top left, var(--primary-alpha-10), transparent 35%);
731
+ border: 1px solid rgb(var(--rgb-outline) / 0.2);
733
732
  box-shadow: var(--shadow-subtle-glow);
734
733
  display: grid;
735
734
  gap: var(--spacing-5);
@@ -742,8 +741,8 @@
742
741
 
743
742
  .media-tagging-audio-preview__icon {
744
743
  align-items: center;
745
- background: rgba(252, 227, 0, 0.08);
746
- border: 1px solid rgba(252, 227, 0, 0.22);
744
+ background: var(--primary-alpha-08);
745
+ border: 1px solid var(--primary-alpha-22);
747
746
  color: var(--color-primary-container);
748
747
  display: flex;
749
748
  height: 5.5rem;
@@ -763,7 +762,7 @@
763
762
  }
764
763
 
765
764
  .media-tagging-audio-preview__eyebrow {
766
- color: rgba(229, 226, 225, 0.48);
765
+ color: rgb(var(--rgb-on-surface) / 0.48);
767
766
  font-family: var(--font-family-mono);
768
767
  font-size: 0.625rem;
769
768
  font-weight: 700;
@@ -789,7 +788,7 @@
789
788
  .media-tagging-preview__placeholder,
790
789
  .media-tagging-preview__empty {
791
790
  align-items: center;
792
- color: rgba(229, 226, 225, 0.4);
791
+ color: rgb(var(--rgb-on-surface) / 0.4);
793
792
  display: flex;
794
793
  flex: 1;
795
794
  flex-direction: column;
@@ -799,7 +798,7 @@
799
798
 
800
799
  .media-tagging-preview__meta {
801
800
  background: var(--color-surface-container-lowest);
802
- border: 1px solid rgba(75, 71, 50, 0.16);
801
+ border: 1px solid rgb(var(--rgb-outline) / 0.16);
803
802
  display: flex;
804
803
  flex-direction: column;
805
804
  min-height: 0;
@@ -816,7 +815,7 @@
816
815
  }
817
816
 
818
817
  .media-tagging-preview__eyebrow {
819
- color: rgba(229, 226, 225, 0.48);
818
+ color: rgb(var(--rgb-on-surface) / 0.48);
820
819
  font-family: var(--font-family-mono);
821
820
  font-size: 0.625rem;
822
821
  font-weight: 700;
@@ -836,7 +835,7 @@
836
835
  }
837
836
 
838
837
  .media-tagging-preview__metadata {
839
- border-top: 1px solid rgba(75, 71, 50, 0.14);
838
+ border-top: 1px solid rgb(var(--rgb-outline) / 0.14);
840
839
  display: flex;
841
840
  flex: 1;
842
841
  flex-direction: column;
@@ -855,7 +854,7 @@
855
854
  }
856
855
 
857
856
  .media-tagging-preview__metadata-row span:first-child {
858
- color: rgba(229, 226, 225, 0.5);
857
+ color: rgb(var(--rgb-on-surface) / 0.5);
859
858
  font-family: var(--font-family-mono);
860
859
  font-size: 0.65rem;
861
860
  letter-spacing: 0.08em;
@@ -879,8 +878,8 @@
879
878
  }
880
879
 
881
880
  .media-tagging-tag-panel {
882
- background: var(--color-surface-container-lowest);
883
- border: 1px solid rgba(75, 71, 50, 0.16);
881
+ background: var(--color-surface-container);
882
+ border: 1px solid rgb(var(--rgb-outline) / 0.16);
884
883
  display: flex;
885
884
  flex: 1;
886
885
  flex-direction: column;
@@ -894,7 +893,7 @@
894
893
  }
895
894
 
896
895
  .media-tagging-tag-panel__header {
897
- border-bottom: 1px solid rgba(75, 71, 50, 0.14);
896
+ border-bottom: 1px solid rgb(var(--rgb-outline) / 0.14);
898
897
  }
899
898
 
900
899
  .media-tagging-tag-panel__header-row {
@@ -905,7 +904,7 @@
905
904
  }
906
905
 
907
906
  .media-tagging-tag-panel__footer {
908
- border-top: 1px solid rgba(75, 71, 50, 0.14);
907
+ border-top: 1px solid rgb(var(--rgb-outline) / 0.14);
909
908
  display: flex;
910
909
  flex-direction: column;
911
910
  gap: var(--spacing-3);
@@ -919,7 +918,7 @@
919
918
  }
920
919
 
921
920
  .media-tagging-tag-panel__remaining {
922
- color: rgba(205, 199, 171, 0.45);
921
+ color: var(--text-variant-subtle);
923
922
  flex: 1 1 auto;
924
923
  font-family: var(--font-family-mono);
925
924
  font-size: 0.6875rem;
@@ -949,10 +948,14 @@
949
948
  }
950
949
 
951
950
  .media-tagging-created-tags-shell {
952
- border-top: 1px solid rgba(75, 71, 50, 0.14);
951
+ border-top: 1px solid rgb(var(--rgb-outline) / 0.14);
952
+ display: flex;
953
+ flex: 1;
954
+ flex-direction: column;
953
955
  margin-top: var(--spacing-2);
954
- padding-top: var(--spacing-4);
956
+ min-height: 0;
955
957
  overflow: hidden;
958
+ padding-top: var(--spacing-4);
956
959
  }
957
960
 
958
961
  .media-tagging-created-tags-frame {
@@ -963,8 +966,8 @@
963
966
  }
964
967
 
965
968
  .media-tagging-created-tags {
966
- background: var(--color-surface-container-lowest);
967
- border: 1px solid rgba(75, 71, 50, 0.16);
969
+ background: var(--color-surface-container);
970
+ border: 1px solid rgb(var(--rgb-outline) / 0.16);
968
971
  display: flex;
969
972
  flex: 1;
970
973
  flex-direction: column;
@@ -977,8 +980,8 @@
977
980
 
978
981
  .media-tagging-created-tag {
979
982
  align-items: center;
980
- background: rgba(252, 227, 0, 0.05);
981
- border: 1px solid rgba(252, 227, 0, 0.12);
983
+ background: var(--primary-alpha-08);
984
+ border: 1px solid var(--primary-alpha-12);
982
985
  display: flex;
983
986
  gap: var(--spacing-3);
984
987
  justify-content: space-between;
@@ -1007,8 +1010,8 @@
1007
1010
  .media-tagging-created-tag__badge,
1008
1011
  .media-tagging-tag-option__badge {
1009
1012
  align-items: center;
1010
- background: rgba(252, 227, 0, 0.1);
1011
- border: 1px solid rgba(252, 227, 0, 0.18);
1013
+ background: var(--primary-alpha-10);
1014
+ border: 1px solid var(--primary-alpha-18);
1012
1015
  color: var(--color-primary-container);
1013
1016
  display: inline-flex;
1014
1017
  font-family: var(--font-family-mono);
@@ -1033,8 +1036,8 @@
1033
1036
 
1034
1037
  .media-tagging-tag-option {
1035
1038
  align-items: start;
1036
- background: rgba(252, 227, 0, 0.03);
1037
- border: 1px solid rgba(75, 71, 50, 0.18);
1039
+ background: var(--primary-alpha-05);
1040
+ border: 1px solid var(--border-medium);
1038
1041
  border-radius: 0.25rem;
1039
1042
  cursor: pointer;
1040
1043
  display: grid;
@@ -1049,15 +1052,20 @@
1049
1052
  }
1050
1053
 
1051
1054
  .media-tagging-tag-option:hover {
1052
- border-color: rgba(252, 227, 0, 0.28);
1053
- background: rgba(252, 227, 0, 0.05);
1055
+ border-color: rgb(var(--rgb-primary) / 0.28);
1056
+ background: var(--primary-alpha-08);
1054
1057
  transform: translateY(-1px);
1055
1058
  }
1056
1059
 
1060
+ .media-tagging-tag-option:focus-within {
1061
+ border-color: var(--primary-alpha-35);
1062
+ box-shadow: var(--focus-ring-inset);
1063
+ }
1064
+
1057
1065
  .media-tagging-tag-option.is-selected {
1058
- background: rgba(252, 227, 0, 0.09);
1059
- border-color: rgba(252, 227, 0, 0.34);
1060
- box-shadow: inset 0 0 0 1px rgba(252, 227, 0, 0.14);
1066
+ background: rgb(var(--rgb-primary) / 0.09);
1067
+ border-color: rgb(var(--rgb-primary) / 0.34);
1068
+ box-shadow: inset 0 0 0 1px rgb(var(--rgb-primary) / 0.14);
1061
1069
  }
1062
1070
 
1063
1071
  .media-tagging-tag-option__checkbox {
@@ -1097,8 +1105,8 @@
1097
1105
  }
1098
1106
 
1099
1107
  .media-tagging-issue {
1100
- background: rgba(252, 227, 0, 0.06);
1101
- border: 1px solid rgba(252, 227, 0, 0.15);
1108
+ background: var(--primary-alpha-06);
1109
+ border: 1px solid var(--primary-alpha-15);
1102
1110
  color: var(--color-on-surface);
1103
1111
  font-size: 0.875rem;
1104
1112
  line-height: 1.65;
@@ -1130,7 +1138,7 @@
1130
1138
  align-items: center;
1131
1139
  background: transparent;
1132
1140
  border: 0;
1133
- color: rgba(229, 226, 225, 0.45);
1141
+ color: var(--text-subtle);
1134
1142
  cursor: pointer;
1135
1143
  display: inline-flex;
1136
1144
  font-size: 1rem;
@@ -1155,8 +1163,8 @@
1155
1163
  }
1156
1164
 
1157
1165
  .media-tagging-inline-warning {
1158
- background: rgba(252, 227, 0, 0.08);
1159
- border: 1px solid rgba(252, 227, 0, 0.2);
1166
+ background: var(--primary-alpha-08);
1167
+ border: 1px solid var(--primary-alpha-20);
1160
1168
  color: var(--color-on-surface);
1161
1169
  font-size: 0.85rem;
1162
1170
  line-height: 1.6;
package/package.json CHANGED
@@ -1,22 +1,35 @@
1
1
  {
2
2
  "name": "sqlite-hub",
3
- "version": "0.9.1",
3
+ "version": "0.9.4",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "main": "server/server.js",
6
6
  "bin": {
7
7
  "sqlite-hub": "bin/sqlite-hub.js"
8
8
  },
9
9
  "scripts": {
10
+ "build": "npm run build:css",
11
+ "build:css": "tailwindcss -c tailwind.config.cjs -i ./frontend/styles/tailwind.css -o ./frontend/styles/tailwind.generated.css --minify",
12
+ "audit": "npm audit --audit-level=high",
13
+ "test": "node --test tests/**/*.test.js",
10
14
  "start": "node bin/sqlite-hub.js",
11
15
  "dev": "node --watch server/server.js --port:4180",
16
+ "prepack": "npm run build:css",
12
17
  "publish": "bash scripts/publish_brew.sh && sleep 10 && npm login --auth-type=web && bash scripts/publish_npm.sh"
13
18
  },
14
19
  "dependencies": {
15
- "better-sqlite3": "^11.8.1",
20
+ "better-sqlite3": "^12.10.0",
16
21
  "cytoscape": "^3.33.2",
17
22
  "cytoscape-elk": "^2.3.0",
18
23
  "echarts": "^5.6.0",
19
24
  "elkjs": "^0.11.1",
20
- "express": "^4.21.2"
25
+ "express": "^4.21.2",
26
+ "express-rate-limit": "^8.5.1",
27
+ "helmet": "^8.1.0",
28
+ "material-symbols": "^0.44.6"
29
+ },
30
+ "devDependencies": {
31
+ "@tailwindcss/container-queries": "^0.1.1",
32
+ "@tailwindcss/forms": "^0.5.10",
33
+ "tailwindcss": "^3.4.17"
21
34
  }
22
35
  }
@@ -1,6 +1,5 @@
1
- const fs = require("node:fs");
2
1
  const express = require("express");
3
- const { NotFoundError, route, successResponse } = require("../utils/errors");
2
+ const { route, successResponse } = require("../utils/errors");
4
3
 
5
4
  function createMediaTaggingRouter({ mediaTaggingService }) {
6
5
  const router = express.Router();
@@ -147,14 +146,7 @@ function createMediaTaggingRouter({ mediaTaggingService }) {
147
146
  router.get(
148
147
  "/media-file",
149
148
  route((req, res) => {
150
- const rawPath = String(req.query.path ?? "").trim();
151
- const resolvedPath = mediaTaggingService.resolveMediaFilePath(rawPath);
152
-
153
- if (!resolvedPath || !fs.existsSync(resolvedPath)) {
154
- throw new NotFoundError(`Media file not found: ${rawPath}`);
155
- }
156
-
157
- res.sendFile(resolvedPath);
149
+ mediaTaggingService.sendMediaFile(req.query.path, res);
158
150
  })
159
151
  );
160
152
 
@@ -1,5 +1,5 @@
1
1
  const express = require("express");
2
- const { route, successResponse } = require("../utils/errors");
2
+ const { DatabaseRequiredError, route, successResponse } = require("../utils/errors");
3
3
 
4
4
  function parseBooleanFlag(value) {
5
5
  if (typeof value === "boolean") {
@@ -37,6 +37,16 @@ function getActiveDatabaseKey(connectionManager) {
37
37
  return connectionManager.getActiveConnection()?.id ?? null;
38
38
  }
39
39
 
40
+ function requireActiveDatabaseKey(connectionManager) {
41
+ const databaseKey = getActiveDatabaseKey(connectionManager);
42
+
43
+ if (!databaseKey) {
44
+ throw new DatabaseRequiredError();
45
+ }
46
+
47
+ return databaseKey;
48
+ }
49
+
40
50
  function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
41
51
  const router = express.Router();
42
52
 
@@ -57,7 +67,7 @@ function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
57
67
  router.get(
58
68
  "/history",
59
69
  route((req, res) => {
60
- const databaseKey = getActiveDatabaseKey(connectionManager);
70
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
61
71
  const tab = String(req.query.tab ?? "recent").trim().toLowerCase();
62
72
  const options = {
63
73
  databaseKey,
@@ -92,7 +102,7 @@ function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
92
102
  router.delete(
93
103
  "/history",
94
104
  route((req, res) => {
95
- const databaseKey = getActiveDatabaseKey(connectionManager);
105
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
96
106
  const deletedCount = appStateStore.clearQueryHistoryForDatabase(databaseKey);
97
107
 
98
108
  res.json(
@@ -114,12 +124,15 @@ function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
114
124
  router.get(
115
125
  "/history/:historyId/runs",
116
126
  route((req, res) => {
127
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
117
128
  res.json(
118
129
  successResponse({
119
130
  data: appStateStore.getQueryRunsByHistoryId(
120
131
  req.params.historyId,
121
- parseListLimit(req.query.limit, 8, 50)
132
+ parseListLimit(req.query.limit, 8, 50),
133
+ databaseKey
122
134
  ),
135
+ metadata: { databaseKey },
123
136
  })
124
137
  );
125
138
  })
@@ -129,10 +142,12 @@ function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
129
142
  "/history/:historyId/favorite",
130
143
  route((req, res) => {
131
144
  const nextValue = parseBooleanFlag(req.body?.value);
145
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
132
146
  res.json(
133
147
  successResponse({
134
148
  message: nextValue ? "Query favorited." : "Query removed from favorites.",
135
- data: appStateStore.toggleFavorite(req.params.historyId, nextValue),
149
+ data: appStateStore.toggleFavorite(req.params.historyId, nextValue, databaseKey),
150
+ metadata: { databaseKey },
136
151
  })
137
152
  );
138
153
  })
@@ -142,10 +157,12 @@ function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
142
157
  "/history/:historyId/saved",
143
158
  route((req, res) => {
144
159
  const nextValue = parseBooleanFlag(req.body?.value);
160
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
145
161
  res.json(
146
162
  successResponse({
147
163
  message: nextValue ? "Query saved." : "Query removed from saved queries.",
148
- data: appStateStore.toggleSaved(req.params.historyId, nextValue),
164
+ data: appStateStore.toggleSaved(req.params.historyId, nextValue, databaseKey),
165
+ metadata: { databaseKey },
149
166
  })
150
167
  );
151
168
  })
@@ -154,10 +171,12 @@ function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
154
171
  router.patch(
155
172
  "/history/:historyId/title",
156
173
  route((req, res) => {
174
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
157
175
  res.json(
158
176
  successResponse({
159
177
  message: "Query title updated.",
160
- data: appStateStore.renameQuery(req.params.historyId, req.body?.title),
178
+ data: appStateStore.renameQuery(req.params.historyId, req.body?.title, databaseKey),
179
+ metadata: { databaseKey },
161
180
  })
162
181
  );
163
182
  })
@@ -166,10 +185,12 @@ function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
166
185
  router.patch(
167
186
  "/history/:historyId/notes",
168
187
  route((req, res) => {
188
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
169
189
  res.json(
170
190
  successResponse({
171
191
  message: "Query notes updated.",
172
- data: appStateStore.updateQueryNotes(req.params.historyId, req.body?.notes),
192
+ data: appStateStore.updateQueryNotes(req.params.historyId, req.body?.notes, databaseKey),
193
+ metadata: { databaseKey },
173
194
  })
174
195
  );
175
196
  })
@@ -178,13 +199,15 @@ function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
178
199
  router.delete(
179
200
  "/history/:historyId",
180
201
  route((req, res) => {
181
- appStateStore.deleteQueryHistoryItem(req.params.historyId);
202
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
203
+ appStateStore.deleteQueryHistoryItem(req.params.historyId, databaseKey);
182
204
  res.json(
183
205
  successResponse({
184
206
  message: "Query history item deleted.",
185
207
  data: {
186
208
  id: Number(req.params.historyId),
187
209
  },
210
+ metadata: { databaseKey },
188
211
  })
189
212
  );
190
213
  })
@@ -193,9 +216,11 @@ function createSqlRouter({ appStateStore, connectionManager, sqlExecutor }) {
193
216
  router.get(
194
217
  "/history/:historyId",
195
218
  route((req, res) => {
219
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
196
220
  res.json(
197
221
  successResponse({
198
- data: appStateStore.getQueryHistoryItemById(req.params.historyId),
222
+ data: appStateStore.getQueryHistoryItemById(req.params.historyId, databaseKey),
223
+ metadata: { databaseKey },
199
224
  })
200
225
  );
201
226
  })