3.8 KiB
3.8 KiB
Architect — F-007 Categories module
done -> work/artifacts/F-007/architect.md
Deliverables
src/modules/categories/with domain, application, infrastructure and api layers.- PostgreSQL migration for
categories_categories. - Unit and integration tests covering slug uniqueness, tree hierarchy, cycle prevention and public slug URL behavior.
Key decisions
- Module ownership: categories owns
categories_categories; no catalog/product dependency in this slice. Product assignment is explicitly deferred to F-008. - Hexagonal shape: domain model + repository port in
domain/; use cases inapplication/; PostgreSQL adapter ininfrastructure/; Fastify routes inapi/. Match existing module style instead of inventing a parallel architecture. - Tree model: adjacency list via nullable
parent_idFK to the same table. It is boring, queryable, and enough for v1. Add recursive reads only where needed for cycle validation/tree response. - Slug as public identity: API must expose
/categoria/<slug>for public reads; internal UUID can exist but must not be required for public URLs. Slug is globally unique for the slice, not sibling-scoped, because backlog says unique slug per category. - Cycle prevention in application + DB-safe query: create/update parent operations must reject self-parenting, unknown parent IDs, and descendant-as-parent. Use a recursive CTE in the repository to detect descendants before updating
parent_id. - SEO metadata is first-class: store
seo_titleandseo_descriptioncolumns on the category row, not a side table. - No dependencies new: current stack already has Fastify, Zod, pg and Vitest.
Suggested API contract
GET /categories/tree→{ items: CategoryTreeNode[] }for navigation/admin tree consumers.GET /categoria/:slug→ public category read by slug; URL uses slug, never internal ID.POST /categories→ create category; admin-only once auth is wired into the route.PATCH /categories/:id→ update metadata/slug/parent; admin-only; rejects cycles.DELETE /categories/:id→ delete only leaf categories for v1, or return409when children exist.
Domain model
Category:id,parentId,name,slug,seoTitle,seoDescription,createdAt,updatedAt.NewCategory:name,slug, optionalparentId, optional SEO metadata.CategoryPatch: optional editable fields, includingparentIdwherenullmeans move to root.
Error mapping
- Duplicate slug →
409 CONFLICTwith stable code such asCATEGORY_SLUG_EXISTS. - Unknown parent →
422 VALIDATION_ERRORor404 NOT_FOUND; choose one and keep tests explicit. - Cycle/self-parent →
422 VALIDATION_ERRORwith stable code such asCATEGORY_TREE_CYCLE. - Delete category with children →
409 CONFLICT.
Test plan
- Unit: slug duplicate conflict mapping in create/update use cases.
- Unit: update parent rejects self-parent and descendant-as-parent.
- Integration: migration up/down/fresh verify remains green.
- Integration/API: duplicate create returns HTTP 409.
- Integration/API: tree supports parent/child and returns nested structure.
- Integration/API:
GET /categoria/<slug>succeeds without internal id in request path.
Security posture
- Public slug reads can be unauthenticated.
- Mutations should be admin-only using shared auth (
Authenticate,requireRole) if this feature exposes mutation routes now. Do not accept role/user data from the body. - Validate all route inputs with Zod through existing
parseJsonpattern.
Risks
- If admin auth is injected into categories routes, composition root must pass the existing authenticator just like users. Keep this cross-module dependency in
src/app, not inside the categories module. - Recursive CTE bugs can silently allow invalid trees; tests must cover root → child → grandchild then moving root under grandchild.