Skip to content

feat: implement home feed sorting, filtering#32

Merged
jaydonkc merged 10 commits intodevfrom
feature/POLY-15-home-feed-sorting-filtering
Feb 10, 2026
Merged

feat: implement home feed sorting, filtering#32
jaydonkc merged 10 commits intodevfrom
feature/POLY-15-home-feed-sorting-filtering

Conversation

@cole-hackman
Copy link
Copy Markdown
Collaborator

@cole-hackman cole-hackman commented Feb 5, 2026

Linked Issues

Linear: POLY-15

Summary

Implement recency-based home feed sorting with category and price range filtering for the PolyBuys marketplace.

Backend changes:

  • Updated getListings query with optional category, minPrice, maxPrice, limit args
  • Uses by_status_createdAt index for deterministic newest-first ordering
  • Validates price inputs (minPrice ≥ 0, maxPrice ≥ minPrice)
  • Excludes hidden and non-active listings

Frontend changes:

  • FilterBar.tsx - Horizontal chip bar with category/price filter chips
  • CategoryPicker.tsx - Bottom sheet for category selection
  • PriceRangePicker.tsx - Bottom sheet with min/max inputs and quick presets
  • Updated home screen with filter state management and "No results" handling

How to Test

Steps to verify locally:

  • npm run lint
  • npm run typecheck
  • npm test
  • Manual flow:
    1. npm run dev:backend (in terminal A)
    2. npm run dev (in terminal B)
    3. Verify the change:
      • Home feed shows listings newest-first
      • Tap "Category" chip → select a category → feed filters correctly
      • Tap "Price" chip → enter range or use preset → feed filters correctly
      • Active filters show as filled chips with × to clear
      • "Clear All" removes all filters
      • Empty filter results show "No listings match your filters" with Clear Filters button

Checklist

  • Tests added/updated (if applicable)
  • Lint/tests pass locally (npm run lint)
  • Docs updated (README/ADR/changelog if needed)
  • Follows conventional commit format
  • No merge conflicts with dev

Summary by CodeRabbit

  • New Features

    • Filter listings by category and price with new UI pickers, Clear Filters button, and updated empty-state messaging.
    • Submit reports on listings or profiles (scam, inappropriate, spam); reports enforce per-user daily limits and can auto-hide content after multiple unique reports.
    • Owners can view their own hidden listings; hidden items are excluded from public search/results.
  • Schema / Data

    • Listings and profiles now track hidden state, timestamp, and hide reason.
  • Tests

    • Added extensive tests covering filtering, reporting, rate limits, auto-moderation, validation, and visibility rules.

- Update getListings query with category, minPrice, maxPrice filters
- Use by_status_createdAt index for deterministic newest-first ordering
- Add price validation (min >= 0, max >= min)
- Add FilterBar component with filter chips
- Add CategoryPicker bottom sheet
- Add PriceRangePicker bottom sheet with presets
- Integrate filters into home screen with state management
- Add 'No results' state with clear filters option
- Add 7 tests for filter combinations
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds reporting infrastructure and auto-moderation (new reports table and createReport mutation), hidden-content fields on listings/profiles, server-side listing filters and visibility rules (including getMyHiddenListings), extensive backend tests, and new frontend filter UI components wired to getListings.

Changes

