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 |
|
|
true |
|
|
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:
-
searchGlobalItems(db: Db, query?: string)— No userId param (per D-03, public data). Usesilikefrom drizzle-orm on brand and model columns. Escape%and_in query before wrapping in%..%pattern. Returndb.select().from(globalItems)with optional where clause usingor(ilike(brand, pattern), ilike(model, pattern)). -
getGlobalItemWithOwnerCount(db: Db, id: number)— Select from globalItems where id matches. Then count from itemGlobalLinks where globalItemId matches. Return{ ...item, ownerCount }or null. -
linkItemToGlobal(db: Db, itemId: number, globalItemId: number)— Insert into itemGlobalLinks. Let unique constraint on itemId handle duplicates (catch and return 409-style error). -
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.jsonand 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.
-
GET /(maps to/api/global-items) — per D-16. Readqfrom query string. CallsearchGlobalItems(db, q). Return JSON array. No auth needed. -
GET /:id(maps to/api/global-items/:id) — per D-17. Parse id withparseId. CallgetGlobalItemWithOwnerCount(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:
-
POST /:id/link— Validate body withlinkItemSchemavia zValidator. Get userId from context. Verify the item belongs to the user (call getItemById first). CalllinkItemToGlobal(db, itemId, globalItemId). Return 201 on success, 409 if already linked, 404 if item not found. -
DELETE /:id/link— Get userId. Verify item ownership. CallunlinkItemFromGlobal(db, itemId). Return 200.
index.ts updates:
- Import
globalItemRoutesfrom routes/global-items.ts - Register:
app.route("/api/global-items", globalItemRoutes)— place after existing route registrations. - Update auth middleware skip: Add
if (c.req.path.startsWith("/api/global-items") && c.req.method === "GET") return next();before therequireAuthcall, 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.
<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`