sqlite-hub 0.11.1 → 0.16.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.
Files changed (52) hide show
  1. package/README.md +52 -13
  2. package/database.sqlite +0 -0
  3. package/frontend/js/api.js +64 -12
  4. package/frontend/js/app.js +723 -37
  5. package/frontend/js/components/emptyState.js +42 -46
  6. package/frontend/js/components/modal.js +526 -2
  7. package/frontend/js/components/queryEditor.js +12 -3
  8. package/frontend/js/components/queryResults.js +79 -17
  9. package/frontend/js/components/rowEditorPanel.js +345 -12
  10. package/frontend/js/components/sidebar.js +106 -23
  11. package/frontend/js/components/tableDesignerEditor.js +69 -11
  12. package/frontend/js/store.js +437 -26
  13. package/frontend/js/utils/copyColumnExport.js +117 -0
  14. package/frontend/js/utils/exportFilenames.js +32 -0
  15. package/frontend/js/utils/filePathPreview.js +315 -0
  16. package/frontend/js/utils/format.js +1 -1
  17. package/frontend/js/utils/rowEditorJson.js +65 -0
  18. package/frontend/js/utils/sqlFormatter.js +691 -0
  19. package/frontend/js/utils/tableDesigner.js +178 -6
  20. package/frontend/js/utils/textCellStats.js +20 -0
  21. package/frontend/js/utils/timestampPreview.js +264 -0
  22. package/frontend/js/views/charts.js +3 -1
  23. package/frontend/js/views/data.js +39 -4
  24. package/frontend/js/views/editor.js +50 -1
  25. package/frontend/js/views/settings.js +1 -4
  26. package/frontend/js/views/structure.js +154 -212
  27. package/frontend/styles/base.css +6 -0
  28. package/frontend/styles/components.css +463 -2
  29. package/frontend/styles/structure-graph.css +0 -3
  30. package/frontend/styles/tailwind.generated.css +1 -1
  31. package/frontend/styles/tokens.css +95 -95
  32. package/package.json +2 -3
  33. package/server/routes/export.js +97 -12
  34. package/server/services/sqlite/dataBrowserService.js +2 -68
  35. package/server/services/sqlite/exportService.js +74 -15
  36. package/server/services/sqlite/introspection.js +209 -1
  37. package/server/services/sqlite/sqlExecutor.js +31 -0
  38. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  39. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  40. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  41. package/server/services/sqlite/tableDesignerService.js +1 -1
  42. package/server/services/sqlite/tableFilter.js +75 -0
  43. package/server/utils/csv.js +30 -4
  44. package/tests/check-constraint-options.test.js +90 -0
  45. package/tests/export-filenames.test.js +34 -0
  46. package/tests/file-path-preview.test.js +165 -0
  47. package/tests/row-editor-json.test.js +82 -0
  48. package/tests/row-editor-timestamp-preview.test.js +192 -0
  49. package/tests/sql-formatter.test.js +173 -0
  50. package/tests/sql-highlight.test.js +38 -0
  51. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  52. package/tests/text-cell-stats.test.js +38 -0
@@ -1,4 +1,4 @@
1
- import { escapeHtml } from '../utils/format.js';
1
+ import { escapeHtml, formatCompactDateTime, truncateMiddle } from '../utils/format.js';
2
2
  import { renderConnectionLogo } from './connectionLogo.js';
3
3
 