Cohort / File(s) Summary
Schema
backend/convex/schema.ts
Add optional isHidden, hiddenAt, hiddenReason to listings and profiles; add new reports table with by_target and by_reporter indexes.
Reporting Backend
backend/convex/reports.ts, backend/convex/__tests__/reports.test.ts
Add createReport mutation with auth/email checks, target existence validation, notes length limit, duplicate-report prevention, per-reporter 10-per-day rate limit, unique-reporter counting, and auto-hide threshold (3). Comprehensive tests for validation, rate limits, auto-hide, and visibility rules.
Listing Retrieval & Visibility
backend/convex/listings.ts, backend/convex/__tests__/listings.test.ts, backend/convex/__tests__/schema.test.ts
getListings gains optional filters (category, minPrice, maxPrice, limit) with validation and deterministic newest-first ordering; hidden listings filtered from search/results; getListing restricts hidden listing visibility to owner; add getMyHiddenListings. Tests updated/added.
Frontend Filter UI
frontend/app/index.tsx, frontend/components/FilterBar.tsx, frontend/components/CategoryPicker.tsx, frontend/components/PriceRangePicker.tsx
Add FilterBar, CategoryPicker, PriceRangePicker components; wire filter state to api.listings.getListings; add Clear Filters control and adjust empty-state messaging and UI.
Tests (general)
backend/convex/__tests__/*
New and expanded tests covering listing filters, report creation/validation, duplicate/rate-limit behavior, auto-hide outcomes, visibility access control, and schema expectations.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Server as Convex Server
    participant DB as Database

    Client->>Server: createReport(targetId, targetType, reason, notes)
    Server->>Server: Authenticate & lookup reporter by email
    Server->>DB: Query existing report for (targetId, reporterId)
    alt duplicate
        Server->>Client: Return error (duplicate)
    else
        Server->>DB: Count reporter's reports in last 24h
        alt rate limit exceeded
            Server->>Client: Return error (rate limit)
        else
            Server->>DB: Insert new report
            Server->>DB: Count unique reporters for targetId
            alt unique reporters >= 3
                Server->>DB: Update target (listing/profile) set isHidden=true, hiddenAt, hiddenReason
            end
            Server->>Client: Return reportId
        end
    end
Loading
sequenceDiagram
    participant User as User
    participant Client as Client
    participant Server as Convex Server
    participant DB as Database

    User->>Client: Select category/min/max filters
    Client->>Server: getListings(category, minPrice, maxPrice, limit)
    Server->>DB: Query by by_status_createdAt index
    Server->>Server: Exclude listings where isHidden=true
    Server->>Server: Apply category and price filters, enforce validations
    Server->>Server: Paginate results
    Server->>Client: Return filtered listings
    Client->>User: Render listings and show Clear Filters option
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jaydonkc

Poem

🐰 I sniffed the reports beneath the moonlit cart,
Three gentle hops and hidden signs took part,
Filters and pickers tidy up the view,
Prices and categories hop into queue,
A rabbit claps — the marketplace looks smart!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing home feed sorting and filtering functionality for the marketplace.
Description check ✅ Passed The description covers all required template sections including linked issues, a clear summary of backend and frontend changes, comprehensive testing instructions, and a completed checklist confirming tests and linting are in place.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/POLY-15-home-feed-sorting-filtering

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@frontend/components/FilterBar.tsx`:
- Around line 69-77: The clear button is nested inside a parent pressable which
can cause the parent’s onPress to fire; instead of attempting stopPropagation
(not supported), remove nesting by moving the clear TouchableOpacity (the
hasCategory block with onClearCategory and styles.clearButton) out of the parent
chip TouchableOpacity and position it alongside via styles (or absolutely
position it using the chip container) so taps target only the clear button; if
you cannot restructure, set the parent chip TouchableOpacity to
pointerEvents="box-none" and tighten hitSlop on the clear button to avoid
overlap, or alternatively implement onStartShouldSetResponder on the clear
button container to claim touches before the parent.

In `@frontend/components/PriceRangePicker.tsx`:
- Around line 47-59: The handler handleApply currently validates minVal but not
maxVal alone; add a check that when maxVal is defined it is not negative (e.g.,
if (maxVal !== undefined && maxVal < 0) { setError('Maximum price cannot be
negative'); return; }) before the min/max comparison, and ensure you still keep
the existing maxVal < minVal check; use the existing minVal, maxVal, and
setError symbols to locate and implement this validation.
🧹 Nitpick comments (6)
backend/convex/schema.ts (1)

64-73: Consider adding a composite index for rate limiting queries.

The by_reporter index supports querying reports by reporter, but the rate limiting logic in reports.ts also filters by createdAt. For optimal performance with many reports per user, consider a composite index:

   .index('by_target', ['targetId', 'targetType'])
-  .index('by_reporter', ['reporterId']),
+  .index('by_reporter', ['reporterId'])
+  .index('by_reporter_createdAt', ['reporterId', 'createdAt']),

This would allow the rate limiting query to use an index range scan on createdAt rather than in-memory filtering. However, this is optional given the expected low volume of reports per user per day.

backend/convex/reports.ts (1)

87-97: Minor: Duplicate step number in comments and rate limit query optimization.

  1. Line 87 comment says "// 6." but should be "// 7." (duplicate of line 74).

  2. The rate limiting query uses .collect() which loads all recent reports into memory. Consider using a count approach or early exit:

♻️ Proposed optimization
     // 7. Check rate limiting (max 10 reports per day)
     const oneDayAgo = Date.now() - ONE_DAY_MS;
     const recentReports = await ctx.db
       .query('reports')
       .withIndex('by_reporter', (q) => q.eq('reporterId', user._id))
       .filter((q) => q.gt(q.field('createdAt'), oneDayAgo))
-      .collect();
-
-    if (recentReports.length >= MAX_REPORTS_PER_DAY) {
+      .take(MAX_REPORTS_PER_DAY);
+
+    if (recentReports.length >= MAX_REPORTS_PER_DAY) {

Using .take(MAX_REPORTS_PER_DAY) stops scanning once the limit is reached, avoiding unnecessary reads.

backend/convex/listings.ts (1)

78-101: Consider using by_status_category index when filtering by category.

The current implementation uses by_status_createdAt for all queries, then applies category filter in memory. When args.category is provided, using by_status_category could reduce the scanned dataset significantly:

♻️ Proposed optimization
-    // Use by_status_createdAt index for deterministic newest-first ordering
-    const query = ctx.db
-      .query('listings')
-      .withIndex('by_status_createdAt', (q) => q.eq('status', 'active'))
-      .order('desc');
-
-    // Apply filters
-    const listings = await query
-      .filter((q) => {
-        let conditions = q.neq(q.field('isHidden'), true);
-
-        if (args.category) {
-          conditions = q.and(conditions, q.eq(q.field('category'), args.category));
-        }
+    // Choose index based on filter: category filter uses by_status_category, otherwise by_status_createdAt
+    let baseQuery;
+    if (args.category) {
+      baseQuery = ctx.db
+        .query('listings')
+        .withIndex('by_status_category', (q) =>
+          q.eq('status', 'active').eq('category', args.category)
+        );
+    } else {
+      baseQuery = ctx.db
+        .query('listings')
+        .withIndex('by_status_createdAt', (q) => q.eq('status', 'active'))
+        .order('desc');
+    }
+
+    // Apply remaining filters
+    const listings = await baseQuery
+      .filter((q) => {
+        let conditions = q.neq(q.field('isHidden'), true);
+
         if (args.minPrice !== undefined) {

Note: When using by_status_category, results need sorting by createdAt in memory afterward to maintain newest-first ordering. This trade-off may not be worth it for small datasets.

backend/convex/__tests__/listings.test.ts (1)

173-182: Consider extracting shared baseArgs to reduce duplication.

The baseArgs object is duplicated between the two describe blocks (lines 23-31 and 174-182). Consider extracting it to the module scope.

♻️ Proposed refactor
 const modules = {
   '../listings.ts': () => Promise.resolve(listingsModule),
   '../_generated/api.ts': () => Promise.resolve(apiModule),
   '../_generated/server.ts': () => Promise.resolve(serverModule),
 } as any;

+// Shared test fixture for listing creation
+const baseListingArgs = {
+  title: 'Great textbook for CSC 202',
+  description: 'Gently used, highlights in a few chapters.',
+  price: 50,
+  sellerEmail: 'test@example.com',
+  category: 'textbooks' as const,
+  images: ['https://example.com/book1.png'],
+  condition: 'used' as const,
+};

 describe('Listings mutations', () => {
-  const baseArgs = {
-    title: 'Great textbook for CSC 202',
-    ...
-  };
+  const baseArgs = baseListingArgs;
frontend/app/index.tsx (1)

48-54: Consider handling query error states.

The loading state check (listings === undefined) is correct for Convex, but there's no handling for potential query errors. While Convex may throw errors that bubble up, consider adding error boundary support or checking for error states to improve user feedback.

frontend/components/PriceRangePicker.tsx (1)

96-115: Consider handling non-numeric input more robustly.

parseFloat can produce unexpected results for certain inputs (e.g., "12abc" parses to 12). While keyboardType="numeric" helps restrict input on mobile, consider sanitizing or validating that the entire string is a valid number before applying.

💡 Optional: Stricter input validation
   const handleApply = () => {
-    const minVal = min ? parseFloat(min) : undefined;
-    const maxVal = max ? parseFloat(max) : undefined;
+    const minVal = min ? Number(min) : undefined;
+    const maxVal = max ? Number(max) : undefined;
+
+    // Check for invalid number input
+    if (min && (isNaN(minVal!) || !/^\d*\.?\d*$/.test(min))) {
+      setError('Please enter a valid minimum price');
+      return;
+    }
+    if (max && (isNaN(maxVal!) || !/^\d*\.?\d*$/.test(max))) {
+      setError('Please enter a valid maximum price');
+      return;
+    }

Comment thread frontend/components/FilterBar.tsx
- Refactor FilterBar to avoid nested TouchableOpacity
- Add negative max price validation to PriceRangePicker
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@frontend/components/PriceRangePicker.tsx`:
- Around line 47-63: In handleApply, parseFloat(min) / parseFloat(max) can
produce NaN for non-numeric input; before performing negative/compare checks and
calling onApply, verify minVal and maxVal are finite numbers (e.g., using
Number.isFinite) and treat empty/whitespace-only strings as undefined (trim
first or test the original strings), calling setError('Please enter a valid
number') when a non-finite value is provided; ensure the rest of the validations
(negative and max < min) only run when values are finite and only pass finite
numbers or undefined into onApply.
🧹 Nitpick comments (2)
frontend/components/FilterBar.tsx (2)

63-75: Consider adding accessibility props for screen reader support.

The interactive elements lack accessibilityLabel and accessibilityRole props, which would improve the experience for users relying on screen readers.

♿ Suggested accessibility improvement
-          <TouchableOpacity style={styles.chipMainButton} onPress={onCategoryPress}>
+          <TouchableOpacity
+            style={styles.chipMainButton}
+            onPress={onCategoryPress}
+            accessibilityRole="button"
+            accessibilityLabel={hasCategory ? `Category filter: ${CATEGORY_LABELS[filters.category!]}` : 'Select category filter'}
+          >
             <Text style={[styles.chipText, hasCategory && styles.chipTextActive]}>
               {hasCategory ? CATEGORY_LABELS[filters.category!] : 'Category'}
             </Text>
           </TouchableOpacity>
           {hasCategory && (
             <TouchableOpacity
               style={styles.clearButton}
               onPress={onClearCategory}
               hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+              accessibilityRole="button"
+              accessibilityLabel="Clear category filter"
             >

Apply similar changes to the price chip and "Clear All" button.


117-128: Remove redundant padding properties for clarity.

The padding: 0 on line 126 overrides the paddingHorizontal and paddingVertical declarations on lines 120-121. While the comment explains the intent, having overridden values is confusing.

🧹 Suggested cleanup
   chip: {
     flexDirection: 'row',
     alignItems: 'center',
-    paddingHorizontal: 14,
-    paddingVertical: 8,
     borderRadius: 20,
     backgroundColor: '#f0f0f0',
     borderWidth: 1,
     borderColor: '#e0e0e0',
-    padding: 0, // Reset padding as inner elements handle spacing
     overflow: 'hidden',
   },

Comment thread frontend/components/PriceRangePicker.tsx
cole-hackman and others added 2 commits February 5, 2026 15:52
- Trim whitespace from inputs before parsing
- Treat empty/whitespace-only strings as undefined
- Validate parsed values are finite with Number.isFinite
- Show clear error message for invalid number input
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@backend/convex/listings.ts`:
- Around line 72-82: The validation logic in listings.ts checks that minPrice is
non-negative but misses a standalone non-negative check for maxPrice, so add a
guard that throws an error when args.maxPrice !== undefined && args.maxPrice < 0
(similar to the minPrice check) and keep the existing cross-check (maxPrice >=
minPrice) intact; update the validation block that references args.minPrice and
args.maxPrice to include this new check so negative maxPrice values are rejected
consistently.
🧹 Nitpick comments (3)
backend/convex/listings.ts (2)

84-88: Variable query shadows the imported query from './_generated/server'.

Line 85 declares const query which shadows the module-level import on line 2. This works because the import is only used at module scope (for query({...})), but it's confusing. Consider renaming to listingsQuery or dbQuery.


428-443: getMyHiddenListings performs a full table scan without an index on sellerId.

The .filter() on line 439 scans every document in the listings table. The schema defines indexes on status, category, and their combinations, but lacks an index on sellerId. Add an index on ['sellerId', 'isHidden'] to efficiently filter by seller and visibility status, or at minimum add a single-column index on sellerId.

backend/convex/schema.ts (1)

66-75: Consider a compound index for rate-limiting queries on reports.

The rate-limiting logic in backend/convex/reports.ts (lines 86-95) queries reports by reporterId and then filters by time: .withIndex('by_reporter', ...).filter((q) => q.gt(q.field('createdAt'), oneDayAgo)). This causes the time filter to run in-memory over all of that reporter's reports. A compound index ['reporterId', 'createdAt'] would let you efficiently range-scan by both fields.

Also, targetId is typed as v.string() — a reasonable trade-off for the polymorphic reference, but Convex won't enforce referential integrity. Worth a doc comment noting this.

Suggested index improvement
   .index('by_target', ['targetId', 'targetType'])
-  .index('by_reporter', ['reporterId']),
+  .index('by_reporter', ['reporterId', 'createdAt']),

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@backend/convex/listings.ts`:
- Line 69: The limit arg currently defined as v.optional(v.number()) can be
non-positive or fractional and gets passed to .take(); update the handler that
reads the limit (the argument named "limit" used before calling .take()) to
coerce and clamp it to a positive integer with a sensible upper bound (e.g.,
default 20, min 1, max 100): if missing use 20, otherwise use Math.floor and
clamp between 1 and the max before passing to .take(); reference the "limit"
argument and the call to .take() in the listings query to locate where to apply
this change.
- Around line 72-86: The current price validation in the listings query only
checks for negativity and ordering but misses NaN/Infinity; update the checks
that reference args.minPrice and args.maxPrice to also ensure
Number.isFinite(value) is true and throw the same validation errors when it's
not; specifically, in the block that validates minPrice, maxPrice and the
comparison (the code using args.minPrice and args.maxPrice), add guards that
reject non-finite numbers so NaN/Infinity cannot pass into the filtering logic.
🧹 Nitpick comments (1)
backend/convex/listings.ts (1)

441-446: Add a composite index to optimize the sellerId + isHidden query.

The query uses .filter() without .withIndex(), causing a full table scan of the listings table. Adding a composite index like by_sellerId_isHidden would improve performance as the listings table grows. Other queries in the codebase use explicit indexes (e.g., by_status_createdAt, by_status_category); this should follow the same pattern.

Regarding the status filter: whether to exclude deleted listings is a design choice. Since this returns a user's own hidden listings, excluding deleted listings may not be necessary—users may want visibility into their deleted hidden listings for management purposes.

category: v.optional(categoryValidator),
minPrice: v.optional(v.number()),
maxPrice: v.optional(v.number()),
limit: v.optional(v.number()), // default 20
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

limit parameter lacks validation — non-positive or non-integer values could cause unexpected behavior with .take().

The limit arg accepts any number with no guards. Values like 0, -1, or 1.5 will be forwarded directly to .take(). Consider clamping to a positive integer with an upper bound:

Proposed fix
+    const MAX_LIMIT = 100;
+    const take = Math.min(
+      Math.max(1, Math.floor(args.limit ?? ITEMS_PER_PAGE)),
+      MAX_LIMIT
+    );
+
     // Use by_status_createdAt index for deterministic newest-first ordering
     const query = ctx.db
       .query('listings')
       .withIndex('by_status_createdAt', (q) => q.eq('status', 'active'))
       .order('desc');
 
     // Apply filters
     const listings = await query
       .filter((q) => {
         ...
       })
-      .take(args.limit ?? ITEMS_PER_PAGE);
+      .take(take);

Also applies to: 111-111

🤖 Prompt for AI Agents
In `@backend/convex/listings.ts` at line 69, The limit arg currently defined as
v.optional(v.number()) can be non-positive or fractional and gets passed to
.take(); update the handler that reads the limit (the argument named "limit"
used before calling .take()) to coerce and clamp it to a positive integer with a
sensible upper bound (e.g., default 20, min 1, max 100): if missing use 20,
otherwise use Math.floor and clamp between 1 and the max before passing to
.take(); reference the "limit" argument and the call to .take() in the listings
query to locate where to apply this change.

Comment on lines +72 to +86
// Validate price filters
if (args.minPrice !== undefined && args.minPrice < 0) {
throw new Error('minPrice must be non-negative');
}
if (args.maxPrice !== undefined && args.maxPrice < 0) {
throw new Error('maxPrice must be non-negative');
}

if (
args.maxPrice !== undefined &&
args.minPrice !== undefined &&
args.maxPrice < args.minPrice
) {
throw new Error('maxPrice must be greater than or equal to minPrice');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Convex v.number() validator NaN Infinity

💡 Result:

v.number() in Convex validates Float64 (IEEE-754 double) numbers and does allow special values like NaN, Infinity, and -Infinity. In Convex’s JSON/export representation, Inf and NaN are serialized as strings (since JSON can’t represent them as numbers). [1] [2]


🏁 Script executed:

# Find and examine the listings.ts file to understand the function signature and validators
find . -type f -name "listings.ts" | head -5

Repository: codebox-calpoly/PolyBuys

Length of output: 96


🏁 Script executed:

# Also search for how v.number() is used in the codebase
rg "v\.number\(\)" --type ts -A 2 -B 2 | head -50

Repository: codebox-calpoly/PolyBuys

Length of output: 2498


🏁 Script executed:

# Read the listings.ts file to see the full function context and validators
cat -n backend/convex/listings.ts | head -100

Repository: codebox-calpoly/PolyBuys

Length of output: 3766


🏁 Script executed:

# Search for the function that contains the validation at lines 72-86
rg "minPrice|maxPrice" backend/convex/listings.ts -B 10 -A 15

Repository: codebox-calpoly/PolyBuys

Length of output: 3767


Add Number.isFinite() guards to price validation.

Convex's v.number() validator accepts NaN and Infinity as valid numbers. Since NaN < 0 and NaN < NaN both evaluate to false, these values silently bypass all validation checks and enter the filter logic, causing unexpected query behavior (NaN comparisons in database filters always return false, matching no results).

Proposed fix
     // Validate price filters
+    if (args.minPrice !== undefined && !Number.isFinite(args.minPrice)) {
+      throw new Error('minPrice must be a finite number');
+    }
+    if (args.maxPrice !== undefined && !Number.isFinite(args.maxPrice)) {
+      throw new Error('maxPrice must be a finite number');
+    }
     if (args.minPrice !== undefined && args.minPrice < 0) {
       throw new Error('minPrice must be non-negative');
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Validate price filters
if (args.minPrice !== undefined && args.minPrice < 0) {
throw new Error('minPrice must be non-negative');
}
if (args.maxPrice !== undefined && args.maxPrice < 0) {
throw new Error('maxPrice must be non-negative');
}
if (
args.maxPrice !== undefined &&
args.minPrice !== undefined &&
args.maxPrice < args.minPrice
) {
throw new Error('maxPrice must be greater than or equal to minPrice');
}
// Validate price filters
if (args.minPrice !== undefined && !Number.isFinite(args.minPrice)) {
throw new Error('minPrice must be a finite number');
}
if (args.maxPrice !== undefined && !Number.isFinite(args.maxPrice)) {
throw new Error('maxPrice must be a finite number');
}
if (args.minPrice !== undefined && args.minPrice < 0) {
throw new Error('minPrice must be non-negative');
}
if (args.maxPrice !== undefined && args.maxPrice < 0) {
throw new Error('maxPrice must be non-negative');
}
if (
args.maxPrice !== undefined &&
args.minPrice !== undefined &&
args.maxPrice < args.minPrice
) {
throw new Error('maxPrice must be greater than or equal to minPrice');
}
🤖 Prompt for AI Agents
In `@backend/convex/listings.ts` around lines 72 - 86, The current price
validation in the listings query only checks for negativity and ordering but
misses NaN/Infinity; update the checks that reference args.minPrice and
args.maxPrice to also ensure Number.isFinite(value) is true and throw the same
validation errors when it's not; specifically, in the block that validates
minPrice, maxPrice and the comparison (the code using args.minPrice and
args.maxPrice), add guards that reject non-finite numbers so NaN/Infinity cannot
pass into the filtering logic.

@jaydonkc jaydonkc merged commit 713ccc9 into dev Feb 10, 2026
3 checks passed
This was referenced Feb 10, 2026
Merged
@coderabbitai coderabbitai Bot mentioned this pull request Mar 11, 2026
5 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Apr 21, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants