3.2 KiB
3.2 KiB
Architect — F-011 Product images
Decision
Keep product images inside the catalog module. Product and variant images are catalog-owned presentation metadata, not a separate bounded context yet.
Data model
Add catalog_product_images:
id uuid primary key default gen_random_uuid()product_id uuid not null references catalog_products(id) on delete cascadevariant_id uuid null references catalog_product_variants(id) on delete cascadeurl text not nullalt_text text not nullposition integer not nullrole text not null check (role in ('main', 'gallery'))- timestamps
Rules:
position >= 0variant_idis optional; when present it must belong to the sameproduct_id.- Exactly one
mainimage per product-level set and per variant-level set. Implement with partial unique indexes:(product_id) WHERE variant_id IS NULL AND role = 'main'(variant_id) WHERE variant_id IS NOT NULL AND role = 'main'
- Stable ordering by
position, thencreated_at, thenid.
Domain/application
Add image domain types in catalog:
ProductImageRole = 'main' | 'gallery'ProductImagewith productId, optional variantId, url, altText, position, role.NewProductImageandProductImagePatch.
Add ProductImageRepository port. It is the boundary that makes storage swappable:
- API/application depend only on the port.
- Local-first storage is represented as URLs accepted by the adapter/repository path for this slice.
- Future CDN/image processing must touch infrastructure only.
Use cases:
ListProductImages(productId, variantId?)AttachProductImage(productId, input); verify product exists, and ifvariantIdis passed verify it belongs to that product.DetachProductImage(productId, imageId)ReorderProductImages(productId, items); validates all listed images belong to the same product/scope.
API
Admin mutations, public read through product serialization:
GET /productos/:slugincludes orderedimageslist for product-level images.GET /products/searchmay continue returning product core only unless cheap to include images; acceptance only requires product exposes ordered image list.GET /products/:id/imagesreturns ordered product-level images.POST /products/:id/imagesattaches product or variant image. Body:url,altText,role, optionalvariantId, optionalposition.DELETE /products/:id/images/:imageIddetaches.PATCH /products/:id/images/reorderaccepts ordered{ imageId, position }[].
Validation:
- URL must be a bounded string and should be parseable URL or local absolute path beginning with
/. altTextis required, trimmed, max 300.roleuses the domain enum.
Tests/evidence
- Unit test use cases with fake repositories:
- ordered list preserves repository ordering.
- attach rejects missing product.
- attach rejects variant from another product.
- Repository/migration shape can be covered by existing migration integration test when
TEST_DATABASE_URLexists. npm test,npm run typecheck, and root./scripts/verify.shmust be green.
Out of scope guardrails
No binary upload, CDN, resizing, optimization pipeline, or external object-store SDK in F-011. No new runtime dependency expected.