docs(18): create phase plan for global items and public profiles
This commit is contained in:
221
.planning/phases/18-global-items-public-profiles/18-02-PLAN.md
Normal file
221
.planning/phases/18-global-items-public-profiles/18-02-PLAN.md
Normal file
@@ -0,0 +1,221 @@
|
||||
---
|
||||
phase: 18-global-items-public-profiles
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["18-01"]
|
||||
files_modified:
|
||||
- src/server/services/global-item.service.ts
|
||||
- src/server/routes/global-items.ts
|
||||
- src/db/seed-global-items.ts
|
||||
- src/db/seed.ts
|
||||
- src/server/index.ts
|
||||
- tests/services/global-item.service.test.ts
|
||||
- tests/routes/global-items.test.ts
|
||||
autonomous: true
|
||||
requirements: [GLOB-01, GLOB-02, GLOB-03, GLOB-04, GLOB-05]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "GET /api/global-items returns the full global catalog without authentication"
|
||||
- "GET /api/global-items?q=revelate returns only items matching brand or model (case-insensitive)"
|
||||
- "GET /api/global-items/:id returns item details with an ownerCount field"
|
||||
- "POST /api/items/:id/link links a user item to a global item (requires auth)"
|
||||
- "DELETE /api/items/:id/link removes the link (requires auth)"
|
||||
- "Seed data imports on first run and is idempotent on subsequent runs"
|
||||
artifacts:
|
||||
- path: "src/server/services/global-item.service.ts"
|
||||
provides: "searchGlobalItems, getGlobalItemWithOwnerCount, linkItemToGlobal, unlinkItemFromGlobal"
|
||||
exports: ["searchGlobalItems", "getGlobalItemWithOwnerCount", "linkItemToGlobal", "unlinkItemFromGlobal"]
|
||||
- path: "src/server/routes/global-items.ts"
|
||||
provides: "GET /api/global-items, GET /api/global-items/:id"
|
||||
min_lines: 30
|
||||
- path: "src/db/seed-global-items.ts"
|
||||
provides: "seedGlobalItems function"
|
||||
exports: ["seedGlobalItems"]
|
||||
- path: "tests/services/global-item.service.test.ts"
|
||||
provides: "Service tests for GLOB-01 through GLOB-05"
|
||||
min_lines: 50
|
||||
- path: "tests/routes/global-items.test.ts"
|
||||
provides: "Route tests for global item endpoints"
|
||||
min_lines: 40
|
||||
key_links:
|
||||
- from: "src/server/routes/global-items.ts"
|
||||
to: "src/server/services/global-item.service.ts"
|
||||
via: "import and call service functions"
|
||||
pattern: "searchGlobalItems|getGlobalItemWithOwnerCount"
|
||||
- from: "src/server/index.ts"
|
||||
to: "src/server/routes/global-items.ts"
|
||||
via: "app.route registration"
|
||||
pattern: "app\\.route.*global-items"
|
||||
- from: "src/db/seed.ts"
|
||||
to: "src/db/seed-global-items.ts"
|
||||
via: "seedGlobalItems call in seedDefaults"
|
||||
pattern: "seedGlobalItems"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the complete global item catalog backend: service layer with ILIKE search and owner count, route handlers for public GET + authenticated link/unlink, seed script integration, and auth middleware updates for public access.
|
||||
|
||||
Purpose: Delivers GLOB-01 through GLOB-05 server-side. Users can search gear, view details with owner counts, and link personal items to global entries.
|
||||
Output: global-item.service.ts, global-items.ts routes, seed-global-items.ts, updated index.ts + seed.ts, service + route tests
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/18-global-items-public-profiles/18-CONTEXT.md
|
||||
@.planning/phases/18-global-items-public-profiles/18-RESEARCH.md
|
||||
@.planning/phases/18-global-items-public-profiles/18-01-SUMMARY.md
|
||||
|
||||
@src/db/schema.ts
|
||||
@src/server/services/item.service.ts
|
||||
@src/server/routes/items.ts
|
||||
@src/server/index.ts
|
||||
@src/server/middleware/auth.ts
|
||||
@src/db/seed.ts
|
||||
@tests/helpers/db.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01 outputs (schema.ts additions): -->
|
||||
export const globalItems = pgTable("global_items", {
|
||||
id: serial("id").primaryKey(),
|
||||
brand: text("brand").notNull(),
|
||||
model: text("model").notNull(),
|
||||
category: text("category"),
|
||||
weightGrams: doublePrecision("weight_grams"),
|
||||
priceCents: integer("price_cents"),
|
||||
imageUrl: text("image_url"),
|
||||
description: text("description"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const itemGlobalLinks = pgTable("item_global_links", {
|
||||
id: serial("id").primaryKey(),
|
||||
itemId: integer("item_id").notNull().references(() => items.id, { onDelete: "cascade" }).unique(),
|
||||
globalItemId: integer("global_item_id").notNull().references(() => globalItems.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
<!-- From schemas.ts: -->
|
||||
export const searchGlobalItemsSchema = z.object({ q: z.string().optional() });
|
||||
export const linkItemSchema = z.object({ globalItemId: z.number().int().positive() });
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Global item service + seed script + tests</name>
|
||||
<files>src/server/services/global-item.service.ts, src/db/seed-global-items.ts, src/db/seed.ts, tests/services/global-item.service.test.ts</files>
|
||||
<read_first>src/server/services/item.service.ts, src/db/seed.ts, tests/helpers/db.ts, src/db/schema.ts, src/shared/schemas.ts</read_first>
|
||||
<behavior>
|
||||
- searchGlobalItems(db) returns all global items when no query provided
|
||||
- searchGlobalItems(db, "revelate") returns only items with "revelate" in brand or model (case-insensitive)
|
||||
- searchGlobalItems(db, "100%") does not match everything (wildcard chars escaped)
|
||||
- getGlobalItemWithOwnerCount(db, id) returns item with ownerCount: 0 when no links exist
|
||||
- getGlobalItemWithOwnerCount(db, id) returns ownerCount: 2 when 2 user items are linked
|
||||
- getGlobalItemWithOwnerCount(db, nonExistentId) returns null
|
||||
- linkItemToGlobal(db, itemId, globalItemId) creates link, returns link row
|
||||
- linkItemToGlobal(db, itemId, globalItemId) when already linked throws/returns error
|
||||
- unlinkItemFromGlobal(db, itemId) removes the link
|
||||
- seedGlobalItems(db) inserts seed data on first call, skips on second call (idempotent)
|
||||
</behavior>
|
||||
<action>
|
||||
**global-item.service.ts**: Create at `src/server/services/global-item.service.ts`. Follow the existing service pattern (import db type from `../../db/index.ts`, use `type Db = typeof prodDb`).
|
||||
|
||||
Functions:
|
||||
1. `searchGlobalItems(db: Db, query?: string)` — No userId param (per D-03, public data). Uses `ilike` from drizzle-orm on brand and model columns. Escape `%` and `_` in query before wrapping in `%..%` pattern. Return `db.select().from(globalItems)` with optional where clause using `or(ilike(brand, pattern), ilike(model, pattern))`.
|
||||
|
||||
2. `getGlobalItemWithOwnerCount(db: Db, id: number)` — Select from globalItems where id matches. Then count from itemGlobalLinks where globalItemId matches. Return `{ ...item, ownerCount }` or null.
|
||||
|
||||
3. `linkItemToGlobal(db: Db, itemId: number, globalItemId: number)` — Insert into itemGlobalLinks. Let unique constraint on itemId handle duplicates (catch and return 409-style error).
|
||||
|
||||
4. `unlinkItemFromGlobal(db: Db, itemId: number)` — Delete from itemGlobalLinks where itemId matches. Return deleted count.
|
||||
|
||||
**seed-global-items.ts**: Create at `src/db/seed-global-items.ts`.
|
||||
- `export async function seedGlobalItems(db: Db)` — Check if any rows exist in globalItems table. If yes, return early. If no, import from `./global-items-seed.json` and insert all rows.
|
||||
|
||||
**seed.ts**: Add `seedGlobalItems(prodDb)` call to the existing `seedDefaults()` function (after existing seeds).
|
||||
|
||||
**Tests**: Write tests FIRST (TDD). Use `createTestDb()` from test helper. Insert test global items directly in test setup. For owner count tests, create test user items and link them.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bun test tests/services/global-item.service.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q "searchGlobalItems" src/server/services/global-item.service.ts
|
||||
- grep -q "getGlobalItemWithOwnerCount" src/server/services/global-item.service.ts
|
||||
- grep -q "linkItemToGlobal" src/server/services/global-item.service.ts
|
||||
- grep -q "unlinkItemFromGlobal" src/server/services/global-item.service.ts
|
||||
- grep -q "seedGlobalItems" src/db/seed-global-items.ts
|
||||
- grep -q "seedGlobalItems" src/db/seed.ts
|
||||
- grep -q "ilike" src/server/services/global-item.service.ts
|
||||
- test -f tests/services/global-item.service.test.ts
|
||||
</acceptance_criteria>
|
||||
<done>All 4 service functions pass tests. Seed script is idempotent. ILIKE search works case-insensitively with wildcard escaping.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Global item routes + auth middleware update + route tests</name>
|
||||
<files>src/server/routes/global-items.ts, src/server/routes/items.ts, src/server/index.ts, tests/routes/global-items.test.ts</files>
|
||||
<read_first>src/server/routes/items.ts, src/server/index.ts, src/server/middleware/auth.ts, tests/routes/setups.test.ts</read_first>
|
||||
<action>
|
||||
**global-items.ts route**: Create at `src/server/routes/global-items.ts`. Follow existing route pattern (Hono app with Env type).
|
||||
|
||||
1. `GET /` (maps to `/api/global-items`) — per D-16. Read `q` from query string. Call `searchGlobalItems(db, q)`. Return JSON array. No auth needed.
|
||||
|
||||
2. `GET /:id` (maps to `/api/global-items/:id`) — per D-17. Parse id with `parseId`. Call `getGlobalItemWithOwnerCount(db, id)`. Return 404 if null, otherwise JSON with ownerCount.
|
||||
|
||||
**items.ts route updates** — per D-18, D-19. Add two new endpoints to existing item routes:
|
||||
|
||||
3. `POST /:id/link` — Validate body with `linkItemSchema` via zValidator. Get userId from context. Verify the item belongs to the user (call getItemById first). Call `linkItemToGlobal(db, itemId, globalItemId)`. Return 201 on success, 409 if already linked, 404 if item not found.
|
||||
|
||||
4. `DELETE /:id/link` — Get userId. Verify item ownership. Call `unlinkItemFromGlobal(db, itemId)`. Return 200.
|
||||
|
||||
**index.ts updates**:
|
||||
1. Import `globalItemRoutes` from routes/global-items.ts
|
||||
2. Register: `app.route("/api/global-items", globalItemRoutes)` — place after existing route registrations.
|
||||
3. Update auth middleware skip: Add `if (c.req.path.startsWith("/api/global-items") && c.req.method === "GET") return next();` before the `requireAuth` call, per Research Pattern 3 recommendation.
|
||||
|
||||
**Route tests**: Follow existing route test pattern (from tests/routes/setups.test.ts). Create test Hono app with db middleware + auth middleware. Test:
|
||||
- GET /api/global-items returns 200 without auth
|
||||
- GET /api/global-items?q=tent filters results
|
||||
- GET /api/global-items/:id returns item with ownerCount
|
||||
- GET /api/global-items/999 returns 404
|
||||
- POST /api/items/:id/link returns 201
|
||||
- POST /api/items/:id/link duplicate returns 409
|
||||
- DELETE /api/items/:id/link returns 200
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bun test tests/routes/global-items.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q "global-items" src/server/index.ts
|
||||
- grep -q "globalItemRoutes\|globalItems" src/server/routes/global-items.ts
|
||||
- grep -q "link" src/server/routes/items.ts
|
||||
- grep -q "api/global-items" src/server/index.ts
|
||||
- test -f tests/routes/global-items.test.ts
|
||||
</acceptance_criteria>
|
||||
<done>Global item endpoints work: search returns filtered results, detail includes ownerCount, link/unlink modify junction table. Auth middleware allows unauthenticated GET access to /api/global-items. All route tests pass.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `bun test tests/services/global-item.service.test.ts` — all service tests pass
|
||||
- `bun test tests/routes/global-items.test.ts` — all route tests pass
|
||||
- `bun test` — full suite passes (no regressions)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
Global item catalog is fully functional server-side. Search, detail with owner count, link/unlink all work. Seed data imports idempotently. Public GET endpoints work without auth. All tests pass.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/18-global-items-public-profiles/18-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user