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
@@ -50,15 +50,7 @@ function renderReadonlyField(label, value) {
50
50
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
51
51
  <div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
52
52
  <span>${escapeHtml(displayLabel)}</span>
53
- ${badges
54
- .map(
55
- (badge) => `
56
- <span class="border border-outline-variant/20 bg-surface-container px-2 py-1 text-[9px] text-primary-container">
57
- ${escapeHtml(badge)}
58
- </span>
59
- `
60
- )
61
- .join("")}
53
+ ${badges.map((badge) => renderFieldBadge(badge)).join("")}
62
54
  </div>
63
55
  ${
64
56
  jsonPreview
@@ -72,37 +64,68 @@ function renderReadonlyField(label, value) {
72
64
  function renderEditableField(field) {
73
65
  const badges = Array.isArray(field.badges) ? field.badges : [];
74
66
  const jsonPreview = getJsonPreview(field.value);
67
+ const inputType = field.inputType === "number" ? "number" : "text";
68
+ const numberStep = field.numberStep === "1" ? "1" : "any";
75
69
 
76
70
  return `
77
71
  <label class="block space-y-2">
78
72
  <span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
79
73
  <span>${escapeHtml(field.label ?? field.name)}</span>
80
- ${badges
81
- .map(
82
- (badge) => `
83
- <span class="border border-outline-variant/20 bg-surface-container px-2 py-1 text-[9px] text-primary-container">
84
- ${escapeHtml(badge)}
85
- </span>
86
- `
87
- )
88
- .join("")}
74
+ ${badges.map((badge) => renderFieldBadge(badge)).join("")}
89
75
  </span>
90
76
  ${
91
77
  jsonPreview
92
78
  ? renderJsonViewer(jsonPreview, "JSON Preview")
93
79
  : ""
94
80
  }
95
- <textarea
96
- 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 ${
97
- jsonPreview ? "min-h-[14rem] font-mono leading-6" : "min-h-[56px]"
98
- }"
99
- name="field:${escapeHtml(field.name)}"
100
- spellcheck="false"
101
- >${escapeHtml(field.value ?? "")}</textarea>
81
+ ${
82
+ inputType === "number" && !jsonPreview
83
+ ? `
84
+ <input
85
+ 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"
86
+ name="field:${escapeHtml(field.name)}"
87
+ step="${escapeHtml(numberStep)}"
88
+ type="number"
89
+ value="${escapeHtml(field.value ?? "")}"
90
+ />
91
+ `
92
+ : `
93
+ <textarea
94
+ 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 ${
95
+ jsonPreview ? "min-h-[14rem] font-mono leading-6" : "min-h-[56px]"
96
+ }"
97
+ name="field:${escapeHtml(field.name)}"
98
+ spellcheck="false"
99
+ >${escapeHtml(field.value ?? "")}</textarea>
100
+ `
101
+ }
102
102
  </label>
103
103
  `;
104
104
  }
105
105
 
106
+ function getFieldBadgeClassName(tone) {
107
+ if (tone === "primary-key") {
108
+ return "border-primary-container/35 bg-primary-container/15 text-primary-container";
109
+ }
110
+
111
+ if (tone === "foreign-key") {
112
+ return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
113
+ }
114
+
115
+ return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
116
+ }
117
+
118
+ function renderFieldBadge(badge) {
119
+ const label = typeof badge === "object" ? badge.label : badge;
120
+ const tone = typeof badge === "object" ? badge.tone : "";
121
+
122
+ return `
123
+ <span class="border px-2 py-1 text-[9px] ${getFieldBadgeClassName(tone)}">
124
+ ${escapeHtml(label)}
125
+ </span>
126
+ `;
127
+ }
128
+
106
129
  export function renderRowEditorPanel({
107
130
  title,
108
131
  sectionLabel = "Row Editor",
@@ -126,43 +149,40 @@ export function renderRowEditorPanel({
126
149
  }) {
127
150
  const canSubmit = !disabledMessage && editableFields.length > 0;
128
151
  const canDelete = !disabledMessage && deleteEnabled;
129
- const formId = `${formName}-panel-form`;
152
+ const formId = [formName, "-panel-form"].join("");
130
153
  const headerActions = [
131
154
  reloadAction
132
- ? `
133
- <button
134
- class="standard-button"
135
- data-action="${escapeHtml(reloadAction)}"
136
- type="button"
137
- >
138
- Reload
139
- </button>
140
- `
155
+ ? [
156
+ '<button class="standard-button" data-action="',
157
+ escapeHtml(reloadAction),
158
+ '" type="button">Reload</button>',
159
+ ].join("")
141
160
  : "",
142
161
  canSubmit
143
- ? `
144
- <button
145
- class="standard-button"
146
- form="${escapeHtml(formId)}"
147
- type="submit"
148
- ${saving || deleting ? "disabled" : ""}
149
- >
150
- ${escapeHtml(saving ? "Saving..." : submitLabel)}
151
- </button>
152
- `
162
+ ? [
163
+ '<button class="standard-button" form="',
164
+ escapeHtml(formId),
165
+ '" type="submit" ',
166
+ saving || deleting ? "disabled" : "",
167
+ ">",
168
+ escapeHtml(saving ? "Saving..." : submitLabel),
169
+ "</button>",
170
+ ].join("")
153
171
  : "",
154
172
  canDelete
155
- ? `
156
- <button
157
- class="delete-button"
158
- data-action="${escapeHtml(deleteAction)}"
159
- ${deleteRowIndex === null ? "" : `data-row-index="${escapeHtml(String(deleteRowIndex))}"`}
160
- type="button"
161
- ${saving || deleting ? "disabled" : ""}
162
- >
163
- ${escapeHtml(deleting ? "Deleting..." : deleteLabel)}
164
- </button>
165
- `
173
+ ? [
174
+ '<button class="delete-button" data-action="',
175
+ escapeHtml(deleteAction),
176
+ '" ',
177
+ deleteRowIndex === null
178
+ ? ""
179
+ : ['data-row-index="', escapeHtml(String(deleteRowIndex)), '"'].join(""),
180
+ ' type="button" ',
181
+ saving || deleting ? "disabled" : "",
182
+ ">",
183
+ escapeHtml(deleting ? "Deleting..." : deleteLabel),
184
+ "</button>",
185
+ ].join("")
166
186
  : "",
167
187
  ]
168
188
  .filter(Boolean)
@@ -57,123 +57,98 @@ function getReferencedColumns(draft, catalogTables, referencesTable) {
57
57
 
58
58
  function renderColumnRow(column, draft, catalogTables) {
59
59
  const referenceColumns = getReferencedColumns(draft, catalogTables, column.referencesTable);
60
+ const typeOptions = draft.supportedTypes
61
+ .map((type) =>
62
+ [
63
+ '<option value="',
64
+ escapeHtml(type),
65
+ '" ',
66
+ type === column.type ? "selected" : "",
67
+ ">",
68
+ escapeHtml(type),
69
+ "</option>",
70
+ ].join("")
71
+ )
72
+ .join("");
73
+ const referenceTableOptions = catalogTables
74
+ .map((table) =>
75
+ [
76
+ '<option value="',
77
+ escapeHtml(table.name),
78
+ '" ',
79
+ table.name === column.referencesTable ? "selected" : "",
80
+ ">",
81
+ escapeHtml(table.name),
82
+ "</option>",
83
+ ].join("")
84
+ )
85
+ .join("");
86
+ const referenceColumnOptions = referenceColumns
87
+ .map((name) =>
88
+ [
89
+ '<option value="',
90
+ escapeHtml(name),
91
+ '" ',
92
+ name === column.referencesColumn ? "selected" : "",
93
+ ">",
94
+ escapeHtml(name),
95
+ "</option>",
96
+ ].join("")
97
+ )
98
+ .join("");
60
99
 
61
- return `
62
- <div class="table-designer-grid__row">
63
- <input
64
- class="table-designer-field"
65
- data-bind="table-designer-column-field"
66
- data-column-id="${escapeHtml(column.id)}"
67
- data-field="name"
68
- placeholder="column_name"
69
- spellcheck="false"
70
- type="text"
71
- value="${escapeHtml(column.name)}"
72
- />
73
- <select
74
- class="table-designer-field"
75
- data-bind="table-designer-column-field"
76
- data-column-id="${escapeHtml(column.id)}"
77
- data-field="type"
78
- >
79
- ${draft.supportedTypes
80
- .map(
81
- (type) => `
82
- <option value="${escapeHtml(type)}" ${type === column.type ? "selected" : ""}>
83
- ${escapeHtml(type)}
84
- </option>
85
- `
86
- )
87
- .join("")}
88
- </select>
89
- <label class="standard-checkbox table-designer-check table-designer-checkbox-override">
90
- <input
91
- data-bind="table-designer-column-flag"
92
- data-column-id="${escapeHtml(column.id)}"
93
- data-field="notNull"
94
- type="checkbox"
95
- ${column.notNull ? "checked" : ""}
96
- />
97
- <span>Not null</span>
98
- </label>
99
- <label class="standard-checkbox table-designer-check table-designer-checkbox-override">
100
- <input
101
- data-bind="table-designer-column-flag"
102
- data-column-id="${escapeHtml(column.id)}"
103
- data-field="unique"
104
- type="checkbox"
105
- ${column.unique ? "checked" : ""}
106
- />
107
- <span>Unique</span>
108
- </label>
109
- <label class="standard-checkbox table-designer-check table-designer-checkbox-override">
110
- <input
111
- data-bind="table-designer-column-flag"
112
- data-column-id="${escapeHtml(column.id)}"
113
- data-field="primaryKey"
114
- type="checkbox"
115
- ${column.primaryKey ? "checked" : ""}
116
- />
117
- <span>PK</span>
118
- </label>
119
- <input
120
- class="table-designer-field"
121
- data-bind="table-designer-column-field"
122
- data-column-id="${escapeHtml(column.id)}"
123
- data-field="defaultValue"
124
- placeholder="SQL default"
125
- spellcheck="false"
126
- type="text"
127
- value="${escapeHtml(column.defaultValue)}"
128
- />
129
- <select
130
- class="table-designer-field"
131
- data-bind="table-designer-column-field"
132
- data-column-id="${escapeHtml(column.id)}"
133
- data-field="referencesTable"
134
- >
135
- <option value="">No FK table</option>
136
- ${catalogTables
137
- .map(
138
- (table) => `
139
- <option
140
- value="${escapeHtml(table.name)}"
141
- ${table.name === column.referencesTable ? "selected" : ""}
142
- >
143
- ${escapeHtml(table.name)}
144
- </option>
145
- `
146
- )
147
- .join("")}
148
- </select>
149
- <select
150
- class="table-designer-field"
151
- data-bind="table-designer-column-field"
152
- data-column-id="${escapeHtml(column.id)}"
153
- data-field="referencesColumn"
154
- >
155
- <option value="">No FK column</option>
156
- ${referenceColumns
157
- .map(
158
- (name) => `
159
- <option value="${escapeHtml(name)}" ${name === column.referencesColumn ? "selected" : ""}>
160
- ${escapeHtml(name)}
161
- </option>
162
- `
163
- )
164
- .join("")}
165
- </select>
166
- <button
167
- class="delete-button"
168
- data-action="remove-table-designer-column"
169
- data-column-id="${escapeHtml(column.id)}"
170
- type="button"
171
- >
172
- <span class="material-symbols-outlined text-base">delete</span>
173
- <span>Remove</span>
174
- </button>
175
- </div>
176
- `;
100
+ const columnId = escapeHtml(column.id);
101
+
102
+ return [
103
+ '<div class="table-designer-grid__row">',
104
+ '<input class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
105
+ columnId,
106
+ '" data-field="name" placeholder="column_name" spellcheck="false" type="text" value="',
107
+ escapeHtml(column.name),
108
+ '" />',
109
+ '<select class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
110
+ columnId,
111
+ '" data-field="type">',
112
+ typeOptions,
113
+ "</select>",
114
+ '<label class="standard-checkbox table-designer-check table-designer-checkbox-override">',
115
+ '<input data-bind="table-designer-column-flag" data-column-id="',
116
+ columnId,
117
+ '" data-field="notNull" type="checkbox" ',
118
+ column.notNull ? "checked" : "",
119
+ ' /><span>Not null</span></label>',
120
+ '<label class="standard-checkbox table-designer-check table-designer-checkbox-override">',
121
+ '<input data-bind="table-designer-column-flag" data-column-id="',
122
+ columnId,
123
+ '" data-field="unique" type="checkbox" ',
124
+ column.unique ? "checked" : "",
125
+ ' /><span>Unique</span></label>',
126
+ '<label class="standard-checkbox table-designer-check table-designer-checkbox-override">',
127
+ '<input data-bind="table-designer-column-flag" data-column-id="',
128
+ columnId,
129
+ '" data-field="primaryKey" type="checkbox" ',
130
+ column.primaryKey ? "checked" : "",
131
+ ' /><span>PK</span></label>',
132
+ '<input class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
133
+ columnId,
134
+ '" data-field="defaultValue" placeholder="SQL default" spellcheck="false" type="text" value="',
135
+ escapeHtml(column.defaultValue),
136
+ '" />',
137
+ '<select class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
138
+ columnId,
139
+ '" data-field="referencesTable"><option value="">No FK table</option>',
140
+ referenceTableOptions,
141
+ "</select>",
142
+ '<select class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
143
+ columnId,
144
+ '" data-field="referencesColumn"><option value="">No FK column</option>',
145
+ referenceColumnOptions,
146
+ "</select>",
147
+ '<button class="delete-button" data-action="remove-table-designer-column" data-column-id="',
148
+ columnId,
149
+ '" type="button"><span class="material-symbols-outlined text-base">delete</span><span>Remove</span></button>',
150
+ "</div>",
151
+ ].join("");
177
152
  }
178
153
 
179
154
  function renderColumnGrid(draft, catalogTables) {
@@ -5,6 +5,7 @@ import {
5
5
  formatQueryChartAxisValue,
6
6
  getAnalysisColumn,
7
7
  sortQueryChartRows,
8
+ sortQueryChartRowsByNumericColumn,
8
9
  } from "./queryCharts.js";
9
10
 
10
11
  const CHART_PALETTE = ["#FCE300", "#2DFAFF", "#FFB4AB", "#CDC7AB", "#7DD3FC", "#86EFAC"];
@@ -55,7 +56,10 @@ function buildLineLabelConfig(enabled) {
55
56
  }
56
57
 
57
58
  export function buildBarChartOption(chart, rows) {
58
- const sortedRows = sortQueryChartRows(rows, chart.config.x_column, chart.config.sort_direction);
59
+ const sortedRows =
60
+ chart.config.sort_by === "y"
61
+ ? sortQueryChartRowsByNumericColumn(rows, chart.config.y_column, chart.config.sort_direction)
62
+ : sortQueryChartRows(rows, chart.config.x_column, chart.config.sort_direction);
59
63
 
60
64
  return {
61
65
  ...buildCommonOption(),
@@ -312,7 +312,43 @@ function getFirstColumn(columns, predicate) {
312
312
 
313
313
  function getNextNumericColumn(analysis, excludedNames = []) {
314
314
  const excluded = new Set(excludedNames);
315
- return getFirstColumn(analysis?.numberColumns, (column) => !excluded.has(column.name));
315
+ const candidates = (analysis?.numberColumns ?? []).filter((column) => !excluded.has(column.name));
316
+
317
+ if (!candidates.length) {
318
+ return null;
319
+ }
320
+
321
+ const scoredCandidates = candidates
322
+ .map((column, index) => ({
323
+ column,
324
+ index,
325
+ score: getNumericMeasureScore(column.name),
326
+ }))
327
+ .sort((left, right) => right.score - left.score || left.index - right.index);
328
+
329
+ return scoredCandidates[0]?.column ?? null;
330
+ }
331
+
332
+ function getNumericMeasureScore(columnName = "") {
333
+ const normalized = String(columnName ?? "")
334
+ .trim()
335
+ .toLowerCase();
336
+
337
+ let score = 0;
338
+
339
+ if (
340
+ /(count|total|sum|avg|average|amount|score|size|value|price|cost|rate|percent|pct|percentage|number)/.test(
341
+ normalized
342
+ )
343
+ ) {
344
+ score += 20;
345
+ }
346
+
347
+ if (/(^|_)(id|uuid|pk|key)$/.test(normalized) || /(^|_)id$/.test(normalized)) {
348
+ score -= 40;
349
+ }
350
+
351
+ return score;
316
352
  }
317
353
 
318
354
  function getPrimaryDimensionColumn(analysis) {
@@ -406,7 +442,8 @@ export function buildSuggestedChartConfig(chartType, analysis) {
406
442
  y_column: firstNumeric?.name ?? "",
407
443
  show_legend: true,
408
444
  show_labels: false,
409
- sort_direction: "asc",
445
+ sort_by: "y",
446
+ sort_direction: "desc",
410
447
  };
411
448
  }
412
449
  }
@@ -524,6 +561,17 @@ export function sortQueryChartRows(rows, columnName, direction = "asc") {
524
561
  );
525
562
  }
526
563
 
564
+ export function sortQueryChartRowsByNumericColumn(rows, columnName, direction = "asc") {
565
+ const multiplier = direction === "desc" ? -1 : 1;
566
+
567
+ return [...(rows ?? [])].sort((left, right) => {
568
+ const leftValue = coerceNumericChartValue(left?.[columnName]);
569
+ const rightValue = coerceNumericChartValue(right?.[columnName]);
570
+
571
+ return compareValues(leftValue, rightValue) * multiplier;
572
+ });
573
+ }
574
+
527
575
  export function formatQueryChartAxisValue(value) {
528
576
  if (value === null || value === undefined) {
529
577
  return "NULL";