--- 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" --- 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 @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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 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" }), }); export const searchGlobalItemsSchema = z.object({ q: z.string().optional() }); export const linkItemSchema = z.object({ globalItemId: z.number().int().positive() }); Task 1: Global item service + seed script + tests src/server/services/global-item.service.ts, src/db/seed-global-items.ts, src/db/seed.ts, tests/services/global-item.service.test.ts src/server/services/item.service.ts, src/db/seed.ts, tests/helpers/db.ts, src/db/schema.ts, src/shared/schemas.ts - 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) **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. bun test tests/services/global-item.service.test.ts - 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 All 4 service functions pass tests. Seed script is idempotent. ILIKE search works case-insensitively with wildcard escaping. Task 2: Global item routes + auth middleware update + route tests src/server/routes/global-items.ts, src/server/routes/items.ts, src/server/index.ts, tests/routes/global-items.test.ts src/server/routes/items.ts, src/server/index.ts, src/server/middleware/auth.ts, tests/routes/setups.test.ts **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 bun test tests/routes/global-items.test.ts - 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 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. - `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) 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. After completion, create `.planning/phases/18-global-items-public-profiles/18-02-SUMMARY.md`