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.
- package/.github/workflows/ci.yml +36 -0
- package/README.md +2 -2
- package/bin/sqlite-hub.js +1 -1
- package/frontend/index.html +3 -158
- package/frontend/js/app.js +231 -5
- package/frontend/js/components/connectionCard.js +62 -87
- package/frontend/js/components/emptyState.js +20 -23
- package/frontend/js/components/modal.js +158 -199
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryEditor.js +16 -30
- package/frontend/js/components/queryHistoryDetail.js +93 -164
- package/frontend/js/components/queryHistoryPanel.js +90 -100
- package/frontend/js/components/queryResults.js +3 -1
- package/frontend/js/components/rowEditorPanel.js +76 -56
- package/frontend/js/components/tableDesignerEditor.js +91 -116
- package/frontend/js/lib/queryChartOptions.js +5 -1
- package/frontend/js/lib/queryCharts.js +50 -2
- package/frontend/js/store.js +161 -23
- package/frontend/js/utils/tableDesigner.js +8 -2
- package/frontend/js/views/charts.js +126 -73
- package/frontend/js/views/data.js +147 -133
- package/frontend/js/views/editor.js +1 -0
- package/frontend/js/views/mediaTagging.js +131 -164
- package/frontend/js/views/structure.js +52 -48
- package/frontend/styles/base.css +57 -13
- package/frontend/styles/components.css +166 -96
- package/frontend/styles/layout.css +1 -1
- package/frontend/styles/structure-graph.css +11 -11
- package/frontend/styles/tailwind.css +81 -0
- package/frontend/styles/tailwind.generated.css +1 -0
- package/frontend/styles/tokens.css +50 -7
- package/frontend/styles/utilities.css +21 -0
- package/frontend/styles/views.css +94 -86
- package/package.json +16 -3
- package/server/routes/mediaTagging.js +2 -10
- package/server/routes/sql.js +35 -10
- package/server/server.js +24 -0
- package/server/services/sqlite/dataBrowserService.js +25 -5
- package/server/services/sqlite/exportService.js +4 -2
- package/server/services/sqlite/introspection.js +2 -2
- package/server/services/sqlite/mediaTaggingService.js +166 -53
- package/server/services/sqlite/structureService.js +2 -2
- package/server/services/sqlite/tableDesigner/sql.js +19 -3
- package/server/services/storage/appStateStore.js +227 -87
- package/server/services/storage/queryHistoryChartUtils.js +19 -2
- package/server/utils/appPaths.js +55 -19
- package/server/utils/fileValidation.js +94 -8
- package/tailwind.config.cjs +73 -0
- package/tests/security-paths.test.js +84 -0
- package/tests/sql-identifier-safety.test.js +66 -0
- package/.npmingnore +0 -4
- package/changelog.md +0 -77
- package/docs/DESIGN_GUIDELINES.md +0 -36
- package/scripts/publish_brew.sh +0 -466
- package/scripts/publish_npm.sh +0 -241
- 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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
class="control-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
</
|
|
384
|
-
<div class="flex
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
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,
|