Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions apps/web/app/(ee)/api/partners/count/route.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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 && {
Comment on lines +17 to 27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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=js

Length 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:

  1. AND vs OR: Lines 23–24 simultaneously apply email.search AND name.search, making results far too restrictive (both must match). Typical UX expects OR (match either field).

  2. 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.

id: { in: partnerIds },
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ export function PartnersTable() {
onRemove={onRemove}
/>
<SearchBoxPersisted
placeholder="Search by ID, name, or email"
placeholder="Search by name or email"
inputClassName="md:w-[19rem]"
/>
</div>
Expand Down
20 changes: 11 additions & 9 deletions apps/web/lib/api/partners/get-partners.ts
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> & {
Expand Down Expand Up @@ -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) },
}
: {}),
Comment on lines +52 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -50

Length of output: 1080


🏁 Script executed:

#!/bin/bash
# Search for sanitizeFullTextSearch definition
rg -n 'sanitizeFullTextSearch' --type ts --type js -B2 -A5

Length 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 ts

Length 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 -100

Length 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 -40

Length 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})")
EOF

Length 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 ts

Length 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.

},
}
: {}),
Expand Down
6 changes: 6 additions & 0 deletions packages/prisma/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -5

Length of output: 24


🏁 Script executed:

#!/bin/bash
# Check content of the file
cat packages/prisma/index.ts

Length of output: 617


🏁 Script executed:

#!/bin/bash
# Search for uses of sanitizeFullTextSearch
rg -n "sanitizeFullTextSearch" --type=ts --type=js -C2

Length 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();
});
EOF

Length 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.

1 change: 0 additions & 1 deletion packages/prisma/schema/campaign.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
3 changes: 3 additions & 0 deletions packages/prisma/schema/partner.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down