diff --git a/src/app/(site)/prospects/escapePostgrestValue.test.ts b/src/app/(site)/prospects/escapePostgrestValue.test.ts new file mode 100644 index 0000000..1e8bc69 --- /dev/null +++ b/src/app/(site)/prospects/escapePostgrestValue.test.ts @@ -0,0 +1,17 @@ + +import { describe, expect, it } from "vitest"; +import { escapePostgrestValue } from "./savedProspectsData"; + +describe("escapePostgrestValue (#50)", () => { + it("neutralizes PostgREST grammar characters", () => { + expect(escapePostgrestValue("Foo (Bar)")).toBe("Foo Bar"); + expect(escapePostgrestValue("a.b")).toBe("a b"); + expect(escapePostgrestValue("x*y")).toBe("x y"); + expect(escapePostgrestValue('say "hi"')).toBe("say hi"); + expect(escapePostgrestValue("a,b")).toBe("a b"); + }); + + it("keeps plain company names intact", () => { + expect(escapePostgrestValue("Acme Corp")).toBe("Acme Corp"); + }); +}); diff --git a/src/app/(site)/prospects/savedProspectsData.ts b/src/app/(site)/prospects/savedProspectsData.ts index dba5d60..d58119d 100644 --- a/src/app/(site)/prospects/savedProspectsData.ts +++ b/src/app/(site)/prospects/savedProspectsData.ts @@ -15,17 +15,34 @@ export type SavedProspectsPage = { totalCount: number; }; + +/** + * Escape a value for PostgREST filter grammar used inside `or=(...)`. + * Strips/neutralizes characters that change expression structure. + */ +export function escapePostgrestValue(value: string): string { + return value + .replace(/\\/g, "") + .replace(/"/g, "") + .replace(/[(),.*]/g, " ") + .replace(/,/g, " ") + .replace(/\s+/g, " ") + .trim(); +} + function createBaseUrl() { return new URL("/rest/v1/saved_analyses", supabaseUrl); } function applyFilters(url: URL, query: SavedProspectsQuery) { if (query.search.trim()) { - const term = query.search.trim().replace(/,/g, " "); - url.searchParams.set( - "or", - `(title.ilike.*${term}*,domain.ilike.*${term}*,url.ilike.*${term}*)`, - ); + const term = escapePostgrestValue(query.search); + if (term) { + url.searchParams.set( + "or", + `(title.ilike.*${term}*,domain.ilike.*${term}*,url.ilike.*${term}*)`, + ); + } } if (query.status !== "all") {