sqlite-hub 1.2.0 → 1.4.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.
- package/README.md +106 -73
- package/bin/sqlite-hub.js +176 -2
- package/docs/API.md +122 -0
- package/docs/CLI.md +227 -0
- package/docs/DESIGN_GUIDELINES.md +45 -0
- package/docs/changelog.md +141 -0
- package/docs/shortkeys.md +27 -0
- package/docs/todo.md +16 -0
- package/examples/api/generate-types.js +52 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +18 -0
- package/frontend/js/app.js +118 -8
- package/frontend/js/components/connectionCard.js +1 -1
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/generateTypesDropdown.js +33 -0
- package/frontend/js/components/metricCard.js +1 -1
- package/frontend/js/components/modal.js +211 -18
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryHistoryDetail.js +3 -3
- package/frontend/js/components/queryHistoryHeader.js +1 -1
- package/frontend/js/components/queryHistoryPanel.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +13 -3
- package/frontend/js/router.js +28 -6
- package/frontend/js/store.js +422 -8
- package/frontend/js/views/backups.js +529 -37
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +1 -1
- package/frontend/js/views/documents.js +3 -3
- package/frontend/js/views/editor.js +6 -6
- package/frontend/js/views/mediaTagging.js +7 -5
- package/frontend/js/views/overview.js +3 -3
- package/frontend/js/views/settings.js +3 -3
- package/frontend/js/views/structure.js +6 -1
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +109 -232
- package/frontend/styles/structure-graph.css +32 -68
- package/frontend/styles/tailwind.css +8 -4
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +28 -21
- package/frontend/styles/views.css +94 -261
- package/package.json +18 -2
- package/server/routes/backups.js +18 -2
- package/server/routes/externalApi.js +36 -0
- package/server/routes/structure.js +22 -0
- package/server/services/databaseCommandService.js +14 -0
- package/server/services/sqlite/backupDiff.js +914 -0
- package/server/services/sqlite/backupService.js +77 -1
- package/server/services/sqlite/structureService.js +7 -0
- package/server/services/typeGenerationService.js +663 -0
- package/tailwind.config.cjs +0 -1
- package/.github/funding.yml +0 -2
- package/.github/workflows/ci.yml +0 -36
- package/database.sqlite +0 -0
- package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
- package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
- package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
- package/frontend/assets/mockups/connections_1200.webp +0 -0
- package/frontend/assets/mockups/data_1_1200.webp +0 -0
- package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
- package/frontend/assets/mockups/documents_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
- package/frontend/assets/mockups/overview_1200.webp +0 -0
- package/frontend/assets/mockups/settings_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
- package/frontend/assets/mockups/structure_1_1200.webp +0 -0
- package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
- package/tests/api-token-auth.test.js +0 -279
- package/tests/backup-manager.test.js +0 -140
- package/tests/backups-view.test.js +0 -70
- package/tests/charts-height-preset-storage.test.js +0 -60
- package/tests/charts-route-state.test.js +0 -144
- package/tests/check-constraint-options.test.js +0 -90
- package/tests/cli-args.test.js +0 -113
- package/tests/cli-service-delegation.test.js +0 -140
- package/tests/connection-removal.test.js +0 -52
- package/tests/connections-file-dialog-route.test.js +0 -89
- package/tests/copy-column-modal.test.js +0 -131
- package/tests/database-command-service.test.js +0 -174
- package/tests/database-documents.test.js +0 -85
- package/tests/documents-view.test.js +0 -132
- package/tests/dropdown-button.test.js +0 -101
- package/tests/email-preview.test.js +0 -89
- package/tests/export-blob.test.js +0 -99
- package/tests/export-filenames.test.js +0 -38
- package/tests/file-path-preview.test.js +0 -165
- package/tests/form-controls.test.js +0 -34
- package/tests/json-preview.test.js +0 -49
- package/tests/local-request-security.test.js +0 -85
- package/tests/markdown-documents.test.js +0 -103
- package/tests/native-file-dialog.test.js +0 -105
- package/tests/query-editor.test.js +0 -28
- package/tests/query-history-detail.test.js +0 -37
- package/tests/query-history-header.test.js +0 -30
- package/tests/query-results-truncation.test.js +0 -20
- package/tests/risky-sql.test.js +0 -30
- package/tests/row-editor-json.test.js +0 -82
- package/tests/row-editor-null-values.test.js +0 -184
- package/tests/row-editor-timestamp-preview.test.js +0 -192
- package/tests/security-paths.test.js +0 -84
- package/tests/settings-api-tokens-route.test.js +0 -97
- package/tests/settings-metadata.test.js +0 -114
- package/tests/settings-view.test.js +0 -108
- package/tests/sql-formatter.test.js +0 -173
- package/tests/sql-highlight.test.js +0 -38
- package/tests/sql-identifier-safety.test.js +0 -171
- package/tests/sql-result-limit.test.js +0 -66
- package/tests/structure-view.test.js +0 -60
- package/tests/table-designer-v2-unique-constraints.test.js +0 -78
- package/tests/table-scroll-state.test.js +0 -70
- package/tests/text-cell-stats.test.js +0 -38
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const path = require("node:path");
|
|
3
|
-
const { pathToFileURL } = require("node:url");
|
|
4
|
-
const test = require("node:test");
|
|
5
|
-
|
|
6
|
-
async function loadEmailPreviewModule() {
|
|
7
|
-
return import(pathToFileURL(path.resolve(__dirname, "../frontend/js/utils/emailPreview.js")).href);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async function loadDataViewModule() {
|
|
11
|
-
return import(pathToFileURL(path.resolve(__dirname, "../frontend/js/views/data.js")).href);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function loadRowEditorPanelModule() {
|
|
15
|
-
return import(pathToFileURL(path.resolve(__dirname, "../frontend/js/components/rowEditorPanel.js")).href);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
test("email preview detects simple email values only", async () => {
|
|
19
|
-
const { detectEmailValue } = await loadEmailPreviewModule();
|
|
20
|
-
|
|
21
|
-
assert.deepEqual(detectEmailValue("oli@example.com"), {
|
|
22
|
-
type: "email",
|
|
23
|
-
value: "oli@example.com",
|
|
24
|
-
localPart: "oli",
|
|
25
|
-
domain: "example.com",
|
|
26
|
-
});
|
|
27
|
-
assert.equal(detectEmailValue("https://example.com"), null);
|
|
28
|
-
assert.equal(detectEmailValue("not an email"), null);
|
|
29
|
-
assert.equal(detectEmailValue(null), null);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("data table renders an email icon for email cells", async () => {
|
|
33
|
-
const { renderDataView } = await loadDataViewModule();
|
|
34
|
-
const rendered = renderDataView({
|
|
35
|
-
connections: {
|
|
36
|
-
active: { readOnly: false },
|
|
37
|
-
},
|
|
38
|
-
dataBrowser: {
|
|
39
|
-
error: null,
|
|
40
|
-
loading: false,
|
|
41
|
-
page: 1,
|
|
42
|
-
pageSize: 50,
|
|
43
|
-
selectedRowIndex: null,
|
|
44
|
-
selectedTable: "contacts",
|
|
45
|
-
tableSearchQuery: "",
|
|
46
|
-
tables: [{ name: "contacts", columnCount: 2 }],
|
|
47
|
-
tablesVisible: true,
|
|
48
|
-
table: {
|
|
49
|
-
name: "contacts",
|
|
50
|
-
columns: ["id", "email"],
|
|
51
|
-
columnMeta: [
|
|
52
|
-
{ name: "id", visible: true, primaryKeyPosition: 1, affinity: "INTEGER" },
|
|
53
|
-
{ name: "email", visible: true, affinity: "TEXT" },
|
|
54
|
-
],
|
|
55
|
-
foreignKeys: [],
|
|
56
|
-
rows: [{ id: 1, email: "oli@example.com" }],
|
|
57
|
-
rowCount: 1,
|
|
58
|
-
page: 1,
|
|
59
|
-
pageCount: 1,
|
|
60
|
-
offset: 0,
|
|
61
|
-
limit: 50,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
}).main;
|
|
65
|
-
|
|
66
|
-
assert.match(rendered, /alternate_email/);
|
|
67
|
-
assert.match(rendered, /oli@example\.com/);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("row editor renders an email badge for email fields", async () => {
|
|
71
|
-
const { renderRowEditorPanel } = await loadRowEditorPanelModule();
|
|
72
|
-
const rendered = renderRowEditorPanel({
|
|
73
|
-
title: "contacts",
|
|
74
|
-
closeAction: "close",
|
|
75
|
-
formName: "save-data-row",
|
|
76
|
-
editableFields: [
|
|
77
|
-
{
|
|
78
|
-
name: "email",
|
|
79
|
-
label: "email",
|
|
80
|
-
badges: [{ label: "TEXT", tone: "type" }],
|
|
81
|
-
value: "oli@example.com",
|
|
82
|
-
rawValue: "oli@example.com",
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
assert.match(rendered, /EMAIL/);
|
|
88
|
-
assert.match(rendered, /data-row-editor-email-field/);
|
|
89
|
-
});
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
const Database = require("better-sqlite3");
|
|
2
|
-
const assert = require("node:assert/strict");
|
|
3
|
-
const parquet = require("parquetjs-lite");
|
|
4
|
-
const test = require("node:test");
|
|
5
|
-
const { ExportService } = require("../server/services/sqlite/exportService");
|
|
6
|
-
const { SqlExecutor } = require("../server/services/sqlite/sqlExecutor");
|
|
7
|
-
|
|
8
|
-
async function readParquetRows(buffer) {
|
|
9
|
-
const reader = await parquet.ParquetReader.openBuffer(buffer);
|
|
10
|
-
const cursor = reader.getCursor();
|
|
11
|
-
const rows = [];
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
let row;
|
|
15
|
-
|
|
16
|
-
while ((row = await cursor.next())) {
|
|
17
|
-
rows.push(row);
|
|
18
|
-
}
|
|
19
|
-
} finally {
|
|
20
|
-
await reader.close();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return rows;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
test("query and table exports include complete BLOB data", async () => {
|
|
27
|
-
const db = new Database(":memory:");
|
|
28
|
-
const blob = Buffer.from(Array.from({ length: 100 }, (_, index) => index));
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
db.exec("CREATE TABLE files (id INTEGER PRIMARY KEY, payload BLOB)");
|
|
32
|
-
db.prepare("INSERT INTO files (payload) VALUES (?)").run(blob);
|
|
33
|
-
|
|
34
|
-
const connection = { id: "blob-export-test", label: "Blob export" };
|
|
35
|
-
const connectionManager = {
|
|
36
|
-
getActiveConnection: () => connection,
|
|
37
|
-
getActiveDatabase: () => db,
|
|
38
|
-
};
|
|
39
|
-
const appStateStore = {
|
|
40
|
-
findQueryHistoryItemBySql: () => null,
|
|
41
|
-
getSettings: () => ({ csvDelimiter: "," }),
|
|
42
|
-
recordQueryExecution: () => 1,
|
|
43
|
-
};
|
|
44
|
-
const sqlExecutor = new SqlExecutor({ connectionManager, appStateStore });
|
|
45
|
-
const exportService = new ExportService({
|
|
46
|
-
appStateStore,
|
|
47
|
-
connectionManager,
|
|
48
|
-
sqlExecutor,
|
|
49
|
-
});
|
|
50
|
-
const expectedBase64 = blob.toString("base64");
|
|
51
|
-
const queryExport = exportService.exportQuery(
|
|
52
|
-
"SELECT payload FROM files ORDER BY id",
|
|
53
|
-
{ format: "csv" }
|
|
54
|
-
);
|
|
55
|
-
const queryJsonExport = exportService.exportQuery(
|
|
56
|
-
"SELECT payload FROM files ORDER BY id",
|
|
57
|
-
{ format: "json" }
|
|
58
|
-
);
|
|
59
|
-
const tableExport = exportService.exportTable("files", { format: "tsv" });
|
|
60
|
-
const jsonExport = exportService.exportTable("files", { format: "json" });
|
|
61
|
-
const queryParquetExport = await exportService.exportQueryDownload(
|
|
62
|
-
"SELECT id, payload FROM files ORDER BY id",
|
|
63
|
-
{ format: "parquet" }
|
|
64
|
-
);
|
|
65
|
-
const tableParquetExport = await exportService.exportTableDownload("files", { format: "parquet" });
|
|
66
|
-
|
|
67
|
-
for (const content of [queryExport.content, tableExport.content]) {
|
|
68
|
-
assert.equal(content.includes(expectedBase64), true);
|
|
69
|
-
assert.match(content, /""encoding"":""base64""/);
|
|
70
|
-
assert.doesNotMatch(content, /base64Preview|hexPreview/);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
assert.equal(jsonExport.mimeType, "application/json; charset=utf-8");
|
|
74
|
-
assert.equal(jsonExport.filename, "files.json");
|
|
75
|
-
|
|
76
|
-
const [queryJsonRow] = JSON.parse(queryJsonExport.content);
|
|
77
|
-
const [jsonRow] = JSON.parse(jsonExport.content);
|
|
78
|
-
assert.equal(queryJsonExport.mimeType, "application/json; charset=utf-8");
|
|
79
|
-
assert.equal(queryJsonRow.payload.encoding, "base64");
|
|
80
|
-
assert.equal(queryJsonRow.payload.data, expectedBase64);
|
|
81
|
-
assert.equal(jsonRow.payload.encoding, "base64");
|
|
82
|
-
assert.equal(jsonRow.payload.data, expectedBase64);
|
|
83
|
-
assert.equal(jsonRow.payload.sizeBytes, blob.length);
|
|
84
|
-
assert.equal(jsonExport.content.includes("base64Preview"), false);
|
|
85
|
-
assert.equal(jsonExport.content.includes("hexPreview"), false);
|
|
86
|
-
|
|
87
|
-
assert.equal(queryParquetExport.mimeType, "application/vnd.apache.parquet");
|
|
88
|
-
assert.equal(queryParquetExport.filename.endsWith(".parquet"), true);
|
|
89
|
-
assert.equal(Buffer.isBuffer(queryParquetExport.content), true);
|
|
90
|
-
assert.equal(tableParquetExport.filename, "files.parquet");
|
|
91
|
-
|
|
92
|
-
const [queryParquetRow] = await readParquetRows(queryParquetExport.content);
|
|
93
|
-
const [tableParquetRow] = await readParquetRows(tableParquetExport.content);
|
|
94
|
-
assert.equal(queryParquetRow.payload.equals(blob), true);
|
|
95
|
-
assert.equal(tableParquetRow.payload.equals(blob), true);
|
|
96
|
-
} finally {
|
|
97
|
-
db.close();
|
|
98
|
-
}
|
|
99
|
-
});
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const { readFileSync } = require("node:fs");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
const test = require("node:test");
|
|
5
|
-
|
|
6
|
-
let exportFilenamesModulePromise = null;
|
|
7
|
-
|
|
8
|
-
function loadExportFilenamesModule() {
|
|
9
|
-
if (!exportFilenamesModulePromise) {
|
|
10
|
-
const source = readFileSync(
|
|
11
|
-
path.resolve(__dirname, "../frontend/js/utils/exportFilenames.js"),
|
|
12
|
-
"utf8"
|
|
13
|
-
);
|
|
14
|
-
const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
|
|
15
|
-
|
|
16
|
-
exportFilenamesModulePromise = import(url);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return exportFilenamesModulePromise;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test("text export filenames use the selected format extension", async () => {
|
|
23
|
-
const { buildTextExportFilename } = await loadExportFilenamesModule();
|
|
24
|
-
|
|
25
|
-
assert.equal(buildTextExportFilename("query-results.csv", { format: "tsv" }), "query-results.tsv");
|
|
26
|
-
assert.equal(buildTextExportFilename("white_house_live_streams", { format: "md" }), "white_house_live_streams.md");
|
|
27
|
-
assert.equal(buildTextExportFilename("companies.tsv", { format: "json" }), "companies.json");
|
|
28
|
-
assert.equal(buildTextExportFilename("companies.json", { format: "parquet" }), "companies.parquet");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("text export filenames sanitize unsafe names and fallback when empty", async () => {
|
|
32
|
-
const { buildTextExportFilename } = await loadExportFilenamesModule();
|
|
33
|
-
|
|
34
|
-
assert.equal(buildTextExportFilename("", { format: "csv", fallback: "table" }), "table.csv");
|
|
35
|
-
assert.equal(buildTextExportFilename("../bad:name?.csv", { format: "csv", fallback: "table" }), "bad name.csv");
|
|
36
|
-
assert.equal(buildTextExportFilename("bad:name?.json", { format: "json", fallback: "table" }), "bad name.json");
|
|
37
|
-
assert.equal(buildTextExportFilename("bad:name?.parquet", { format: "parquet", fallback: "table" }), "bad name.parquet");
|
|
38
|
-
});
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const { readFileSync } = require("node:fs");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
const test = require("node:test");
|
|
5
|
-
|
|
6
|
-
let filePathPreviewModulePromise = null;
|
|
7
|
-
|
|
8
|
-
function loadFilePathPreviewModule() {
|
|
9
|
-
if (!filePathPreviewModulePromise) {
|
|
10
|
-
const timestampPreviewSource = readFileSync(
|
|
11
|
-
path.resolve(__dirname, "../frontend/js/utils/timestampPreview.js"),
|
|
12
|
-
"utf8"
|
|
13
|
-
);
|
|
14
|
-
const timestampPreviewUrl = `data:text/javascript;base64,${Buffer.from(timestampPreviewSource).toString("base64")}`;
|
|
15
|
-
const filePathPreviewSource = readFileSync(
|
|
16
|
-
path.resolve(__dirname, "../frontend/js/utils/filePathPreview.js"),
|
|
17
|
-
"utf8"
|
|
18
|
-
).replace(
|
|
19
|
-
'import { isProtectedKeyColumn } from "./timestampPreview.js";',
|
|
20
|
-
`import { isProtectedKeyColumn } from "${timestampPreviewUrl}";`
|
|
21
|
-
);
|
|
22
|
-
const filePathPreviewUrl = `data:text/javascript;base64,${Buffer.from(filePathPreviewSource).toString("base64")}`;
|
|
23
|
-
|
|
24
|
-
filePathPreviewModulePromise = import(filePathPreviewUrl);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return filePathPreviewModulePromise;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function createTableMeta() {
|
|
31
|
-
return {
|
|
32
|
-
columns: [
|
|
33
|
-
{ name: "id", primaryKeyPosition: 1 },
|
|
34
|
-
{ name: "parent_id", primaryKeyPosition: 0 },
|
|
35
|
-
{ name: "file_path", primaryKeyPosition: 0 },
|
|
36
|
-
{ name: "filename", primaryKeyPosition: 0 },
|
|
37
|
-
{ name: "note", primaryKeyPosition: 0 },
|
|
38
|
-
],
|
|
39
|
-
foreignKeys: [
|
|
40
|
-
{
|
|
41
|
-
mappings: [{ from: "parent_id", to: "id" }],
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
test("filepath preview detects supported path styles", async () => {
|
|
48
|
-
const { detectFilePathValue } = await loadFilePathPreviewModule();
|
|
49
|
-
const tableMeta = createTableMeta();
|
|
50
|
-
const cases = [
|
|
51
|
-
{
|
|
52
|
-
value: "/Users/oli/project/file.txt",
|
|
53
|
-
pathType: "unix",
|
|
54
|
-
fileName: "file.txt",
|
|
55
|
-
directory: "/Users/oli/project",
|
|
56
|
-
extension: "txt",
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
value: "/home/oli/data/app.sqlite",
|
|
60
|
-
pathType: "unix",
|
|
61
|
-
fileName: "app.sqlite",
|
|
62
|
-
directory: "/home/oli/data",
|
|
63
|
-
extension: "sqlite",
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
value: "C:\\Users\\Oliver\\Desktop\\file.pdf",
|
|
67
|
-
pathType: "windows",
|
|
68
|
-
fileName: "file.pdf",
|
|
69
|
-
directory: "C:\\Users\\Oliver\\Desktop",
|
|
70
|
-
extension: "pdf",
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
value: "./data/export.csv",
|
|
74
|
-
pathType: "relative",
|
|
75
|
-
fileName: "export.csv",
|
|
76
|
-
directory: "./data",
|
|
77
|
-
extension: "csv",
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
value: "../logs/app.log",
|
|
81
|
-
pathType: "relative",
|
|
82
|
-
fileName: "app.log",
|
|
83
|
-
directory: "../logs",
|
|
84
|
-
extension: "log",
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
value: "~/Documents/report.md",
|
|
88
|
-
pathType: "home",
|
|
89
|
-
fileName: "report.md",
|
|
90
|
-
directory: "~/Documents",
|
|
91
|
-
extension: "md",
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
value: "data/images/avatar.png",
|
|
95
|
-
pathType: "relative",
|
|
96
|
-
fileName: "avatar.png",
|
|
97
|
-
directory: "data/images",
|
|
98
|
-
extension: "png",
|
|
99
|
-
},
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
for (const item of cases) {
|
|
103
|
-
const preview = detectFilePathValue(item.value, "file_path", tableMeta);
|
|
104
|
-
|
|
105
|
-
assert.equal(preview?.type, "filepath");
|
|
106
|
-
assert.equal(preview.pathType, item.pathType);
|
|
107
|
-
assert.equal(preview.fileName, item.fileName);
|
|
108
|
-
assert.equal(preview.directory, item.directory);
|
|
109
|
-
assert.equal(preview.extension, item.extension);
|
|
110
|
-
assert.ok(preview.confidence >= 0.7);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("filepath preview does not detect URLs, primitive text, JSON, emails, or weak filenames", async () => {
|
|
115
|
-
const { detectFilePathValue } = await loadFilePathPreviewModule();
|
|
116
|
-
const tableMeta = createTableMeta();
|
|
117
|
-
const rejectedValues = [
|
|
118
|
-
"https://example.com/file.txt",
|
|
119
|
-
"http://localhost:3000/test",
|
|
120
|
-
"mailto:oliver@example.com",
|
|
121
|
-
"tel:+431234567",
|
|
122
|
-
"ftp://example.com/file.txt",
|
|
123
|
-
"oliver@example.com",
|
|
124
|
-
"1717682400",
|
|
125
|
-
"true",
|
|
126
|
-
"false",
|
|
127
|
-
'{"path":"/tmp/file.txt"}',
|
|
128
|
-
"hello world",
|
|
129
|
-
"file",
|
|
130
|
-
"invoice.pdf",
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
for (const value of rejectedValues) {
|
|
134
|
-
assert.equal(detectFilePathValue(value, "note", tableMeta), null);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("filepath preview protects primary keys and foreign keys", async () => {
|
|
139
|
-
const { detectFilePathValue } = await loadFilePathPreviewModule();
|
|
140
|
-
const tableMeta = createTableMeta();
|
|
141
|
-
|
|
142
|
-
assert.equal(detectFilePathValue("/Users/oli/project/file.txt", "id", tableMeta), null);
|
|
143
|
-
assert.equal(detectFilePathValue("/Users/oli/project/file.txt", "parent_id", tableMeta), null);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("filepath preview allows bare filenames only for strong file columns", async () => {
|
|
147
|
-
const { detectFilePathValue } = await loadFilePathPreviewModule();
|
|
148
|
-
const tableMeta = createTableMeta();
|
|
149
|
-
|
|
150
|
-
assert.equal(detectFilePathValue("invoice.pdf", "note", tableMeta), null);
|
|
151
|
-
|
|
152
|
-
const preview = detectFilePathValue("invoice.pdf", "filename", tableMeta);
|
|
153
|
-
|
|
154
|
-
assert.equal(preview?.pathType, "relative");
|
|
155
|
-
assert.equal(preview.fileName, "invoice.pdf");
|
|
156
|
-
assert.equal(preview.directory, null);
|
|
157
|
-
assert.equal(preview.extension, "pdf");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("filepath preview compacts long paths while preserving filename", async () => {
|
|
161
|
-
const { compactPathForDisplay } = await loadFilePathPreviewModule();
|
|
162
|
-
const display = compactPathForDisplay("/Users/oli/projects/sqlite-hub/data/app.sqlite", 24);
|
|
163
|
-
|
|
164
|
-
assert.equal(display, ".../data/app.sqlite");
|
|
165
|
-
});
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
const assert = require('node:assert/strict');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const { pathToFileURL } = require('node:url');
|
|
4
|
-
const test = require('node:test');
|
|
5
|
-
|
|
6
|
-
let formControlsModulePromise = null;
|
|
7
|
-
|
|
8
|
-
function loadFormControlsModule() {
|
|
9
|
-
if (!formControlsModulePromise) {
|
|
10
|
-
formControlsModulePromise = import(
|
|
11
|
-
pathToFileURL(path.resolve(__dirname, '../frontend/js/components/formControls.js')).href
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return formControlsModulePromise;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
test('standard text input uses the shared application styling', async () => {
|
|
19
|
-
const { renderTextInput } = await loadFormControlsModule();
|
|
20
|
-
const markup = renderTextInput({
|
|
21
|
-
className: 'flex-1',
|
|
22
|
-
dataAttributes: { tokenName: true },
|
|
23
|
-
maxlength: 80,
|
|
24
|
-
placeholder: 'Token name',
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
assert.match(markup, /control-input/);
|
|
28
|
-
assert.match(markup, /border-outline-variant\/20/);
|
|
29
|
-
assert.match(markup, /bg-surface-container-lowest/);
|
|
30
|
-
assert.match(markup, /placeholder:text-on-surface-variant\/35/);
|
|
31
|
-
assert.match(markup, /focus:border-primary-container/);
|
|
32
|
-
assert.match(markup, /data-token-name/);
|
|
33
|
-
assert.match(markup, /maxlength="80"/);
|
|
34
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const { readFileSync } = require("node:fs");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
const test = require("node:test");
|
|
5
|
-
|
|
6
|
-
let jsonPreviewModulePromise = null;
|
|
7
|
-
|
|
8
|
-
function loadJsonPreviewModule() {
|
|
9
|
-
if (!jsonPreviewModulePromise) {
|
|
10
|
-
const source = readFileSync(
|
|
11
|
-
path.resolve(__dirname, "../frontend/js/utils/jsonPreview.js"),
|
|
12
|
-
"utf8"
|
|
13
|
-
);
|
|
14
|
-
const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
|
|
15
|
-
|
|
16
|
-
jsonPreviewModulePromise = import(url);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return jsonPreviewModulePromise;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test("JSON preview formats objects and arrays with line breaks and indentation", async () => {
|
|
23
|
-
const { formatJsonPreview } = await loadJsonPreviewModule();
|
|
24
|
-
|
|
25
|
-
assert.equal(
|
|
26
|
-
formatJsonPreview('{"event":"created","metadata":{"source":"api"}}'),
|
|
27
|
-
[
|
|
28
|
-
"{",
|
|
29
|
-
' "event": "created",',
|
|
30
|
-
' "metadata": {',
|
|
31
|
-
' "source": "api"',
|
|
32
|
-
" }",
|
|
33
|
-
"}",
|
|
34
|
-
].join("\n")
|
|
35
|
-
);
|
|
36
|
-
assert.equal(
|
|
37
|
-
formatJsonPreview([1, { active: true }]),
|
|
38
|
-
["[", " 1,", " {", ' "active": true', " }", "]"].join("\n")
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("JSON preview ignores invalid JSON and scalar values", async () => {
|
|
43
|
-
const { formatJsonPreview } = await loadJsonPreviewModule();
|
|
44
|
-
|
|
45
|
-
assert.equal(formatJsonPreview("plain text"), null);
|
|
46
|
-
assert.equal(formatJsonPreview("{invalid"), null);
|
|
47
|
-
assert.equal(formatJsonPreview("42"), null);
|
|
48
|
-
assert.equal(formatJsonPreview(null), null);
|
|
49
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const test = require("node:test");
|
|
3
|
-
const {
|
|
4
|
-
LOOPBACK_HOST,
|
|
5
|
-
listenOnLoopback,
|
|
6
|
-
localRequestSecurity,
|
|
7
|
-
} = require("../server/middleware/localRequestSecurity");
|
|
8
|
-
|
|
9
|
-
function runMiddleware({ method = "GET", protocol = "http", headers = {} } = {}) {
|
|
10
|
-
const normalizedHeaders = Object.fromEntries(
|
|
11
|
-
Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value])
|
|
12
|
-
);
|
|
13
|
-
const req = {
|
|
14
|
-
method,
|
|
15
|
-
protocol,
|
|
16
|
-
get(name) {
|
|
17
|
-
return normalizedHeaders[String(name).toLowerCase()];
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
return new Promise((resolve) => {
|
|
22
|
-
localRequestSecurity(req, {}, (error) => resolve(error ?? null));
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
test("server listener binds explicitly to the IPv4 loopback address", () => {
|
|
27
|
-
let receivedArguments = null;
|
|
28
|
-
const listener = {};
|
|
29
|
-
const app = {
|
|
30
|
-
listen(...args) {
|
|
31
|
-
receivedArguments = args;
|
|
32
|
-
return listener;
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
assert.equal(listenOnLoopback(app, 4173), listener);
|
|
37
|
-
assert.deepEqual(receivedArguments, [4173, LOOPBACK_HOST]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("local API security accepts same-origin browser and CLI requests", async () => {
|
|
41
|
-
assert.equal(
|
|
42
|
-
await runMiddleware({
|
|
43
|
-
method: "POST",
|
|
44
|
-
headers: {
|
|
45
|
-
host: "127.0.0.1:4173",
|
|
46
|
-
origin: "http://127.0.0.1:4173",
|
|
47
|
-
"sec-fetch-site": "same-origin",
|
|
48
|
-
},
|
|
49
|
-
}),
|
|
50
|
-
null
|
|
51
|
-
);
|
|
52
|
-
assert.equal(
|
|
53
|
-
await runMiddleware({
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers: { host: "localhost:4173" },
|
|
56
|
-
}),
|
|
57
|
-
null
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("local API security rejects foreign hosts and cross-origin mutations", async () => {
|
|
62
|
-
const foreignHostError = await runMiddleware({
|
|
63
|
-
headers: { host: "example.test:4173" },
|
|
64
|
-
});
|
|
65
|
-
const crossOriginError = await runMiddleware({
|
|
66
|
-
method: "POST",
|
|
67
|
-
headers: {
|
|
68
|
-
host: "127.0.0.1:4173",
|
|
69
|
-
origin: "https://example.test",
|
|
70
|
-
"sec-fetch-site": "cross-site",
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
const wrongSchemeError = await runMiddleware({
|
|
74
|
-
method: "PATCH",
|
|
75
|
-
headers: {
|
|
76
|
-
host: "localhost:4173",
|
|
77
|
-
origin: "https://localhost:4173",
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
for (const error of [foreignHostError, crossOriginError, wrongSchemeError]) {
|
|
82
|
-
assert.equal(error?.statusCode, 403);
|
|
83
|
-
assert.equal(error?.code, "LOCAL_REQUEST_REQUIRED");
|
|
84
|
-
}
|
|
85
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const path = require("node:path");
|
|
3
|
-
const { pathToFileURL } = require("node:url");
|
|
4
|
-
const test = require("node:test");
|
|
5
|
-
|
|
6
|
-
let markdownModulePromise = null;
|
|
7
|
-
|
|
8
|
-
function loadMarkdownModule() {
|
|
9
|
-
if (!markdownModulePromise) {
|
|
10
|
-
markdownModulePromise = import(
|
|
11
|
-
pathToFileURL(path.resolve(__dirname, "../frontend/js/utils/markdownDocuments.js")).href
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return markdownModulePromise;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
test("markdown preview renders clickable todo checkboxes with source line indexes", async () => {
|
|
19
|
-
const { renderMarkdownPreview } = await loadMarkdownModule();
|
|
20
|
-
const html = renderMarkdownPreview(["# Tasks", "- [ ] item1", "- [x] item2", "```", "- [ ] code", "```"].join("\n"));
|
|
21
|
-
|
|
22
|
-
assert.match(html, /document-markdown-task-list/);
|
|
23
|
-
assert.match(html, /data-action="toggle-document-todo"/);
|
|
24
|
-
assert.match(html, /data-line-index="1"/);
|
|
25
|
-
assert.match(html, /data-line-index="2"/);
|
|
26
|
-
assert.doesNotMatch(html, /data-line-index="4"/);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("markdown todo toggle updates only real task lines", async () => {
|
|
30
|
-
const { toggleMarkdownTodoLine } = await loadMarkdownModule();
|
|
31
|
-
const markdown = ["- [ ] item1", "```", "- [ ] code", "```", "- [x] item2"].join("\n");
|
|
32
|
-
|
|
33
|
-
assert.equal(toggleMarkdownTodoLine(markdown, 0).split("\n")[0], "- [x] item1");
|
|
34
|
-
assert.equal(toggleMarkdownTodoLine(markdown, 2), markdown);
|
|
35
|
-
assert.equal(toggleMarkdownTodoLine(markdown, 4).split("\n")[4], "- [ ] item2");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("markdown preview fallback renders unordered and ordered lists", async () => {
|
|
39
|
-
const { renderMarkdownPreview } = await loadMarkdownModule();
|
|
40
|
-
const previousMarked = globalThis.marked;
|
|
41
|
-
|
|
42
|
-
globalThis.marked = undefined;
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const html = renderMarkdownPreview(["- alpha", "- beta", "", "1. first", "2. second"].join("\n"));
|
|
46
|
-
|
|
47
|
-
assert.match(html, /<ul><li>alpha<\/li><li>beta<\/li><\/ul>/);
|
|
48
|
-
assert.match(html, /<ol><li>first<\/li><li>second<\/li><\/ol>/);
|
|
49
|
-
} finally {
|
|
50
|
-
globalThis.marked = previousMarked;
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("markdown preview escapes raw html content", async () => {
|
|
55
|
-
const { renderMarkdownPreview } = await loadMarkdownModule();
|
|
56
|
-
const html = renderMarkdownPreview('<img src=x onerror="alert(1)">\n- [ ] <script>alert(1)</script>');
|
|
57
|
-
|
|
58
|
-
assert.doesNotMatch(html, /<script>/i);
|
|
59
|
-
assert.doesNotMatch(html, /<img/i);
|
|
60
|
-
assert.match(html, /<script>/);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("markdown preview keeps JSON code block quotes readable", async () => {
|
|
64
|
-
const { renderMarkdownPreview } = await loadMarkdownModule();
|
|
65
|
-
const previousMarked = globalThis.marked;
|
|
66
|
-
let parsedSource = "";
|
|
67
|
-
|
|
68
|
-
globalThis.marked = {
|
|
69
|
-
parse: source => {
|
|
70
|
-
parsedSource = source;
|
|
71
|
-
return `<pre><code>${source}</code></pre>`;
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const html = renderMarkdownPreview(['<img src=x onerror="alert(1)">', '', '```json', '{"id":"abc"}', '```'].join("\n"));
|
|
77
|
-
|
|
78
|
-
assert.match(parsedSource, /<img/);
|
|
79
|
-
assert.match(parsedSource, /"id":"abc"/);
|
|
80
|
-
assert.doesNotMatch(parsedSource, /"id"/);
|
|
81
|
-
assert.match(html, /"id":"abc"/);
|
|
82
|
-
} finally {
|
|
83
|
-
globalThis.marked = previousMarked;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("markdown preview strips executable link targets from rendered markdown", async () => {
|
|
88
|
-
const { renderMarkdownPreview } = await loadMarkdownModule();
|
|
89
|
-
const previousMarked = globalThis.marked;
|
|
90
|
-
|
|
91
|
-
globalThis.marked = {
|
|
92
|
-
parse: () => '<p><a href="javascript:alert(1)">unsafe</a></p>',
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const html = renderMarkdownPreview("[unsafe](javascript:alert(1))");
|
|
97
|
-
|
|
98
|
-
assert.doesNotMatch(html, /href="javascript:/i);
|
|
99
|
-
assert.match(html, /href="#"/);
|
|
100
|
-
} finally {
|
|
101
|
-
globalThis.marked = previousMarked;
|
|
102
|
-
}
|
|
103
|
-
});
|