feat(ADM-018): completed feature
This commit is contained in:
65
work/artifacts/F-011/architect.md
Normal file
65
work/artifacts/F-011/architect.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user