53 lines
3.8 KiB
Markdown
53 lines
3.8 KiB
Markdown
# 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
|
|
1. **Module ownership**: categories owns `categories_categories`; no catalog/product dependency in this slice. Product assignment is explicitly deferred to F-008.
|
|
2. **Hexagonal shape**: domain model + repository port in `domain/`; use cases in `application/`; PostgreSQL adapter in `infrastructure/`; Fastify routes in `api/`. Match existing module style instead of inventing a parallel architecture.
|
|
3. **Tree model**: adjacency list via nullable `parent_id` FK to the same table. It is boring, queryable, and enough for v1. Add recursive reads only where needed for cycle validation/tree response.
|
|
4. **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.
|
|
5. **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`.
|
|
6. **SEO metadata is first-class**: store `seo_title` and `seo_description` columns on the category row, not a side table.
|
|
7. **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 return `409` when children exist.
|
|
|
|
## Domain model
|
|
- `Category`: `id`, `parentId`, `name`, `slug`, `seoTitle`, `seoDescription`, `createdAt`, `updatedAt`.
|
|
- `NewCategory`: `name`, `slug`, optional `parentId`, optional SEO metadata.
|
|
- `CategoryPatch`: optional editable fields, including `parentId` where `null` means move to root.
|
|
|
|
## Error mapping
|
|
- Duplicate slug → `409 CONFLICT` with stable code such as `CATEGORY_SLUG_EXISTS`.
|
|
- Unknown parent → `422 VALIDATION_ERROR` or `404 NOT_FOUND`; choose one and keep tests explicit.
|
|
- Cycle/self-parent → `422 VALIDATION_ERROR` with stable code such as `CATEGORY_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 `parseJson` pattern.
|
|
|
|
## 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.
|