diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index edaeeea0..ac5b1c32 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -60,6 +60,7 @@ import { exportMermaid } from "./erd/mermaid"; import { inferRelationships } from "./erd/autoInfer"; import { exportDbml } from "./erd/dbml"; import { GRID_COLUMNS, GRID_X_GAP, GRID_Y_GAP } from "./erd/layoutConstants"; +import { tableNodeMatchesSearch } from "./erd/nodeSearch"; import type { Connection, Project, Snapshot, SnapshotDetail } from "./types"; const TERMINAL_SNAPSHOT_STATUSES = new Set([ @@ -195,18 +196,7 @@ export default function App() { if (!normalizedNodeSearch) return new Set(); const matches = new Set(); for (const node of nodes) { - const haystack = [ - node.data.title, - node.data.comment ?? "", - ...node.data.columns.flatMap((column) => [ - column.column_name, - column.data_type, - column.column_comment ?? "", - ]), - ] - .join(" ") - .toLocaleLowerCase(); - if (haystack.includes(normalizedNodeSearch)) { + if (tableNodeMatchesSearch(node, normalizedNodeSearch)) { matches.add(node.id); } } diff --git a/frontend/src/erd/__tests__/nodeSearch.test.ts b/frontend/src/erd/__tests__/nodeSearch.test.ts new file mode 100644 index 00000000..f0755da1 --- /dev/null +++ b/frontend/src/erd/__tests__/nodeSearch.test.ts @@ -0,0 +1,40 @@ +import type { Node } from "@xyflow/react"; +import { describe, expect, it } from "vitest"; + +import type { TableNodeData } from "../convert"; +import { tableNodeMatchesSearch } from "../nodeSearch"; + +const node: Node = { + id: "users", + type: "tableNode", + position: { x: 0, y: 0 }, + data: { + title: "public.users", + comment: "Application accounts", + columns: [ + { + column_name: "email_address", + data_type: "varchar", + is_not_null: true, + is_pk: false, + column_comment: "Primary login address", + }, + ], + badges: { pk: false, fk: false }, + }, +}; + +describe("tableNodeMatchesSearch", () => { + it("matches title, table comments, and column metadata without joined haystacks", () => { + expect(tableNodeMatchesSearch(node, "users")).toBe(true); + expect(tableNodeMatchesSearch(node, "accounts")).toBe(true); + expect(tableNodeMatchesSearch(node, "email")).toBe(true); + expect(tableNodeMatchesSearch(node, "varchar")).toBe(true); + expect(tableNodeMatchesSearch(node, "login")).toBe(true); + }); + + it("returns false for empty or missing matches", () => { + expect(tableNodeMatchesSearch(node, "")).toBe(false); + expect(tableNodeMatchesSearch(node, "orders")).toBe(false); + }); +}); diff --git a/frontend/src/erd/nodeSearch.ts b/frontend/src/erd/nodeSearch.ts new file mode 100644 index 00000000..7020aab4 --- /dev/null +++ b/frontend/src/erd/nodeSearch.ts @@ -0,0 +1,36 @@ +import type { Node } from "@xyflow/react"; + +import type { TableNodeData } from "./convert"; + +export function tableNodeMatchesSearch( + node: Node, + normalizedSearch: string, +): boolean { + if (!normalizedSearch) return false; + const { data } = node; + + if (data.title.toLocaleLowerCase().includes(normalizedSearch)) return true; + if ( + data.comment && + data.comment.toLocaleLowerCase().includes(normalizedSearch) + ) { + return true; + } + + for (const column of data.columns) { + if (column.column_name.toLocaleLowerCase().includes(normalizedSearch)) { + return true; + } + if (column.data_type.toLocaleLowerCase().includes(normalizedSearch)) { + return true; + } + if ( + column.column_comment && + column.column_comment.toLocaleLowerCase().includes(normalizedSearch) + ) { + return true; + } + } + + return false; +}