4.4 KiB
4.4 KiB
F-058 — Implementer evidence
Scope delivered
Admin /products showed a 🌿 leaf emoji for every row, even for products
that have an image uploaded. The leaf is the fallback in the table cell when
p.imageUrl is falsy. Two layers contributed to the bug:
- Backend —
/catalog/products(admin list) and/products/search(public search) both calledserializeProduct(product)without theimagesargument, so the response always carriedimages: []even when rows existed incatalog_product_images. Other endpoints (/productos/:slug,/products/:id,/products/:id/images) already passed the images correctly. - Frontend —
apps/admin/src/app/(dashboard)/products/page.tsxreadp.imageUrl(a single string), which the API never returns. The backend instead exposesimages: ProductImage[]. TheimageUrlfield on the type was dead code.
Changes
Backend
project/src/modules/catalog/domain/ports.ts
- Added
listByProductIds(productIds): Promise<ProductImage[]>to theProductImageRepositoryinterface.
project/src/modules/catalog/infrastructure/pg-product-image-repository.ts
- Implemented
listByProductIdswith a singleANY($1::uuid[])query, ordered by(product_id, position, created_at, id)so the per-product ordering matcheslistByProductId.
project/src/modules/catalog/api/catalog.routes.ts
- New helper
groupImagesByProductIdthat buckets images byproductId. /catalog/productsnow callsimages.listByProductIds(...)after the page is fetched and groups the result;serializeProductreceives the per-product list (defaulting to[]when none)./products/searchdoes the same. Logging of the search duration is preserved.
project/src/modules/catalog/tests/image-use-cases.test.ts
FakeImageRepositoryupdated to implement the new interface method (mirrors the SQL semantics: filter byproductId IN set, sort by position/created/id).
Frontend
project/apps/admin/src/app/(dashboard)/products/page.tsx
- The product cell now reads
p.images?.[0]?.urlinstead ofp.imageUrl. The 40 px thumbnail URL is computed the same way:images[0].url.replace('/uploads/', '/uploads/40/').
Acceptance traceability
| Acceptance criterion | How it is met |
|---|---|
GET /api/catalog/products returns images for products with attached images |
The endpoint now calls images.listByProductIds and feeds the per-product list to serializeProduct. Verified with curl: images=1 url=/uploads/8223b962...jpg for the seeded product. |
GET /api/products/search returns images for products with attached images |
Same fix applied. Verified with curl: images=1 for the same product. |
Admin /products shows 40px thumbnail (not leaf) for products with images |
Frontend now reads images[0].url; the uploads/40/<file> URL is requested, served 200. |
| No regression in uploads pipeline | Only added a read query and a frontend prop switch. No changes to attach, detach, reorder, or storage. |
verify.sh is green |
Exit 0. |
Manual verification
$ curl /api/catalog/products?limit=5 (admin auth)
{"items":[{"name":"Proteina Guisante Ecologica",
"images":[{"url":"/uploads/8223b962-642c-4c95-bc2e-37c2d2fad4ac.jpg",
"role":"gallery",...}]}, ...]}
$ curl /api/products/search?limit=20 (public)
… "Proteina Guisante Ecologica" …
images: [{ url: "/uploads/8223b962-642c-4c95-bc2e-37c2d2fad4ac.jpg" }]
$ curl http://192.168.18.93:3004/uploads/40/8223b962-642c-4c95-bc2e-37c2d2fad4ac.jpg
HTTP 200, 568 bytes (thumbnail served)
Build verification
npm run typecheck(project/) — exit 0npm test(project/) — 124 passed, 56 skippednpm run build(project/) — exit 0npx tsc --noEmit(apps/admin, frontend, storefront) — exit 0./scripts/verify.sh— exit 0
Files touched
project/src/modules/catalog/api/catalog.routes.ts (modified)
project/src/modules/catalog/domain/ports.ts (modified)
project/src/modules/catalog/infrastructure/pg-product-image-repository.ts (modified)
project/src/modules/catalog/tests/image-use-cases.test.ts (modified)
project/apps/admin/src/app/(dashboard)/products/page.tsx (modified)