From 30a17d5e678bfe56749ec2b4d6b764877b777436 Mon Sep 17 00:00:00 2001 From: Ashay Date: Tue, 11 Aug 2026 09:38:01 +0530 Subject: [PATCH] fix: escape PostgREST filter values in prospect search Neutralize grammar characters in search terms so company names like Foo (Bar) cannot rewrite the or() expression. --- .../prospects/escapePostgrestValue.test.ts | 17 ++++++++++++ .../(site)/prospects/savedProspectsData.ts | 27 +++++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 src/app/(site)/prospects/escapePostgrestValue.test.ts 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") {