sqlite-hub 0.9.3 → 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 (46) 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 +2 -158
  5. package/frontend/js/app.js +5 -1
  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 +145 -195
  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 +81 -99
  13. package/frontend/js/components/queryResults.js +3 -1
  14. package/frontend/js/components/rowEditorPanel.js +28 -31
  15. package/frontend/js/components/tableDesignerEditor.js +91 -116
  16. package/frontend/js/store.js +39 -3
  17. package/frontend/js/utils/tableDesigner.js +8 -2
  18. package/frontend/js/views/charts.js +23 -43
  19. package/frontend/js/views/data.js +116 -132
  20. package/frontend/js/views/mediaTagging.js +131 -164
  21. package/frontend/js/views/structure.js +52 -48
  22. package/frontend/styles/tailwind.css +81 -0
  23. package/frontend/styles/tailwind.generated.css +1 -0
  24. package/frontend/styles/tokens.css +3 -3
  25. package/package.json +16 -3
  26. package/server/routes/mediaTagging.js +2 -10
  27. package/server/routes/sql.js +35 -10
  28. package/server/server.js +24 -0
  29. package/server/services/sqlite/dataBrowserService.js +25 -5
  30. package/server/services/sqlite/exportService.js +4 -2
  31. package/server/services/sqlite/introspection.js +2 -2
  32. package/server/services/sqlite/mediaTaggingService.js +166 -53
  33. package/server/services/sqlite/structureService.js +2 -2
  34. package/server/services/sqlite/tableDesigner/sql.js +19 -3
  35. package/server/services/storage/appStateStore.js +227 -87
  36. package/server/utils/appPaths.js +55 -19
  37. package/server/utils/fileValidation.js +94 -8
  38. package/tailwind.config.cjs +73 -0
  39. package/tests/security-paths.test.js +84 -0
  40. package/tests/sql-identifier-safety.test.js +66 -0
  41. package/.npmingnore +0 -4
  42. package/changelog.md +0 -84
  43. package/docs/DESIGN_GUIDELINES.md +0 -36
  44. package/scripts/publish_brew.sh +0 -466
  45. package/scripts/publish_npm.sh +0 -241
  46. package/shortkeys.md +0 -5
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build-test-audit:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - name: Checkout
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Setup Node
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 26
24
+ cache: npm
25
+
26
+ - name: Install dependencies
27
+ run: npm ci
28
+
29
+ - name: Build
30
+ run: npm run build
31
+
32
+ - name: Test
33
+ run: npm test
34
+
35
+ - name: Dependency audit
36
+ run: npm run audit
package/README.md CHANGED
@@ -66,13 +66,13 @@ brew install sqlite-hub
66
66
  ### NPM
67
67
 
68
68
  ```bash
69
- npm install -g sqlit-hub
69
+ npm install -g sqlite-hub
70
70
  ```
71
71
 
72
72
  ## alternative port
73
73
 
74
74
  ```bash
75
- sqlit-hub --port:4174
75
+ sqlite-hub --port:4174
76
76
  ```
77
77
 
78
78
  ## CLI Interface
package/bin/sqlite-hub.js CHANGED
@@ -334,7 +334,7 @@ async function main() {
334
334
  } else if (stmt.kind === 'mutation') {
335
335
  console.log(`Changes: ${stmt.changes}`);
336
336
  if (stmt.lastInsertRowid) {
337
- console.log(`Last insert rowid: ${stmt.lastInsertRowid}`);
337
+ console.log('Last insert rowid:', stmt.lastInsertRowid);
338
338
  }
339
339
  }
340
340
  });
@@ -6,164 +6,8 @@
6
6
  <title>SQLite Hub</title>
7
7
  <link href="/assets/images/logo_raw.png" rel="icon" type="image/png" />
8
8
  <link href="/assets/images/logo_raw.png" rel="shortcut icon" type="image/png" />
