-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<typeof getPartnersQuerySchemaExtended> & { | ||
|
|
@@ -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 } | ||
| : search | ||
| ? search.includes("@") | ||
| ? { email: search } | ||
| : { | ||
| email: { search: sanitizeFullTextSearch(search) }, | ||
| name: { search: sanitizeFullTextSearch(search) }, | ||
| } | ||
| : {}), | ||
|
Comment on lines
+52
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainUse OR across email/name and guard empty sanitized search. Current where merges 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. 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 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 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 Also note: Apply to - ...(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 🤖 Prompt for AI Agents |
||
| }, | ||
| } | ||
| : {}), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
| }; | ||
|
Comment on lines
+19
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainSanitizer can produce empty queries and over‑sanitize (drops spaces).
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: 💡 Result: Short answer
Why
What to do
References
Sources 🏁 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:
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) },
}
+ : {}
🤖 Prompt for AI Agents |
||
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.searchandname.searchsimultaneously (AND), causing near-zero matches and potential FTS errors when sanitized is empty.Fix:
Consider making the email match case‑insensitive or partial if desired UX (e.g.,
containswithmode: 'insensitive'). Please confirm DB/provider supports the chosen FTS semantics.I'll verify the review comment by examining the actual codebase.
🏁 Script executed:
Length of output: 215
🏁 Script executed:
Length of output: 3320
🏁 Script executed:
Length of output: 215
🏁 Script executed:
Length of output: 2710
🏁 Script executed:
Length of output: 3722
🏁 Script executed:
Length of output: 2161
🏁 Script executed:
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
ORblock. This also affects the identical pattern inapps/web/lib/api/partners/get-partners.tslines 58–59.🤖 Prompt for AI Agents