66 lines
3.2 KiB
Markdown
66 lines
3.2 KiB
Markdown
# 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 cascade`
|
|
- `variant_id uuid null references catalog_product_variants(id) on delete cascade`
|
|
- `url text not null`
|
|
- `alt_text text not null`
|
|
- `position integer not null`
|
|
- `role text not null check (role in ('main', 'gallery'))`
|
|
- timestamps
|
|
|
|
Rules:
|
|
- `position >= 0`
|
|
- `variant_id` is optional; when present it must belong to the same `product_id`.
|
|
- Exactly one `main` image 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`, then `created_at`, then `id`.
|
|
|
|
## Domain/application
|
|
Add image domain types in catalog:
|
|
- `ProductImageRole = 'main' | 'gallery'`
|
|
- `ProductImage` with productId, optional variantId, url, altText, position, role.
|
|
- `NewProductImage` and `ProductImagePatch`.
|
|
|
|
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 if `variantId` is 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/:slug` includes ordered `images` list for product-level images.
|
|
- `GET /products/search` may continue returning product core only unless cheap to include images; acceptance only requires product exposes ordered image list.
|
|
- `GET /products/:id/images` returns ordered product-level images.
|
|
- `POST /products/:id/images` attaches product or variant image. Body: `url`, `altText`, `role`, optional `variantId`, optional `position`.
|
|
- `DELETE /products/:id/images/:imageId` detaches.
|
|
- `PATCH /products/:id/images/reorder` accepts ordered `{ imageId, position }[]`.
|
|
|
|
Validation:
|
|
- URL must be a bounded string and should be parseable URL or local absolute path beginning with `/`.
|
|
- `altText` is required, trimmed, max 300.
|
|
- `role` uses 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_URL` exists.
|
|
- `npm test`, `npm run typecheck`, and root `./scripts/verify.sh` must 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.
|