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
@@ -241,38 +241,34 @@ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
241
241
  if (!table || !columns.length) {
242
242
  return '';
243
243
  }
244
-
245
- return `
246
- <div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">
247
- <label class="control-shell flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-3">
248
- <span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>
249
- <input
250
- class="control-input control-input--ghost min-w-0 flex-1 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40"
251
- data-bind="data-search-query"
252
- placeholder="Filter current page..."
253
- type="search"
254
- value="${escapeHtml(state.dataBrowser.searchQuery ?? '')}"
255
- />
256
- </label>
257
- <select
258
- class="control-select min-w-[14rem] border border-outline-variant/20 bg-surface-container-lowest font-mono text-xs tracking-[0.04em] text-on-surface outline-none"
259
- data-bind="data-search-column"
260
- >
261
- ${columns
262
- .map(
263
- columnName => `
264
- <option value="${escapeHtml(columnName)}" ${columnName === activeColumn ? 'selected' : ''}>
265
- ${escapeHtml(columnName)}
266
- </option>
267
- `,
268
- )
269
- .join('')}
270
- </select>
271
- <div class="text-[10px] font-mono tracking-[0.14em] text-on-surface-variant/55">
272
- ${escapeHtml(formatNumber(filteredRowCount))} match${filteredRowCount === 1 ? '' : 'es'} on this page
273
- </div>
274
- </div>
275
- `;
244
+ const columnOptions = columns
245
+ .map(columnName => [
246
+ '<option value="',
247
+ escapeHtml(columnName),
248
+ '" ',
249
+ columnName === activeColumn ? 'selected' : '',
250
+ '>',
251
+ escapeHtml(columnName),
252
+ '</option>',
253
+ ].join(''))
254
+ .join('');
255
+
256
+ return [
257
+ '<div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">',
258
+ '<label class="control-shell flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-3">',
259
+ '<span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>',
260
+ '<input class="control-input control-input--ghost min-w-0 flex-1 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40" data-bind="data-search-query" placeholder="Filter current page..." type="search" value="',
261
+ escapeHtml(state.dataBrowser.searchQuery ?? ''),
262
+ '" /></label>',
263
+ '<select class="control-select min-w-[14rem] border border-outline-variant/20 bg-surface-container-lowest font-mono text-xs tracking-[0.04em] text-on-surface outline-none" data-bind="data-search-column">',
264
+ columnOptions,
265
+ '</select>',
266
+ '<div class="text-[10px] font-mono tracking-[0.14em] text-on-surface-variant/55">',
267
+ escapeHtml(formatNumber(filteredRowCount)),
268
+ ' match',
269
+ filteredRowCount === 1 ? '' : 'es',
270
+ ' on this page</div></div>',
271
+ ].join('');
276
272
  }
277
273
 