9
- <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
10
- <link
11
- href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800;900&family=Inter:wght@300;400;500;600;700;800&family=Roboto+Mono:wght@400;500;700&display=swap"
12
- rel="stylesheet"
13
- />
14
- <link
15
- href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
16
- rel="stylesheet"
17
- />
18
- <script id="tailwind-config">
19
- tailwind.config = {
20
- darkMode: "class",
21
- theme: {
22
- extend: {
23
- colors: {
24
- "on-secondary-fixed": "#1c1b1b",
25
- background: "rgb(var(--rgb-background) / <alpha-value>)",
26
- "inverse-primary": "#6a5f00",
27
- "primary-fixed": "#fde403",
28
- tertiary: "#fbfffe",
29
- outline: "#979177",
30
- "on-secondary": "#313030",
31
- "error-container": "#93000a",
32
- "on-primary-fixed-variant": "#504700",
33
- "on-tertiary-container": "#006f72",
34
- "on-tertiary-fixed-variant": "#004f51",
35
- "secondary-fixed-dim": "#c8c6c5",
36
- "tertiary-fixed-dim": "#00dce1",
37
- "on-background": "rgb(var(--rgb-on-surface) / <alpha-value>)",
38
- "surface-tint": "#dec800",
39
- "primary-fixed-dim": "#dec800",
40
- "on-surface": "rgb(var(--rgb-on-surface) / <alpha-value>)",
41
- "surface-container-low": "rgb(var(--rgb-surface-low) / <alpha-value>)",
42
- "on-tertiary": "#003738",
43
- "surface-container": "rgb(var(--rgb-surface-container) / <alpha-value>)",
44
- "on-surface-variant": "rgb(var(--rgb-on-surface-variant) / <alpha-value>)",
45
- "on-primary-container": "#706400",
46
- "on-error": "#690005",
47
- "surface-container-high": "rgb(var(--rgb-surface-high) / <alpha-value>)",
48
- "surface-container-highest": "rgb(var(--rgb-surface-highest) / <alpha-value>)",
49
- error: "rgb(var(--rgb-error) / <alpha-value>)",
50
- primary: "#fffeff",
51
- "outline-variant": "rgb(var(--rgb-outline) / <alpha-value>)",
52
- "on-error-container": "#ffdad6",
53
- "on-secondary-fixed-variant": "#474746",
54
- "surface-container-lowest": "rgb(var(--rgb-surface-lowest) / <alpha-value>)",
55
- "inverse-surface": "#e5e2e1",
56
- surface: "rgb(var(--rgb-surface) / <alpha-value>)",
57
- "tertiary-container": "#04faff",
58
- "on-tertiary-fixed": "#002021",
59
- "tertiary-fixed": "#2dfaff",
60
- "surface-variant": "#353534",
61
- "on-primary": "#373100",
62
- "primary-container": "rgb(var(--rgb-primary) / <alpha-value>)",
63
- "inverse-on-surface": "#313030",
64
- secondary: "#c8c6c5",
65
- "on-secondary-container": "#b7b5b4",
66
- "on-primary-fixed": "#201c00",
67
- "secondary-container": "#474746",
68
- "surface-dim": "rgb(var(--rgb-surface) / <alpha-value>)",
69
- "surface-bright": "rgb(var(--rgb-surface-bright) / <alpha-value>)",
70
- "secondary-fixed": "#e5e2e1",
71
- },
72
- fontFamily: {
73
- headline: ["Space Grotesk"],
74
- body: ["Inter"],
75
- label: ["Inter"],
76
- mono: ["Roboto Mono"],
77
- },
78
- borderRadius: {
79
- DEFAULT: "0px",
80
- lg: "0px",
81
- xl: "0px",
82
- full: "9999px",
83
- },
84
- },
85
- },
86
- };
87
- </script>
88
- <style type="text/tailwindcss">
89
- @layer components {
90
- .signature-button {
91
- @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-primary-container bg-primary-container px-[var(--control-padding-inline)] font-headline text-xs font-bold uppercase tracking-[0.16em] text-on-primary transition-all;
92
- box-shadow: 0 0 18px -10px rgb(var(--rgb-primary) / 0.7);
93
- clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
94
- }
95
-
96
- .signature-button:hover {
97
- @apply -translate-y-px bg-primary-fixed text-on-primary;
98
- border-color: rgb(var(--rgb-primary) / 0.55);
99
- box-shadow: 0 0 24px -8px rgb(var(--rgb-primary) / 0.92);
100
- filter: brightness(1.03);
101
- }
102
-
103
- .signature-button:disabled,
104
- .signature-button[aria-disabled="true"] {
105
- @apply cursor-not-allowed opacity-45;
106
- box-shadow: none;
107
- filter: none;
108
- transform: none;
109
- }
110
-
111
- .signature-button .material-symbols-outlined {
112
- @apply text-base;
113
- }
114
-
115
- .standard-button {
116
- @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-outline-variant/20 bg-surface-container px-[var(--control-padding-inline)] font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface transition-colors;
117
- }
118
-
119
- .standard-button:hover {
120
- @apply bg-surface-container-highest text-primary-container;
121
- border-color: var(--primary-alpha-24);
122
- }
123
-
124
- .standard-button.is-active {
125
- @apply bg-surface-container-high text-primary-container;
126
- border-color: var(--primary-alpha-30);
127
- }
128
-
129
- .standard-button:disabled,
130
- .standard-button[aria-disabled="true"] {
131
- @apply cursor-not-allowed opacity-40;
132
- }
133
-
134
- .standard-button .material-symbols-outlined {
135
- @apply text-base;
136
- }
137
-
138
- .standard-checkbox {
139
- @apply inline-flex w-full items-center gap-3 min-h-[var(--control-height)] border border-outline-variant/10 bg-surface-container-lowest px-[var(--control-padding-inline)] text-sm text-on-surface;
140
- }
141
-
142
- .standard-checkbox input[type="checkbox"] {
143
- @apply m-0 rounded-none border-outline bg-surface-container-lowest text-primary-container focus:ring-primary-container;
144
- accent-color: var(--color-primary-container);
145
- }
146
-
147
- .standard-checkbox.is-disabled,
148
- .standard-checkbox:has(input:disabled) {
149
- @apply opacity-45;
150
- }
151
-
152
- .delete-button {
153
- @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-error/25 bg-error-container/10 px-[var(--control-padding-inline)] font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-error transition-all;
154
- }
155
-
156
- .delete-button:hover {
157
- @apply -translate-y-px border-error bg-error-container/20 text-error;
158
- }
159
-
160
- .delete-button:disabled,
161
- .delete-button[aria-disabled="true"] {
162
- @apply cursor-not-allowed opacity-45;
163
- transform: none;
164
- }
165
- }
166
- </style>
9
+ <link href="/vendor/material-symbols/outlined.css" rel="stylesheet" />
10
+ <link href="styles/tailwind.generated.css" rel="stylesheet" />
167
11
  <link href="styles/tokens.css" rel="stylesheet" />
