Feat: Implement server-side pagination and advanced database filtering#431
Feat: Implement server-side pagination and advanced database filtering#431wolgwang1729 wants to merge 5 commits into
Conversation
|
@wolgwang1729 is attempting to deploy a commit to the shopstr-eng Team on Vercel. A member of the Team first needs to authorize it. |
|
I have performed a benchmark test comparing the legacy database-fetching logic against the new server-side pagination and filtering implementation.
The minor increase in response time (averaging 30-60ms) is primarily due to the introduction of server-side data validation and the new requirement for reliable pagination metrics. Specifically, the database now performs parallel |
|
Curious about the hardcoded block for |
It was already in that file’s client-side filter, and I turned it into a named constant in the same file so the new server-filter path could reuse the exact same rule. Check line around 158 in this file |
I meant why it was added in the first place. But I've figured it out. It was added in the commit 95221de as a temp fix to prevent adult content from coming up on the feed. |
|
TODO(after the PR is merged):
loading.more.products.flicker.mp4 |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Implements server-side pagination, total counts, and advanced filtering for marketplace product listings, while refactoring the fetch flow to show cached DB results immediately and refresh from relays in the background.
Changes:
- Added shared filter types and propagated filter/total-count support through context, UI, and fetch service.
- Updated DB querying to support paging + filtering and added a total-count query used by
/api/db/fetch-products. - Expanded caching/schema rules to include kind
1(“zapsnag”) events as products.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/types/types.ts | Adds FilterParams type for shared filter plumbing |
| utils/nostr/fetch-service.ts | Hybrid DB-first + relay refresh fetching, plus filter forwarding |
| utils/db/db-service.ts | Adds filtered/paginated product query + total count helper; updates kind handling |
| utils/db/cache-event-policy.ts | Caches kind 1 events |
| utils/context/context.ts | Extends ProductContext with totals + paging/refresh methods |
| pages/api/db/fetch-products.ts | Adds filter parsing and returns { events, total } |
| pages/_app.tsx | Implements loadMoreProducts / refreshProducts and total tracking |
| components/display-products.tsx | Uses server totals and triggers server-side refresh/load-more |
| components/tests/display-products.test.tsx | Updates mocked ProductContext shape |
| tests/pages/api/db/fetch-products.test.ts | Adds API handler coverage for parsing + response shape |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let apiUrl = "/api/db/fetch-products"; | ||
| const queryParams = new URLSearchParams(); | ||
| if (until) { | ||
| queryParams.append("until", until.toString()); | ||
| } | ||
| return event.id; | ||
| }; | ||
| if (filters?.search) { | ||
| queryParams.append("search", filters.search); | ||
| } | ||
| if (filters?.categories && filters.categories.length > 0) { | ||
| queryParams.append("categories", filters.categories.join(",")); | ||
| } | ||
| if (filters?.location) { | ||
| queryParams.append("location", filters.location); | ||
| } | ||
| appendFilterParam(queryParams, "pubkey", filters?.pubkey); | ||
| appendFilterParam( | ||
| queryParams, | ||
| "excludePubkeys", | ||
| filters?.excludePubkeys | ||
| ); |
| const requestedAuthors = | ||
| filters?.pubkey === undefined | ||
| ? undefined | ||
| : Array.isArray(filters.pubkey) | ||
| ? filters.pubkey | ||
| : [filters.pubkey]; | ||
| const excludePubkeys = new Set(filters?.excludePubkeys ?? []); | ||
| const shouldFetchRelayEvents = | ||
| requestedAuthors === undefined || requestedAuthors.length > 0; |
| // Migration: Update product_events_kind_check constraint | ||
| await client.query(` | ||
| ALTER TABLE product_events DROP CONSTRAINT IF EXISTS product_events_kind_check; | ||
| ALTER TABLE product_events ADD CONSTRAINT product_events_kind_check CHECK (kind IN (30402, 1)); |
| const oldestEvent = [...prev.productEvents].sort( | ||
| (a, b) => a.created_at - b.created_at | ||
| )[0]; | ||
| if (!oldestEvent) return prev; |
| Showing {(currentPage - 1) * itemsPerPage + 1} to{" "} | ||
| {Math.min(totalEvents, currentPage * itemsPerPage)} of{" "} | ||
| {totalEvents} products |
| const dbEventKeys = new Set<string>(); | ||
|
|
||
| for (const event of productArrayFromDb) { | ||
| if (event && event.id) { | ||
| const key = getEventKey(event); | ||
| dbEventKeys.add(key); |
| } | ||
|
|
||
| if (filters?.search) { | ||
| whereClause += ` AND (${alias}.content ILIKE $${paramIndex} OR ${alias}.tags::text ILIKE $${paramIndex++})`; |
| const filter30402: any = { | ||
| kinds: [30402], // Listings | ||
| limit: 500, | ||
| }; | ||
|
|
||
| const zapsnagFilter: Filter = { | ||
| kinds: [1], | ||
| "#t": ["shopstr-zapsnag", "zapsnag"], | ||
| const filter1: any = { | ||
| kinds: [1], // Zapsnag notes | ||
| "#t": ["zapsnag", "shopstr-zapsnag"], | ||
| limit: 500, | ||
| }; |
| // Refresh products when filters change (server-side) | ||
| useEffect(() => { | ||
| if (!isInitialized) return; | ||
| const filters = getServerFilters(); | ||
| if (!filters) return; | ||
|
|
||
| if (Array.isArray(filters.pubkey) && filters.pubkey.length === 0) { | ||
| productEventContext.setProductEvents([], 0); | ||
| return; | ||
| } | ||
|
|
||
| productEventContext.refreshProducts(filters); | ||
| }, [ |
Description
Added server side pagination and filtering to improve marketplace performance and user experience.
limitandoffsetlogic infetchAllProductsFromDband the/api/db/fetch-productsendpoint to load products in batches.totalfield alongside the product events, enabling accurate pagination UI and "Load More" behavior.fetchProductsto immediately display results from the database while fetching fresh data from Nostr relays in the background.Affirmation