-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add fulltext search to GET /partners #3005
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR refactors partner search functionality to prioritize email-based filtering and implement full-text search sanitization. It introduces a new sanitizeFullTextSearch utility, updates search logic across API routes and client libraries, adds database indexes on partner email and name fields, and removes an unused body field from the Campaign model. Changes
Sequence DiagramsequenceDiagram
actor User
participant UI as SearchBoxPersisted
participant API as /api/partners/count
participant Lib as get-partners.ts
participant DB as Prisma<br/>Partner Query
User->>UI: Enter search term
UI->>API: GET /api/partners/count?search=term
API->>Lib: getPartners({search, email})
alt Email parameter provided
Lib->>DB: WHERE email = {email}
else Search contains "@"
Lib->>DB: WHERE email CONTAINS {search}
else Search is text
Lib->>Lib: sanitized = sanitizeFullTextSearch(search)
Lib->>DB: WHERE MATCH email, name AGAINST {sanitized}
end
DB-->>Lib: Partner results
Lib-->>API: Filtered partners
API-->>UI: Count result
UI-->>User: Display results
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/web/app/(ee)/api/partners/count/route.ts(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx(1 hunks)apps/web/lib/api/partners/get-partners.ts(2 hunks)packages/prisma/index.ts(1 hunks)packages/prisma/schema/campaign.prisma(0 hunks)packages/prisma/schema/partner.prisma(1 hunks)
💤 Files with no reviewable changes (1)
- packages/prisma/schema/campaign.prisma
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/app/(ee)/api/partners/count/route.ts (2)
apps/web/lib/zod/schemas/partners.ts (1)
partnersCountQuerySchema(184-193)packages/prisma/index.ts (1)
sanitizeFullTextSearch(19-23)
apps/web/lib/api/partners/get-partners.ts (1)
packages/prisma/index.ts (1)
sanitizeFullTextSearch(19-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1)
414-416: LGTM — placeholder aligns with new name/email search scope.
Please ensure any docs/tooltips referencing ID search are updated as well.packages/prisma/schema/partner.prisma (1)
81-83: Drop redundant email index; clarify intent for name index.
@unique(line 30) → an extra@@index([email])is redundant and adds write/read overhead. Unique constraints already optimize exact lookups.@@index(name)appears to be new (no prior declaration found), not duplicate. The comment "Already exists" is misleading. Verify whether this index is intentional for query performance on the name field.- The composite
@@fulltext([email, name])is appropriate for full-text search.Suggested change:
- @@index([email]) // For exact email lookups - @@index(name) // Already exists + @@index(name) @@fulltext([email, name]) // For full-text searchOr remove the name index entirely if it's not needed for query patterns. Please clarify the intent.
| ? { email } | ||
| : search | ||
| ? search.includes("@") | ||
| ? { email: search } | ||
| : { | ||
| email: { search: sanitizeFullTextSearch(search) }, | ||
| name: { search: sanitizeFullTextSearch(search) }, | ||
| } | ||
| : {}), | ||
| ...(partnerIds && { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Search logic uses AND across email/name; should be OR. Also needs empty‑query guard.
Current block applies both email.search and name.search simultaneously (AND), causing near-zero matches and potential FTS errors when sanitized is empty.
Fix:
- const commonWhere: Prisma.PartnerWhereInput = {
- ...(email
- ? { email }
- : search
- ? search.includes("@")
- ? { email: search }
- : {
- email: { search: sanitizeFullTextSearch(search) },
- name: { search: sanitizeFullTextSearch(search) },
- }
- : {}),
+ const sanitized =
+ search && !search.includes("@") ? sanitizeFullTextSearch(search) : undefined;
+
+ const searchWhere: Prisma.PartnerWhereInput =
+ email
+ ? { email }
+ : search
+ ? search.includes("@")
+ ? { email: search }
+ : sanitized
+ ? {
+ OR: [
+ { email: { search: sanitized } },
+ { name: { search: sanitized } },
+ ],
+ }
+ : {}
+ : {};
+
+ const commonWhere: Prisma.PartnerWhereInput = {
+ ...searchWhere,
...(partnerIds && {
id: { in: partnerIds },
}),
};Consider making the email match case‑insensitive or partial if desired UX (e.g., contains with mode: 'insensitive'). Please confirm DB/provider supports the chosen FTS semantics.
I'll verify the review comment by examining the actual codebase.
🏁 Script executed:
#!/bin/bash
# 1. Check if the file exists and show lines around 17-27
fd "route.ts" --path "*partners/count*" --exec cat {} \;Length of output: 215
🏁 Script executed:
#!/bin/bash
# 2. Find the sanitizeFullTextSearch function definition to understand its behavior
rg -n "sanitizeFullTextSearch" -A 5 -B 2 --type=ts --type=jsLength of output: 3320
🏁 Script executed:
#!/bin/bash
# 3. Get full context of the route.ts file to understand the complete logic
fd "route.ts" --path "*partners/count*" --exec wc -l {} \;Length of output: 215
🏁 Script executed:
#!/bin/bash
# 4. Check Prisma schema to understand PartnerWhereInput structure
fd "schema.prisma" --exec cat {} \;Length of output: 2710
🏁 Script executed:
#!/bin/bash
# Get full content of the partners count route file
cat "apps/web/app/(ee)/api/partners/count/route.ts"Length of output: 3722
🏁 Script executed:
#!/bin/bash
# Find full where-clause context with more lines before/after
rg -n "commonWhere" -B 10 -A 5 "apps/web/app/(ee)/api/partners/count/route.ts"Length of output: 2161
🏁 Script executed:
#!/bin/bash
# Check how the where clause is actually used in the query
rg -n "where.*commonWhere" -A 3 -B 3 --type=ts "apps/web/app/(ee)/api/partners/count"Length of output: 0
AND logic in email/name search is overly restrictive; empty-string handling needed.
The review correctly identifies two issues:
-
AND vs OR: Lines 23–24 simultaneously apply
email.searchANDname.search, making results far too restrictive (both must match). Typical UX expects OR (match either field). -
Empty string risk:
sanitizeFullTextSearch()can return""(e.g., if search is@or contains only special chars). Passing empty string to Prisma full-text search on MySQL may cause errors or unexpected behavior.
The suggested fix is valid: extract and guard the sanitized value, then wrap both conditions in an OR block. This also affects the identical pattern in apps/web/lib/api/partners/get-partners.ts lines 58–59.
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/partners/count/route.ts lines 17-27 (and similarly in
apps/web/lib/api/partners/get-partners.ts around lines 58-59), the current logic
combines email.search and name.search with AND and passes the raw sanitized
string directly; fix by first computing a sanitizedSearch =
sanitizeFullTextSearch(search) and only add the full-text search block when
sanitizedSearch !== ""; when search includes "@" keep the existing equality
email match, otherwise replace the existing { email: { search: ... }, name: {
search: ... } } AND structure with an OR block that includes either { email: {
search: sanitizedSearch } } OR { name: { search: sanitizedSearch } }; ensure the
code falls back to no full-text filter if sanitizedSearch is empty to avoid
Prisma/MySQL errors.
| ? { email } | ||
| : search | ||
| ? search.includes("@") | ||
| ? { email: search } | ||
| : { | ||
| email: { search: sanitizeFullTextSearch(search) }, | ||
| name: { search: sanitizeFullTextSearch(search) }, | ||
| } | ||
| : {}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Use OR across email/name and guard empty sanitized search.
Current where merges email.search and name.search as AND and doesn’t guard empty sanitized input.
Apply:
- ...(email
- ? { email }
- : search
- ? search.includes("@")
- ? { email: search }
- : {
- email: { search: sanitizeFullTextSearch(search) },
- name: { search: sanitizeFullTextSearch(search) },
- }
- : {}),
+ ...(email
+ ? { email }
+ : search
+ ? search.includes("@")
+ ? { email: search }
+ : sanitizedSearch
+ ? {
+ OR: [
+ { email: { search: sanitizedSearch } },
+ { name: { search: sanitizedSearch } },
+ ],
+ }
+ : {}
+ : {}),Add above the query (after destructuring):
const sanitizedSearch =
search && !search.includes("@") ? sanitizeFullTextSearch(search) : undefined;Please verify that results now include matches where either name or email satisfies the query and that whitespace‑only queries don’t throw.
---
🏁 Script executed:
```shell
#!/bin/bash
# Find and inspect the get-partners.ts file
fd 'get-partners\.ts' --type f
Length of output: 41
🏁 Script executed:
#!/bin/bash
# Inspect the file context around lines 40-70
cat apps/web/lib/api/partners/get-partners.ts | head -80 | tail -50Length of output: 1080
🏁 Script executed:
#!/bin/bash
# Search for sanitizeFullTextSearch definition
rg -n 'sanitizeFullTextSearch' --type ts --type js -B2 -A5Length of output: 3320
🏁 Script executed:
#!/bin/bash
# Look for count route mentioned in review
rg -n 'count.*where' apps/web/lib/api/partners/ -B3 -A3 --type tsLength of output: 0
🏁 Script executed:
#!/bin/bash
# Get full context of the query to understand its structure
cat apps/web/lib/api/partners/get-partners.ts | head -100Length of output: 2148
---
🏁 Script executed:
```shell
#!/bin/bash
# Inspect count route to compare with get-partners.ts
cat apps/web/app/(ee)/api/partners/count/route.ts
Length of output: 136
🏁 Script executed:
#!/bin/bash
# Search for other uses of Prisma OR pattern to confirm syntax
rg -n 'OR:\s*\[' apps/ -A3 --type ts --type js | head -40Length of output: 2239
🏁 Script executed:
#!/bin/bash
# Verify sanitizeFullTextSearch edge cases
python3 << 'EOF'
import re
def sanitizeFullTextSearch(search):
return re.sub(r'[*+\- ()~@%<>!=?:\s]', '', search).strip()
# Test cases
test_cases = [
"john doe",
" ",
"***---",
"@@@",
"[email protected]",
"normal",
]
for test in test_cases:
result = sanitizeFullTextSearch(test)
print(f"'{test}' → '{result}' (empty: {not result})")
EOFLength of output: 200
---
🏁 Script executed:
```shell
#!/bin/bash
# Find and read count route with proper escaping
find apps/web/app -path "*partners/count/route.ts" -type f -exec cat {} \;
Length of output: 7640
🏁 Script executed:
#!/bin/bash
# Confirm both files have the same search pattern issue
rg -n 'email.*search.*sanitizeFullTextSearch' apps/web/lib/api/partners/get-partners.ts apps/web/app -B1 -A1 --type tsLength of output: 606
Use OR across email/name and guard empty sanitized search.
Current where merges email.search and name.search as AND, and doesn't guard empty sanitized input. The sanitizeFullTextSearch function can return an empty string when input is whitespace-only or contains only special characters, causing Prisma full-text search to fail or return unexpected results.
Also note: apps/web/app/(ee)/api/partners/count/route.ts lines 23-24 has the identical issue and needs the same fix for consistency.
Apply to apps/web/lib/api/partners/get-partners.ts:
- ...(email
- ? { email }
- : search
- ? search.includes("@")
- ? { email: search }
- : {
- email: { search: sanitizeFullTextSearch(search) },
- name: { search: sanitizeFullTextSearch(search) },
- }
- : {}),
+ ...(email
+ ? { email }
+ : search
+ ? search.includes("@")
+ ? { email: search }
+ : sanitizedSearch
+ ? {
+ OR: [
+ { email: { search: sanitizedSearch } },
+ { name: { search: sanitizedSearch } },
+ ],
+ }
+ : {}
+ : {}),Add above the query (after destructuring):
const sanitizedSearch =
search && !search.includes("@") ? sanitizeFullTextSearch(search) : undefined;Apply the same changes to apps/web/app/(ee)/api/partners/count/route.ts lines 20-26.
🤖 Prompt for AI Agents
In apps/web/lib/api/partners/get-partners.ts around lines 52 to 61, the current
merge builds email.search and name.search as an AND and doesn’t guard against
sanitizeFullTextSearch returning an empty string; create a sanitizedSearch
variable right after destructuring as: const sanitizedSearch = search &&
!search.includes("@") ? sanitizeFullTextSearch(search) : undefined; then change
the where clause to use an OR between email and name when using sanitizedSearch
(only include the full-text search conditions if sanitizedSearch is truthy) and
keep the direct email branch for exact email matches; apply the identical fix in
apps/web/app/(ee)/api/partners/count/route.ts around lines 20 to 26.
| export const sanitizeFullTextSearch = (search: string) => { | ||
| // remove unsupported characters for full text search | ||
| // '*', '+', '-', ' ', '(', ')', '~', '@','%', '<', '>', '=', '|', '!', '?', ':' | ||
| return search.replace(/[*+\- ()~@%<>!=?:\s]/g, "").trim(); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Sanitizer can produce empty queries and over‑sanitize (drops spaces).
- Returning "" risks FTS errors downstream.
- Removing whitespace downgrades multi‑token search.
Refine and keep tokens:
-export const sanitizeFullTextSearch = (search: string) => {
- // remove unsupported characters for full text search
- // '*', '+', '-', ' ', '(', ')', '~', '@','%', '<', '>', '=', '|', '!', '?', ':'
- return search.replace(/[*+\- ()~@%<>!=?:\s]/g, "").trim();
-};
+export const sanitizeFullTextSearch = (search: string) => {
+ // Keep letters/numbers/spaces; strip punctuation; collapse whitespace.
+ const sanitized = search
+ .normalize("NFKC")
+ .replace(/[^\p{L}\p{N}\s]+/gu, " ")
+ .replace(/\s+/g, " ")
+ .trim();
+ return sanitized;
+};Please verify behavior for inputs like "john doe", "john-doe", "***", and whitespace-only.
I'll verify this review comment by examining the codebase and understanding the Prisma FTS constraints.
🏁 Script executed:
#!/bin/bash
# Find the file and inspect current implementation
fd -t f "index.ts" packages/prisma 2>/dev/null | head -5Length of output: 24
🏁 Script executed:
#!/bin/bash
# Check content of the file
cat packages/prisma/index.tsLength of output: 617
🏁 Script executed:
#!/bin/bash
# Search for uses of sanitizeFullTextSearch
rg -n "sanitizeFullTextSearch" --type=ts --type=js -C2Length of output: 2331
🏁 Script executed:
#!/bin/bash
# Test the regex patterns and function behavior
node << 'EOF'
// Current implementation
const current = (search) => {
return search.replace(/[*+\- ()~@%<>!=?:\s]/g, "").trim();
};
// Proposed implementation
const proposed = (search) => {
const sanitized = search
.normalize("NFKC")
.replace(/[^\p{L}\p{N}\s]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
return sanitized;
};
const testCases = ["john doe", "john-doe", "***", " ", "", "hello world", "@@@test"];
console.log("TEST RESULTS:");
console.log("=".repeat(60));
testCases.forEach(input => {
const curr = current(input);
const prop = proposed(input);
console.log(`Input: "${input}"`);
console.log(` Current: "${curr}" (empty: ${curr === ""})`);
console.log(` Proposed: "${prop}" (empty: ${prop === ""})`);
console.log();
});
EOFLength of output: 658
🌐 Web query:
Prisma full text search empty string MATCH AGAINST behavior PostgreSQL
💡 Result:
Short answer
- Prisma's Postgres full-text search delegates to PostgreSQL. An empty/blank search string produces a tsquery with no lexemes (effectively an empty/non‑indexable query), so the match (@@) yields no matches — i.e., you get no rows unless you handle the empty input in your app/ORM layer. [1][2][3]
Why
- Prisma's FTS API uses the database's native text‑search machinery (so behavior comes from PostgreSQL). [1]
- PostgreSQL text‑search helpers such as plainto_tsquery/websearch_to_tsquery convert input text into a tsquery; if no lexemes survive (empty string or only stopwords), the resulting query is non‑indexable / empty and will not match documents. This is documented in PostgreSQL and reported in user Q&As. [2][3]
What to do
- Check for empty/blank search strings in your application and:
- skip the full‑text filter (return all rows or apply other filters), or
- treat empty as a special case (e.g., fallback to ILIKE or a different query), or
- build a tsquery yourself (e.g., avoid calling plainto_tsquery with '') if you want different behavior.
- If you need web‑style parsing, consider websearch_to_tsquery but still guard empty input. [2][4]
References
- Prisma docs: Full‑text search (Postgres uses DB native queries). [1]
- PostgreSQL text‑search functions/operators (plainto_tsquery/websearch_to_tsquery, behavior with no surviving lexemes). [2]
- Reports / Q&A showing plainto_tsquery/tsquery empty/no matches for empty/stopword inputs. [3]
- PostgreSQL websearch_to_tsquery documentation (alternative, but still requires non‑empty input). [4]
Sources
[1] Prisma — Full‑Text Search (PostgreSQL).
[2] PostgreSQL docs — Text Search Functions and Operators / Controlling Text Search.
[3] Example discussions: StackOverflow (plainto_tsquery/no lexemes) and related threads.
[4] PostgreSQL docs — websearch_to_tsquery / plainto_tsquery.
🏁 Script executed:
#!/bin/bash
# Check if usage sites guard against empty strings after sanitization
cat apps/web/lib/api/partners/get-partners.ts | sed -n '50,70p'Length of output: 556
🏁 Script executed:
#!/bin/bash
# Check the second usage site
cat apps/web/app/(ee)/api/partners/count/route.ts | sed -n '15,35p'Length of output: 154
🏁 Script executed:
#!/bin/bash
# Fix path escaping for second usage site
cat "apps/web/app/(ee)/api/partners/count/route.ts" | sed -n '15,35p'Length of output: 576
🏁 Script executed:
#!/bin/bash
# Get broader context around first usage to see full conditional logic
cat apps/web/lib/api/partners/get-partners.ts | sed -n '40,75p'Length of output: 839
Proposed fix is incomplete—addresses space removal but not empty-string risk.
The review correctly identifies two problems: space removal (hurts multi-token search) and empty strings (break FTS). The proposed diff fixes space preservation but leaves the empty-string issue unresolved.
Test results:
- Current:
"***"→""(empty, breaks FTS) - Proposed:
"***"→""(still empty, still breaks FTS)
Both call sites (get-partners.ts, count/route.ts) pass the sanitized value directly to Prisma without checking for empty strings. Per PostgreSQL behavior, an empty tsquery yields no results.
Refine the proposed fix to guard against empty results:
export const sanitizeFullTextSearch = (search: string) => {
// Keep letters/numbers/spaces; strip punctuation; collapse whitespace.
const sanitized = search
.normalize("NFKC")
.replace(/[^\p{L}\p{N}\s]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
return sanitized || null; // Return null if empty to signal invalid query
};Then update call sites to skip FTS when the result is null/empty:
: search
? search.includes("@")
? { email: search }
- : {
+ : sanitizeFullTextSearch(search) ? {
email: { search: sanitizeFullTextSearch(search) },
name: { search: sanitizeFullTextSearch(search) },
}
+ : {}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/prisma/index.ts around lines 19 to 23, the sanitizer currently
strips unsupported characters but can return an empty string (e.g., "***"),
which breaks PostgreSQL full-text search; change sanitizeFullTextSearch to
preserve spaces where appropriate and return null (or undefined) instead of an
empty string when the sanitized result contains no alphanumeric tokens (i.e.,
after trimming and collapsing multiple spaces, if the string is empty or only
non-word characters, return null). Then update call sites (get-partners.ts and
count/route.ts) to check the sanitizer's result and skip adding the Prisma
full-text-search condition when the value is null/empty (use the non-FTS
fallback or omit the where clause), ensuring no empty tsquery is passed to
Prisma.
Summary by CodeRabbit
New Features
Changes