4
4
  const sidebarItems = [
@@ -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: 'SQL Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
9
+ { label: 'SQL_Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
10
10
  { label: 'Charts', href: '#/charts', key: 'charts', icon: 'bar_chart' },
11
- { label: 'Table Designer', href: '#/table-designer', key: 'tableDesigner', icon: 'table_chart' },
11
+ { label: 'Table_Designer', href: '#/table-designer', key: 'tableDesigner', icon: 'table_chart' },
12
12
  {
13
- label: 'Media Tagging',
13
+ label: 'MEDIA_TAGGING',
14
14
  key: 'mediaTagging',
15
15
  icon: 'sell',
16
16
  children: [
17
- { label: 'Setup', href: '#/media-tagging', key: 'mediaTaggingSetup' },
18
- { label: 'Tagging Queue', href: '#/media-tagging/queue', key: 'mediaTaggingQueue' },
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' },
@@ -37,9 +37,107 @@ function getActiveSidebarKey(routeName) {
37
37
  return routeName;
38
38
  }
39
39
 
40
+ function getConnectionTimeValue(connection) {
41
+ const value = connection?.lastOpenedAt ?? connection?.lastModifiedAt ?? 0;
42
+ const time = new Date(value).getTime();
43
+ return Number.isFinite(time) ? time : 0;
44
+ }
45
+
46
+ function getQuickPickConnections(state) {
47
+ const byId = new Map();
48
+
49
+ [state.connections.active, ...(state.connections.recent ?? [])].forEach(connection => {
50
+ if (!connection?.id || byId.has(connection.id)) {
51
+ return;
52
+ }
53
+
54
+ byId.set(connection.id, connection);
55
+ });
56
+
57
+ return [...byId.values()]
58
+ .sort((left, right) => getConnectionTimeValue(right) - getConnectionTimeValue(left))
59
+ .slice(0, 5);
60
+ }
61
+
62
+ function renderQuickPickCard(connection, activeConnectionId) {
63
+ const isActive = connection.id === activeConnectionId;
64
+ const label = connection.label || 'Untitled database';
65
+ const path = connection.path || '';
66
+
67
+ return `
68
+ <button
69
+ class="sidebar-db-picker-card ${isActive ? 'is-active' : ''}"
70
+ data-action="select-connection"
71
+ data-connection-id="${escapeHtml(connection.id)}"
72
+ type="button"
73
+ >
74
+ ${renderConnectionLogo(connection, {
75
+ containerClass: 'sidebar-db-picker-card__mark overflow-hidden',
76
+ imageClassName: 'h-full w-full object-cover',
77
+ iconClassName: 'text-[15px]',
78
+ icon: 'database',
79
+ })}
80
+ <span class="sidebar-db-picker-card__body">
81
+ <span class="sidebar-db-picker-card__label" title="${escapeHtml(label)}">
82
+ ${escapeHtml(label)}
83
+ </span>
84
+ <span class="sidebar-db-picker-card__path" title="${escapeHtml(path)}">
85
+ ${escapeHtml(path ? truncateMiddle(path, 34) : 'path:n/a')}
86
+ </span>
87
+ <span class="sidebar-db-picker-card__meta">
88
+ ${isActive ? 'ACTIVE' : escapeHtml(formatCompactDateTime(connection.lastOpenedAt))}
89
+ ${connection.readOnly ? ' // READ_ONLY' : ''}
90
+ </span>
91
+ </span>
92
+ </button>
93
+ `;
94
+ }
95
+
96
+ function renderConnectionQuickPicker(state) {
97
+ const activeConnection = state.connections.active;
98
+ const quickPicks = getQuickPickConnections(state);
99
+ const hasQuickPicks = quickPicks.length > 0;
100
+
101
+ return `
102
+ <details class="sidebar-db-picker">
103
+ <summary class="sidebar-footer-card" title="Switch active database">
104
+ ${renderConnectionLogo(activeConnection, {
105
+ containerClass: 'sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary',
106
+ imageClassName: 'h-full w-full object-cover',
107
+ iconClassName: 'text-[15px]',
108
+ icon: 'memory',
109
+ })}
110
+ <span class="min-w-0 flex-1">
111
+ <span class="block truncate text-[10px] font-bold text-on-surface">
112
+ ${escapeHtml(activeConnection?.label ?? 'NO_ACTIVE_DATABASE')}
113
+ </span>
114
+ <span class="block text-[8px] text-on-surface-variant/60">
115
+ ${activeConnection?.readOnly ? 'READ_ONLY' : activeConnection ? 'READ_WRITE' : 'SELECT_DATABASE'}
116
+ </span>
117
+ </span>
118
+ <span class="material-symbols-outlined sidebar-db-picker__chevron" aria-hidden="true">expand_less</span>
119
+ </summary>
120
+ <div class="sidebar-db-picker__panel">
121
+ <div class="sidebar-db-picker__header">
122
+ <span>Quick Picks</span>
123
+ <span>${escapeHtml(String(quickPicks.length))}/5</span>
124
+ </div>
125
+ ${
126
+ hasQuickPicks
127
+ ? `<div class="sidebar-db-picker__list">
128
+ ${quickPicks
129
+ .map(connection => renderQuickPickCard(connection, activeConnection?.id))
130
+ .join('')}
131
+ </div>`
132
+ : `<div class="sidebar-db-picker__empty">No recent databases</div>`
133
+ }
134
+ </div>
135
+ </details>
136
+ `;
137
+ }
138
+
40
139
  export function renderSidebar(state) {
41
140
  const activeKey = getActiveSidebarKey(state.route.name);
42
- const activeConnection = state.connections.active;
43
141
  const expandedKey = activeKey === 'mediaTagging' ? 'mediaTagging' : null;
44
142
 
45
143
  return `
@@ -86,22 +184,7 @@ export function renderSidebar(state) {
86
184
  .join('')}
87
185
  </nav>
88
186
  <div class="sidebar-footer">
89
- <div class="sidebar-footer-card">
90
- ${renderConnectionLogo(activeConnection, {
91
- containerClass: 'sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary',
92
- imageClassName: 'h-full w-full object-cover',
93
- iconClassName: 'text-[15px]',
94
- icon: 'memory',
95
- })}
96
- <div class="min-w-0">
97
- <p class="truncate text-[10px] font-bold text-on-surface">
98
- ${escapeHtml(activeConnection?.label ?? 'NO_ACTIVE_DATABASE')}
99
- </p>
100
- <p class="text-[8px] text-on-surface-variant/60">
101
- ${activeConnection?.readOnly ? 'READ_ONLY' : 'READ_WRITE'}
102
- </p>
103
- </div>
104
- </div>
187
+ ${renderConnectionQuickPicker(state)}
105
188
  </div>
106
189
  `;
107
190
  }
@@ -81,6 +81,54 @@ export function renderTableDesignerReferenceColumnOptions(
81
81
  ].join("");
82
82
  }
83
83
 
84
+ function normalizeConstraintColumnName(name) {
85
+ return String(name ?? "").trim().toLowerCase();
86
+ }
87
+
88
+ function constraintIncludesColumn(constraint, columnName) {
89
+ const normalizedColumn = normalizeConstraintColumnName(columnName);
90
+
91
+ if (!normalizedColumn) {
92
+ return false;
93
+ }
94
+
95
+ return (constraint.columns ?? []).some(
96
+ (constraintColumn) => normalizeConstraintColumnName(constraintColumn.name) === normalizedColumn
97
+ );
98
+ }
99
+
100
+ function countColumnCheckConstraints(column, draft) {
101
+ if (draft.mode !== "edit") {
102
+ return 0;
103
+ }
104
+
105
+ return (draft.checkConstraints ?? []).filter((constraint) =>
106
+ constraintIncludesColumn(constraint, column.name)
107
+ ).length;
108
+ }
109
+
110
+ function renderColumnCheckAction(column, draft) {
111
+ const checkCount = countColumnCheckConstraints(column, draft);
112
+ const columnName = column.name || "Unnamed column";
113
+
114
+ return `
115
+ <button
116
+ aria-label="Checks for ${escapeHtml(columnName)}"
117
+ class="standard-button table-designer-row-check-button"
118
+ data-action="open-modal"
119
+ data-column-id="${escapeHtml(column.id)}"
120
+ data-column-name="${escapeHtml(column.name)}"
121
+ data-modal="table-designer-constraints"
122
+ title="Checks for ${escapeHtml(columnName)}"
123
+ type="button"
124
+ >
125
+ <span class="material-symbols-outlined text-base">fact_check</span>
126
+ <span>Checks</span>
127
+ ${checkCount ? `<span class="status-badge status-badge--muted">${escapeHtml(String(checkCount))}</span>` : ""}
128
+ </button>
129
+ `;
130
+ }
131
+
84
132
  function renderColumnRow(column, draft, catalogTables) {
85
133
  const typeOptions = draft.supportedTypes
86
134
  .map((type) =>
@@ -162,10 +210,13 @@ function renderColumnRow(column, draft, catalogTables) {
162
210
  '" data-field="referencesColumn">',
163
211
  referenceColumnOptions,
164
212
  "</select>",
213
+ '<div class="table-designer-row-actions">',
214
+ renderColumnCheckAction(column, draft),
165
215
  '<button class="delete-button" data-action="remove-table-designer-column" data-column-id="',
166
216
  columnId,
167
217
  '" type="button"><span class="material-symbols-outlined text-base">delete</span><span>Remove</span></button>',
168
218
  "</div>",
219
+ "</div>",
169
220
  ].join("");
170
221
  }
171
222
 
@@ -183,7 +234,7 @@ function renderColumnGrid(draft, catalogTables) {
183
234
  <div>Default</div>
184
235
  <div>FK Table</div>
185
236
  <div>FK Column</div>
186
- <div></div>
237
+ <div>Actions</div>
187
238
  </div>
188
239
  ${visibleColumns.map((column) => renderColumnRow(column, draft, catalogTables)).join("")}
189
240
  </div>
@@ -225,6 +276,10 @@ export function renderTableDesignerFeedback(draft, saveError) {
225
276
  title: message,
226
277
  }));
227
278
  const warningItems = draft?.warnings ?? [];
279
+ const schemaNotes = warningItems.filter(
280
+ (item) => item.tone === "muted" && item.code !== "COMPLEX_UNIQUE_CONSTRAINTS_PRESENT"
281
+ );
282
+ const alertWarnings = warningItems.filter((item) => item.tone !== "muted");
228
283
 
229
284
  return [
230
285
  saveError
@@ -233,10 +288,11 @@ export function renderTableDesignerFeedback(draft, saveError) {
233
288
  <div class="table-designer-main__error-code">${escapeHtml(saveError.code)}</div>
234
289
  <div class="table-designer-main__error-text">${escapeHtml(saveError.message)}</div>
235
290
  </div>
236
- `
291
+ `
237
292
  : "",
238
293
  renderWarningList(validationItems, "table-designer-banner is-validation", "Validation", "alert"),
239
- renderWarningList(warningItems, "table-designer-banner is-warning", "Warnings", "alert"),
294
+ renderWarningList(alertWarnings, "table-designer-banner is-warning", "Warnings", "alert"),
295
+ renderWarningList(schemaNotes, "table-designer-banner is-note", "Schema Notes", "muted"),
240
296
  ].join("");
241
297
  }
242
298
 
@@ -305,7 +361,7 @@ export function renderTableDesignerEditor(state) {
305
361
  <div class="table-designer-main__subtitle">
306
362
  ${escapeHtml(String(visibleColumns.length))} visible column${
307
363
  visibleColumns.length === 1 ? "" : "s"
308
- } // SQLite-safe operations only
364
+ } // Table Designer v2 // SQLite-safe operations only
309
365
  </div>
310
366
  </div>
311
367
  <div class="table-designer-main__actions">
@@ -338,13 +394,15 @@ export function renderTableDesignerEditor(state) {
338
394
  <div>
339
395
  <div class="table-designer-main__section-title">Columns</div>
340
396
  </div>
341
- <button
342
- class="standard-button"
343
- data-action="add-table-designer-column"
344
- type="button"
345
- >
346
- + Add Column
347
- </button>
397
+ <div class="table-designer-main__section-actions">
398
+ <button
399
+ class="standard-button"
400
+ data-action="add-table-designer-column"
401
+ type="button"
402
+ >
403
+ + Add Column
404
+ </button>
405
+ </div>
348
406
  </div>
349
407
  ${renderColumnGrid(draft, catalogTables)}
350
408
  </section>