--- phase: 26-discovery-landing-page plan: "01" subsystem: server/services tags: [discovery, service-layer, cursor-pagination, tdd, drizzle] dependency_graph: requires: [] provides: [discovery.service.ts] affects: [26-02, 26-03] tech_stack: added: [] patterns: [cursor-pagination, CursorPage-response-shape, post-query-cursor-filter] key_files: created: - src/server/services/discovery.service.ts - tests/services/discovery.service.test.ts modified: [] decisions: - "Composite cursor for setups: itemCount_id format, filtered post-query in JS for simplicity with grouped SQL" - "createdAt ISO string cursor for recent items: standard timestamp-based pagination" - "No cursor pagination for trending categories: bounded small list (< 50), simple limit is sufficient per RESEARCH.md open question 3" - "Shared CursorPage generic interface for consistent cursor response shape across setups and items" metrics: duration: "~2 min" completed_date: "2026-04-10" tasks_completed: 1 tasks_total: 1 files_created: 2 files_modified: 0 --- # Phase 26 Plan 01: Discovery Service Summary **One-liner:** Discovery service layer with cursor pagination using Drizzle ORM — getPopularSetups (itemCount_id composite cursor), getRecentGlobalItems (ISO timestamp cursor), getTrendingCategories (simple limit). ## Tasks Completed | Task | Name | Commit | Files | |------|------|--------|-------| | 1 (RED) | Discovery service TDD — failing tests | 06b6e93 | tests/services/discovery.service.test.ts | | 1 (GREEN) | Discovery service TDD — implementation | d1f8a7a | src/server/services/discovery.service.ts | ## What Was Built ### `src/server/services/discovery.service.ts` Three exported async functions: **`getPopularSetups(db, limit=6, cursor?)`** - JOINs setups → setupItems (count) → users (displayName) - WHERE isPublic=true, GROUP BY setup fields - ORDER BY item count DESC, id DESC - Cursor: `itemCount_id` composite string, filtered post-query in JS - Returns `CursorPage<{ id, name, createdAt, itemCount, creatorName }>` **`getRecentGlobalItems(db, limit=8, cursor?)`** - SELECT * FROM globalItems WHERE createdAt < cursor (if provided) - ORDER BY createdAt DESC, LIMIT limit+1 for hasMore detection - Cursor: ISO timestamp of last item's createdAt - Returns `CursorPage` **`getTrendingCategories(db, limit=12)`** - SELECT category, COUNT(id) FROM globalItems WHERE category IS NOT NULL - GROUP BY category, ORDER BY count DESC - Returns plain `Array<{ name: string; itemCount: number }>` (no cursor) ### `tests/services/discovery.service.test.ts` 11 tests covering: - `getPopularSetups`: ordering by count desc, private setup exclusion, hasMore/nextCursor, second page deduplication, creatorName from users.displayName - `getRecentGlobalItems`: ordering by createdAt desc, hasMore/nextCursor, second page deduplication - `getTrendingCategories`: ordering by count desc, null category exclusion, empty state ## Deviations from Plan None — plan executed exactly as written. The test used a dynamic import pattern for `eq` which was corrected to a static import (minor code quality fix before RED commit). ## Verification - `bun test tests/services/discovery.service.test.ts`: 11 pass, 0 fail - `bun test` full suite: 290 tests — same pass/fail ratio as before (15 pre-existing failures from `withImageUrl` storage service export issue, unrelated to this plan) ## Self-Check Checked below.