Files
GearBox/.planning/phases/18-global-items-public-profiles/18-02-PLAN.md

11 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
18-global-items-public-profiles 02 execute 2
18-01
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
true
GLOB-01
GLOB-02
GLOB-03
GLOB-04
GLOB-05
truths artifacts key_links
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
path provides exports
src/server/services/global-item.service.ts searchGlobalItems, getGlobalItemWithOwnerCount, linkItemToGlobal, unlinkItemFromGlobal
searchGlobalItems
getGlobalItemWithOwnerCount
linkItemToGlobal
unlinkItemFromGlobal
path provides min_lines
src/server/routes/global-items.ts GET /api/global-items, GET /api/global-items/:id 30
path provides exports
src/db/seed-global-items.ts seedGlobalItems function
seedGlobalItems
path provides min_lines
tests/services/global-item.service.test.ts Service tests for GLOB-01 through GLOB-05 50
path provides min_lines
tests/routes/global-items.test.ts Route tests for global item endpoints 40
from to via pattern
src/server/routes/global-items.ts src/server/services/global-item.service.ts import and call service functions searchGlobalItems|getGlobalItemWithOwnerCount
from to via pattern
src/server/index.ts src/server/routes/global-items.ts app.route registration app.route.*global-items
from to via pattern
src/db/seed.ts src/db/seed-global-items.ts seedGlobalItems call in seedDefaults 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

<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/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 <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> 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:

  1. 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.

  2. 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 <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> 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)

<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>

After completion, create `.planning/phases/18-global-items-public-profiles/18-02-SUMMARY.md`