From 235fb13ba385e8a4e0ae237a67d6acfa99dc63de Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sat, 25 Oct 2025 21:40:35 -0700 Subject: [PATCH] Add fulltext search to GET /partners --- apps/web/app/(ee)/api/partners/count/route.ts | 17 +++++++++++----- .../(ee)/program/partners/partners-table.tsx | 2 +- apps/web/lib/api/partners/get-partners.ts | 20 ++++++++++--------- packages/prisma/index.ts | 6 ++++++ packages/prisma/schema/campaign.prisma | 1 - packages/prisma/schema/partner.prisma | 3 +++ 6 files changed, 33 insertions(+), 16 deletions(-) diff --git a/apps/web/app/(ee)/api/partners/count/route.ts b/apps/web/app/(ee)/api/partners/count/route.ts index 8ec013c38d8..8c2db99bd27 100644 --- a/apps/web/app/(ee)/api/partners/count/route.ts +++ b/apps/web/app/(ee)/api/partners/count/route.ts @@ -1,7 +1,7 @@ import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw"; import { withWorkspace } from "@/lib/auth"; import { partnersCountQuerySchema } from "@/lib/zod/schemas/partners"; -import { prisma } from "@dub/prisma"; +import { prisma, sanitizeFullTextSearch } from "@dub/prisma"; import { Prisma, ProgramEnrollmentStatus } from "@dub/prisma/client"; import { NextResponse } from "next/server"; @@ -10,13 +10,20 @@ export const GET = withWorkspace( async ({ workspace, searchParams }) => { const programId = getDefaultProgramIdOrThrow(workspace); - const { groupBy, status, country, search, partnerIds, groupId } = + const { groupBy, status, country, search, email, partnerIds, groupId } = partnersCountQuerySchema.parse(searchParams); const commonWhere: Prisma.PartnerWhereInput = { - ...(search && { - OR: [{ name: { contains: search } }, { email: { contains: search } }], - }), + ...(email + ? { email } + : search + ? search.includes("@") + ? { email: search } + : { + email: { search: sanitizeFullTextSearch(search) }, + name: { search: sanitizeFullTextSearch(search) }, + } + : {}), ...(partnerIds && { id: { in: partnerIds }, }), diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx index 89b7a780db3..46f87b2b18d 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx @@ -411,7 +411,7 @@ export function PartnersTable() { onRemove={onRemove} /> diff --git a/apps/web/lib/api/partners/get-partners.ts b/apps/web/lib/api/partners/get-partners.ts index 0710c427847..20d1d41b788 100644 --- a/apps/web/lib/api/partners/get-partners.ts +++ b/apps/web/lib/api/partners/get-partners.ts @@ -1,5 +1,5 @@ import { getPartnersQuerySchemaExtended } from "@/lib/zod/schemas/partners"; -import { prisma } from "@dub/prisma"; +import { prisma, sanitizeFullTextSearch } from "@dub/prisma"; import { z } from "zod"; type PartnerFilters = z.infer & { @@ -49,14 +49,16 @@ export async function getPartners(filters: PartnerFilters) { ? { partner: { country, - ...(search && { - OR: [ - { id: { contains: search } }, - { name: { contains: search } }, - { email: { contains: search } }, - ], - }), - email, + ...(email + ? { email } + : search + ? search.includes("@") + ? { email: search } + : { + email: { search: sanitizeFullTextSearch(search) }, + name: { search: sanitizeFullTextSearch(search) }, + } + : {}), }, } : {}), diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts index 9acb0313c0c..7bbcc67bd8d 100644 --- a/packages/prisma/index.ts +++ b/packages/prisma/index.ts @@ -15,3 +15,9 @@ declare global { } if (process.env.NODE_ENV === "development") global.prisma = prisma; + +export const sanitizeFullTextSearch = (search: string) => { + // remove unsupported characters for full text search + // '*', '+', '-', ' ', '(', ')', '~', '@','%', '<', '>', '=', '|', '!', '?', ':' + return search.replace(/[*+\- ()~@%<>!=?:\s]/g, "").trim(); +}; diff --git a/packages/prisma/schema/campaign.prisma b/packages/prisma/schema/campaign.prisma index 93f3201093f..228535756ec 100644 --- a/packages/prisma/schema/campaign.prisma +++ b/packages/prisma/schema/campaign.prisma @@ -24,7 +24,6 @@ model Campaign { status CampaignStatus @default(draft) name String subject String - body String? @db.Text // TODO: Remove after migration complete bodyJson Json @db.Json scheduledAt DateTime? createdAt DateTime @default(now()) diff --git a/packages/prisma/schema/partner.prisma b/packages/prisma/schema/partner.prisma index 686bfad2cc6..5835d8c1d13 100644 --- a/packages/prisma/schema/partner.prisma +++ b/packages/prisma/schema/partner.prisma @@ -78,6 +78,9 @@ model Partner { discoveredByPrograms DiscoveredPartner[] + @@index([email]) // For exact email lookups + @@index(name) // Already exists + @@fulltext([email, name]) // For full-text search @@index(discoverableAt) }