168
12
  <link href="styles/base.css" rel="stylesheet" />
169
13
  <link href="styles/utilities.css" rel="stylesheet" />
@@ -367,7 +367,11 @@ function syncQueryHistoryUi(historyId) {
367
367
 
368
368
  const historyItem = state.editor.history.find(entry => Number(entry.id) === numericId) ?? state.editor.historyDetail ?? null;
369
369
  const listItemNode = shellRefs.view.querySelector(
370
- `[data-action="select-query-history-item"][data-history-id="${String(numericId)}"]`,
370
+ [
371
+ '[data-action="select-query-history-item"][data-history-id="',
372
+ String(numericId),
373
+ '"]',
374
+ ].join(''),
371
375
  )?.closest('.query-history-item');
372
376
 
373
377
  if (historyItem && listItemNode instanceof HTMLElement) {
@@ -13,92 +13,67 @@ export function renderConnectionCard(connection, activeConnectionId) {
13
13
  : Boolean(connection.isActive);
14
14
  const clipPath = "polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%)";
15
15
  const primaryActionLabel = isActive ? "Open Overview" : "Set Active";
16
+ const connectionId = escapeHtml(connection.id);
17
+ const logoMarkup = renderConnectionLogo(connection, {
18
+ containerClass: [
19
+ "clipped-corner flex h-10 w-10 items-center justify-center overflow-hidden transition-colors",
20
+ isActive ? "bg-primary-container" : "bg-surface-container-highest",
21
+ ].join(" "),
22
+ containerAttributes: ['style="--clip-path: ', clipPath, ';"'].join(""),
23
+ imageClassName: "h-full w-full object-cover",
24
+ iconClassName: isActive ? "text-on-primary" : "text-outline-variant",
25
+ });
16
26
 
17
- return `
18
- <article
19
- class="connection-card clipped-corner ${isActive ? "is-active" : ""}"
20
- style="--clip-path: ${clipPath};"
21
- >
22
- <div class="flex-1 p-6">
23
- <div class="mb-6 flex items-start justify-between">
24
- ${renderConnectionLogo(connection, {
25
- containerClass: `clipped-corner flex h-10 w-10 items-center justify-center overflow-hidden transition-colors ${
26
- isActive ? "bg-primary-container" : "bg-surface-container-highest"
27
- }`,
28
- containerAttributes: `style="--clip-path: ${clipPath};"`,
29
- imageClassName: "h-full w-full object-cover",
30
- iconClassName: isActive ? "text-on-primary" : "text-outline-variant",
31
- })}
32
- <div class="flex items-center gap-2">
33
- ${renderStatusBadge(isActive ? "ACTIVE" : "RECENT", isActive ? "primary" : "muted")}
34
- ${connection.readOnly ? renderStatusBadge("READ_ONLY", "alert") : ""}
35
- </div>
36
- </div>
37
- <h3 class="mb-1 font-headline text-xl font-bold uppercase ${
38
- isActive ? "text-[#FCE300]" : "text-on-surface"
39
- }">
40
- ${escapeHtml(connection.label)}
41
- </h3>
42
- <p
43
- class="block overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[10px] text-outline-variant"
44
- title="${escapeHtml(connection.path)}"
45
- >
46
- ${escapeHtml(truncateMiddle(connection.path, 68))}
47
- </p>
48
- <div class="mt-8 grid grid-cols-2 gap-4">
49
- <div>
50
- <div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Allocation</div>
51
- <div class="text-xs font-bold text-on-surface">${escapeHtml(
52
- formatBytes(connection.sizeBytes)
53
- )}</div>
54
- </div>
55
- <div>
56
- <div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Modified</div>
57
- <div class="text-xs font-bold text-on-surface">${escapeHtml(
58
- formatDateTime(connection.lastModifiedAt)
59
- )}</div>
60
- </div>
61
- <div>
62
- <div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Opened</div>
63
- <div class="text-xs font-bold text-on-surface">${escapeHtml(
64
- formatDateTime(connection.lastOpenedAt)
65
- )}</div>
66
- </div>
67
- <div>
68
- <div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div>
69
- <div class="text-xs font-bold text-on-surface">${connection.readOnly ? "Read only" : "Read / Write"}</div>
70
- </div>
71
- </div>
72
- </div>
73
- <div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">
74
- <div class="grid grid-cols-[minmax(0,1fr)_5.1rem_5.8rem] gap-2">
75
- <button
76
- class="signature-button"
77
- data-action="select-connection"
78
- data-connection-id="${escapeHtml(connection.id)}"
79
- type="button"
80
- title="${primaryActionLabel}"
81
- >
82
- ${primaryActionLabel}
83
- </button>
84
- <button
85
- class="standard-button"
86
- data-action="edit-connection"
87
- data-connection-id="${escapeHtml(connection.id)}"
88
- type="button"
89
- >
90
- Edit
91
- </button>
92
- <button
93
- class="delete-button"
94
- data-action="remove-connection"
95
- data-connection-id="${escapeHtml(connection.id)}"
96
- type="button"
97
- >
98
- Remove
99
- </button>
100
- </div>
101
- </div>
102
- </article>
103
- `;
27
+ return [
28
+ '<article class="connection-card clipped-corner ',
29
+ isActive ? "is-active" : "",
30
+ '" style="--clip-path: ',
31
+ clipPath,
32
+ ';">',
33
+ '<div class="flex-1 p-6"><div class="mb-6 flex items-start justify-between">',
34
+ logoMarkup,
35
+ '<div class="flex items-center gap-2">',
36
+ renderStatusBadge(isActive ? "ACTIVE" : "RECENT", isActive ? "primary" : "muted"),
37
+ connection.readOnly ? renderStatusBadge("READ_ONLY", "alert") : "",
38
+ "</div></div>",
39
+ '<h3 class="mb-1 font-headline text-xl font-bold uppercase ',
40
+ isActive ? "text-[#FCE300]" : "text-on-surface",
41
+ '">',
42
+ escapeHtml(connection.label),
43
+ "</h3>",
44
+ '<p class="block overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[10px] text-outline-variant" title="',
45
+ escapeHtml(connection.path),
46
+ '">',
47
+ escapeHtml(truncateMiddle(connection.path, 68)),
48
+ "</p>",
49
+ '<div class="mt-8 grid grid-cols-2 gap-4">',
50
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Allocation</div><div class="text-xs font-bold text-on-surface">',
51
+ escapeHtml(formatBytes(connection.sizeBytes)),
52
+ "</div></div>",
53
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Modified</div><div class="text-xs font-bold text-on-surface">',
54
+ escapeHtml(formatDateTime(connection.lastModifiedAt)),
55
+ "</div></div>",
56
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Opened</div><div class="text-xs font-bold text-on-surface">',
57
+ escapeHtml(formatDateTime(connection.lastOpenedAt)),
58
+ "</div></div>",
59
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div><div class="text-xs font-bold text-on-surface">',
60
+ connection.readOnly ? "Read only" : "Read / Write",
61
+ "</div></div></div></div>",
62
+ '<div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">',
63
+ '<div class="grid grid-cols-[minmax(0,1fr)_5.1rem_5.8rem] gap-2">',
64
+ '<button class="signature-button" data-action="select-connection" data-connection-id="',
65
+ connectionId,
66
+ '" type="button" title="',
67
+ primaryActionLabel,
68
+ '">',
69
+ primaryActionLabel,
70
+ "</button>",
71
+ '<button class="standard-button" data-action="edit-connection" data-connection-id="',
72
+ connectionId,
73
+ '" type="button">Edit</button>',
74
+ '<button class="delete-button" data-action="remove-connection" data-connection-id="',
75
+ connectionId,
76
+ '" type="button">Remove</button>',
77
+ "</div></div></article>",
78
+ ].join("");
104
79
  }
@@ -6,6 +6,25 @@ import {
6
6
  } from "../utils/format.js";
7
7
  import { renderConnectionLogo } from "./connectionLogo.js";
8
8
 
9
+ function renderRecentConnectionButton(connection) {
10
+ return [
11
+ '<button class="control-button flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high" data-action="select-connection" data-connection-id="',
12
+ escapeHtml(connection.id),
13
+ '" type="button">',
14
+ renderConnectionLogo(connection, {
15
+ containerClass:
16
+ "flex h-8 w-8 items-center justify-center overflow-hidden bg-surface-container-highest",
17
+ imageClassName: "h-full w-full object-cover",
18
+ iconClassName: "text-sm text-primary-container",
19
+ }),
20
+ '<span class="min-w-0"><span class="block truncate font-mono text-xs">',
21
+ escapeHtml(connection.label),
22
+ '</span><span class="block truncate text-[10px] text-on-surface-variant/45">',
23
+ escapeHtml(truncateMiddle(connection.path, 34)),
24
+ "</span></span></button>",
25
+ ].join("");
26
+ }
27
+
9
28
  function renderRecentConnections(recentConnections = []) {
10
29
  if (!recentConnections.length) {
11
30
  return `
@@ -19,29 +38,7 @@ function renderRecentConnections(recentConnections = []) {
19
38
  <div class="flex flex-wrap justify-center gap-4">
20
39
  ${recentConnections
21
40
  .slice(0, 4)
22
- .map(
23
- (connection) => `
24
- <button
25
- class="control-button flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high"
26
- data-action="select-connection"
27
- data-connection-id="${escapeHtml(connection.id)}"
28
- type="button"
29
- >
30
- ${renderConnectionLogo(connection, {
31
- containerClass:
32
- "flex h-8 w-8 items-center justify-center overflow-hidden bg-surface-container-highest",
33
- imageClassName: "h-full w-full object-cover",
34
- iconClassName: "text-sm text-primary-container",
35
- })}
36
- <span class="min-w-0">
37
- <span class="block truncate font-mono text-xs">${escapeHtml(connection.label)}</span>
38
- <span class="block truncate text-[10px] text-on-surface-variant/45">${escapeHtml(
39
- truncateMiddle(connection.path, 34)
40
- )}</span>
41
- </span>
42
- </button>
43
- `
44
- )
41
+ .map((connection) => renderRecentConnectionButton(connection))
45
42
  .join("")}
46
43
  </div>
47
44
  `;