278
274
  function renderTableSurface(state) {
@@ -331,107 +327,95 @@ function renderTableSurface(state) {
331
327
  const pageSizes = [25, 50, 100];
332
328
  const filteredRowCount = filteredRows.length;
333
329
  const hasActiveSearch = Boolean(searchQuery);
330
+ const gridMarkup = renderDataGrid({
331
+ columns,
332
+ rows: filteredRows.map(({ row }) => row),
333
+ tableClass: 'min-w-full border-collapse text-left font-mono text-xs',
334
+ theadClass: 'sticky top-0 z-10 bg-surface-container-highest',
335
+ tbodyClass: 'divide-y divide-outline-variant/5',
336
+ getRowClass: (_, filteredIndex) => {
337
+ const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
338
+
339
+ return [
340
+ 'data-browser-row',
341
+ filteredIndex % 2 === 0 ? 'data-browser-row--even' : 'data-browser-row--odd',
342
+ state.dataBrowser.selectedRowIndex === rowIndex ? 'is-selected' : '',
343
+ 'cursor-pointer transition-colors',
344
+ ].filter(Boolean).join(' ');
345
+ },
346
+ getRowAttrs: (_, filteredIndex) => {
347
+ const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
334
348
 
335
- return `
336
- <div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">
337
- ${renderTableSearchBar(table, state, activeColumn, filteredRowCount)}
338
- <div class="custom-scrollbar flex-1 overflow-auto">
339
- ${renderDataGrid({
340
- columns,
341
- rows: filteredRows.map(({ row }) => row),
342
- tableClass: 'min-w-full border-collapse text-left font-mono text-xs',
343
- theadClass: 'sticky top-0 z-10 bg-surface-container-highest',
344
- tbodyClass: 'divide-y divide-outline-variant/5',
345
- getRowClass: (_, filteredIndex) => {
346
- const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
347
-
348
- return `data-browser-row ${
349
- filteredIndex % 2 === 0 ? 'data-browser-row--even' : 'data-browser-row--odd'
350
- } ${state.dataBrowser.selectedRowIndex === rowIndex ? 'is-selected' : ''} cursor-pointer transition-colors`;
351
- },
352
- getRowAttrs: (_, filteredIndex) => {
353
- const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
354
-
355
- return `data-action="select-data-row" data-row-index="${rowIndex}"`;
356
- },
357
- })}
358
- ${
359
- !table.rows?.length
360
- ? `
361
- <div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
362
- <p class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/40">
363
- TABLE_IS_EMPTY
364
- </p>
365
- </div>
366
- `
367
- : !filteredRowCount
368
- ? `
369
- <div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
370
- <p class="font-mono text-[10px] tracking-[0.18em] text-on-surface-variant/40">
371
- ${hasActiveSearch ? 'No matching rows on this page.' : 'No rows available.'}
372
- </p>
373
- </div>
374
- `
375
- : ''
376
- }
377
- </div>
378
- <footer class="flex flex-wrap items-center justify-between gap-4 border-t border-outline-variant/10 bg-surface-container px-6 py-4">
379
- <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
380
- showing ${escapeHtml(formatNumber(fromRow))}-${escapeHtml(formatNumber(toRow))} of ${escapeHtml(
381
- formatNumber(totalRows),
382
- )} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` : ''}
383
- </div>
384
- <div class="flex flex-wrap items-center gap-4">
385
- <div class="flex items-center gap-2">
386
- <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
387
- rows
388
- </span>
389
- <div class="flex items-center gap-2">
390
- ${pageSizes
391
- .map(
392
- pageSize => `
393
- <button
394
- class="standard-button ${
395
- pageSize === (table.limit ?? state.dataBrowser.pageSize) ? 'is-active' : ''
396
- }"
397
- data-action="set-data-page-size"
398
- data-page-size="${pageSize}"
399
- type="button"
400
- >
401
- ${pageSize}
402
- </button>
403
- `,
404
- )
405
- .join('')}
406
- </div>
407
- </div>
408
- <div class="flex items-center gap-2">
409
- <button
410
- class="standard-button"
411
- data-action="set-data-page"
412
- data-page="${page - 1}"
413
- type="button"
414
- ${page <= 1 ? 'disabled' : ''}
415
- >
416
- Prev
417
- </button>
418
- <div class="min-w-[7rem] text-center text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
419
- page ${escapeHtml(formatNumber(page))} / ${escapeHtml(formatNumber(pageCount))}
420
- </div>
421
- <button
422
- class="standard-button"
423
- data-action="set-data-page"
424
- data-page="${page + 1}"
425
- type="button"
426
- ${page >= pageCount ? 'disabled' : ''}
427
- >
428
- Next
429
- </button>
430
- </div>
431
- </div>
432
- </footer>
433
- </div>
434
- `;
349
+ return ['data-action="select-data-row" data-row-index="', rowIndex, '"'].join('');
350
+ },
351
+ });
352
+ const emptyMarkup = !table.rows?.length
353
+ ? '<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10"><p class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/40">TABLE_IS_EMPTY</p></div>'
354
+ : !filteredRowCount
355
+ ? [
356
+ '<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">',
357
+ '<p class="font-mono text-[10px] tracking-[0.18em] text-on-surface-variant/40">',
358
+ hasActiveSearch ? 'No matching rows on this page.' : 'No rows available.',
359
+ '</p></div>',
360
+ ].join('')
361
+ : '';
362
+ const visibleRowsText = hasActiveSearch
363
+ ? [' // ', escapeHtml(formatNumber(filteredRowCount)), ' visible on this page'].join('')
364
+ : '';
365
+ const pageSizeButtons = pageSizes
366
+ .map(pageSize =>
367
+ [
368
+ '<button class="standard-button ',
369
+ pageSize === (table.limit ?? state.dataBrowser.pageSize) ? 'is-active' : '',
370
+ '" data-action="set-data-page-size" data-page-size="',
371
+ pageSize,
372
+ '" type="button">',
373
+ pageSize,
374
+ '</button>',
375
+ ].join(''),
376
+ )
377
+ .join('');
378
+
379
+ return [
380
+ '<div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">',
381
+ renderTableSearchBar(table, state, activeColumn, filteredRowCount),
382
+ '<div class="custom-scrollbar flex-1 overflow-auto">',
383
+ gridMarkup,
384
+ emptyMarkup,
385
+ '</div>',
386
+ '<footer class="flex flex-wrap items-center justify-between gap-4 border-t border-outline-variant/10 bg-surface-container px-6 py-4">',
387
+ '<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">showing ',
388
+ escapeHtml(formatNumber(fromRow)),
389
+ '-',
390
+ escapeHtml(formatNumber(toRow)),
391
+ ' of ',
392
+ escapeHtml(formatNumber(totalRows)),
393
+ ' rows',
394
+ visibleRowsText,
395
+ '</div>',
396
+ '<div class="flex flex-wrap items-center gap-4"><div class="flex items-center gap-2">',
397
+ '<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">rows</span>',
398
+ '<div class="flex items-center gap-2">',
399
+ pageSizeButtons,
400
+ '</div></div>',
401
+ '<div class="flex items-center gap-2">',
402
+ '<button class="standard-button" data-action="set-data-page" data-page="',
403
+ page - 1,
404
+ '" type="button" ',
405
+ page <= 1 ? 'disabled' : '',
406
+ '>Prev</button>',
407
+ '<div class="min-w-[7rem] text-center text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">page ',
408
+ escapeHtml(formatNumber(page)),
409
+ ' / ',
410
+ escapeHtml(formatNumber(pageCount)),
411
+ '</div>',
412
+ '<button class="standard-button" data-action="set-data-page" data-page="',
413
+ page + 1,
414
+ '" type="button" ',
415
+ page >= pageCount ? 'disabled' : '',
416
+ '>Next</button>',
417
+ '</div></div></footer></div>',
418
+ ].join('');
435
419
  }
436
420
 
437
421
  export function renderDataRowEditorPanel(state) {
@@ -449,7 +433,36 @@ export function renderDataRowEditorPanel(state) {
449
433
  (foreignKey.mappings ?? []).map(mapping => String(mapping.from ?? '').trim()).filter(Boolean),
450
434
  ),
451
435
  );
452
- const getColumnBadges = column => (foreignKeyColumnNames.has(column.name) ? ['FK'] : []);
436
+ const getColumnTypeBadge = column => String(column.declaredType || column.affinity || 'BLOB').trim().toUpperCase();
437
+ const getColumnNumberInputMeta = column => {
438
+ const affinity = String(column.affinity ?? '').toUpperCase();
439
+
440
+ if (affinity === 'INTEGER') {
441
+ return { inputType: 'number', numberStep: '1' };
442
+ }
443
+
444
+ if (affinity === 'NUMERIC') {
445
+ return { inputType: 'number', numberStep: 'any' };
446
+ }
447
+
448
+ return {};
449
+ };
450
+ const getColumnBadges = column => {
451
+ const badges = [{ label: getColumnTypeBadge(column), tone: 'type' }];
452
+
453
+ if (column.primaryKeyPosition > 0) {
454
+ badges.push({
455
+ label: column.primaryKeyPosition > 1 ? `PK ${column.primaryKeyPosition}` : 'PK',
456
+ tone: 'primary-key',
457
+ });
458
+ }
459
+
460
+ if (foreignKeyColumnNames.has(column.name)) {
461
+ badges.push({ label: 'FK', tone: 'foreign-key' });
462
+ }
463
+
464
+ return badges;
465
+ };
453
466
 
454
467
  const identityColumns = table.identityStrategy?.type === 'primaryKey' ? (table.identityStrategy.columns ?? []) : [];
455
468
  const editableColumns = (table.columnMeta ?? []).filter(column => {
@@ -502,6 +515,7 @@ export function renderDataRowEditorPanel(state) {
502
515
  name: column.name,
503
516
  label: column.name,
504
517
  badges: getColumnBadges(column),
518
+ ...getColumnNumberInputMeta(column),
505
519
  value: value === null || value === undefined ? '' : String(value),
506
520
  };
507
521
  }),
@@ -265,6 +265,7 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
265
265
  error: state.editor.historyError,
266
266
  activeTab: state.editor.historyTab,
267
267
  search: state.editor.historySearchInput,
268
+ committedSearch: state.editor.historySearch,
268
269
  total: state.editor.historyTotal,
269
270
  hasMore: state.editor.historyHasMore,
270
271
  activeHistoryId: state.editor.historyActiveId,