10 KiB
10 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 26-discovery-landing-page | 01 | tdd | 1 |
|
true |
|
|
Purpose: Provides the data layer for the discovery landing page feed sections. TDD approach ensures correct ordering, filtering, and pagination before wiring to routes.
Output: discovery.service.ts with three exported functions, fully tested.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/26-discovery-landing-page/26-CONTEXT.md @.planning/phases/26-discovery-landing-page/26-RESEARCH.md @src/db/schema.ts @tests/helpers/db.ts @tests/services/global-item.service.test.ts (pattern reference for test structure) @src/server/services/global-item.service.ts (pattern reference for service structure) Task 1: Discovery service with TDD — popular setups, recent items, trending categories src/server/services/discovery.service.ts, tests/services/discovery.service.test.ts - src/db/schema.ts (table definitions: globalItems, setups, setupItems, users) - tests/helpers/db.ts (createTestDb pattern) - tests/services/global-item.service.test.ts (test file structure, insertGlobalItem helper pattern) - src/server/services/global-item.service.ts (service function patterns — how db param is typed, import style) - getPopularSetups: returns only public setups (isPublic=true), ordered by setupItems count descending then by id descending. Each result includes id, name, createdAt, itemCount (number), creatorName (string|null from users.displayName). Private setups are excluded. - getPopularSetups cursor: given cursor "5_42" (itemCount=5, id=42), returns setups where (itemCount < 5) OR (itemCount === 5 AND id < 42). hasMore is true when rows exceed limit. - getRecentGlobalItems: returns globalItems ordered by createdAt descending. Each result includes all globalItems columns. - getRecentGlobalItems cursor: given cursor ISO timestamp, returns items where createdAt < cursor timestamp. hasMore is true when rows exceed limit. - getTrendingCategories: returns { name: string, itemCount: number }[] ordered by itemCount descending. Excludes rows where globalItems.category IS NULL. No cursor pagination (simple limit). - getTrendingCategories empty: returns empty array when no items have a category set. **RED phase — write tests first in `tests/services/discovery.service.test.ts`:**Use the same test structure as `global-item.service.test.ts`:
- Import `{ beforeEach, describe, expect, it }` from `"bun:test"`
- Import schema tables: `globalItems, setups, setupItems, users` from `../../src/db/schema.ts`
- Import `createTestDb` from `../helpers/db.ts`
- Import service functions from `../../src/server/services/discovery.service.ts`
- Type `TestDb = Awaited<ReturnType<typeof createTestDb>>`
Helper functions needed in test file:
```typescript
async function insertGlobalItem(db, data: { brand: string; model: string; category?: string }) {
const [row] = await db.insert(globalItems).values({ brand: data.brand, model: data.model, category: data.category ?? null }).returning();
return row;
}
async function insertPublicSetup(db, userId: number, name: string, itemIds: number[]) {
const [setup] = await db.insert(setups).values({ name, userId, isPublic: true }).returning();
// Insert items into the items table first, then setupItems
for (const itemId of itemIds) {
await db.insert(setupItems).values({ setupId: setup.id, itemId });
}
return setup;
}
```
Note: `setupItems.itemId` references the `items` table, not `globalItems`. So tests need to insert real `items` rows first (use `db.insert(items).values({ name: "Test", categoryId: 1, userId })`) before creating setupItems.
Write tests for:
1. `getPopularSetups` — seed 2 public setups with different item counts, verify order is by count desc
2. `getPopularSetups` — seed 1 private setup, verify it's excluded
3. `getPopularSetups` — cursor pagination: seed 3 setups, fetch limit=1, verify hasMore=true and nextCursor returned, fetch page 2 with cursor, verify different setup returned
4. `getPopularSetups` — includes creatorName from users.displayName (seed user with displayName, verify it appears)
5. `getRecentGlobalItems` — seed 3 items with different createdAt, verify order is newest first
6. `getRecentGlobalItems` — cursor pagination: fetch limit=1, verify hasMore, fetch page 2 with cursor
7. `getTrendingCategories` — seed items in 3 categories with different counts, verify order by count desc
8. `getTrendingCategories` — seed item with null category, verify it's excluded from results
**GREEN phase — create `src/server/services/discovery.service.ts`:**
Import from drizzle-orm: `count, desc, eq, lt, sql, and, isNotNull`
Import schema: `globalItems, setups, setupItems, users`
Import types: infer Db type the same way as `global-item.service.ts` does
Three exported functions:
`getPopularSetups(db: Db, limit = 6, cursor?: string)`:
- Query: SELECT setups.id, setups.name, setups.createdAt, COUNT(setupItems.id) AS itemCount, users.displayName AS creatorName
- FROM setups LEFT JOIN setupItems ON setupItems.setupId = setups.id LEFT JOIN users ON users.id = setups.userId
- WHERE setups.isPublic = true
- GROUP BY setups.id, setups.name, setups.createdAt, users.displayName
- ORDER BY itemCount DESC, setups.id DESC
- LIMIT limit + 1
For cursor: parse "itemCount_id" format. Use SQL HAVING or WHERE with subquery. Since Drizzle groupBy with cursor is tricky, use the post-filter approach from RESEARCH.md:
- Fetch more rows (limit * 2 + 1 if cursor provided)
- Filter in JS: keep rows where (itemCount < cursorCount) OR (itemCount === cursorCount AND id < cursorId)
- Slice to limit + 1
Return `{ items: T[], nextCursor: string | null, hasMore: boolean }` shape:
- hasMore = rows.length > limit
- items = hasMore ? rows.slice(0, limit) : rows
- nextCursor = hasMore ? `${items[items.length-1].itemCount}_${items[items.length-1].id}` : null
`getRecentGlobalItems(db: Db, limit = 8, cursor?: string)`:
- Query: SELECT * FROM globalItems WHERE (cursor ? createdAt < new Date(cursor) : true) ORDER BY createdAt DESC LIMIT limit + 1
- Return `{ items, nextCursor, hasMore }` — nextCursor is ISO string of last item's createdAt
`getTrendingCategories(db: Db, limit = 12)`:
- Query: SELECT category AS name, COUNT(id) AS itemCount FROM globalItems WHERE category IS NOT NULL GROUP BY category ORDER BY COUNT(id) DESC LIMIT limit
- Return array directly (no cursor pagination per RESEARCH.md open question 3)
**REFACTOR:** Ensure all functions handle edge cases (empty results, no cursor). Extract shared `buildCursorResponse` helper if patterns are identical.
cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/services/discovery.service.test.ts
- tests/services/discovery.service.test.ts contains `describe("getPopularSetups"` and `describe("getRecentGlobalItems"` and `describe("getTrendingCategories"`
- tests/services/discovery.service.test.ts contains at least 8 `it(` calls
- src/server/services/discovery.service.ts contains `export async function getPopularSetups(`
- src/server/services/discovery.service.ts contains `export async function getRecentGlobalItems(`
- src/server/services/discovery.service.ts contains `export async function getTrendingCategories(`
- src/server/services/discovery.service.ts contains `isNotNull(globalItems.category)` (null category exclusion)
- src/server/services/discovery.service.ts contains `eq(setups.isPublic, true)` (public-only filter)
- src/server/services/discovery.service.ts contains `nextCursor` and `hasMore` in return shapes
- `bun test tests/services/discovery.service.test.ts` exits 0
All three discovery service functions pass their tests: correct ordering, cursor pagination works for setups and items, categories exclude nulls, and hasMore/nextCursor response shape is correct.
- `bun test tests/services/discovery.service.test.ts` — all tests pass
- `bun test` — full suite still green (no regressions)
<success_criteria>
- Three exported service functions exist with cursor pagination (setups, items) and simple limit (categories)
- All tests pass covering ordering, filtering, cursor, and edge cases
- Service functions are pure (take db instance, no HTTP awareness) </success_criteria>