feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View File

@@ -0,0 +1,52 @@
# 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.

View File

@@ -0,0 +1,17 @@
# Documenter — F-007 Categories module
done -> work/artifacts/F-007/documenter.md
## Documentation updated
- `project/README.md`
## Covered
- Category-specific error codes.
- Categories module endpoint table.
- Public slug URL rule: `/categoria/<slug>`.
- Tree/cycle behavior and SEO metadata fields.
- Layout entry for `src/modules/categories`.
## Verification
- `cd project && npm run lint` — PASS after README Prettier normalization.
- `./scripts/verify.sh` — PASS before documentation close.

View File

@@ -0,0 +1,50 @@
# Implementer — F-007 Categories module
done -> work/artifacts/F-007/implementer.md
## Summary
Implemented the categories module as a hexagonal backend slice with PostgreSQL persistence, Fastify routes, validation, tree handling, slug public URLs, and tests.
## Changed files
- `project/src/modules/categories/domain/category.ts`
- `project/src/modules/categories/domain/errors.ts`
- `project/src/modules/categories/domain/ports.ts`
- `project/src/modules/categories/application/category-use-cases.ts`
- `project/src/modules/categories/infrastructure/pg-category-repository.ts`
- `project/src/modules/categories/api/categories.routes.ts`
- `project/src/modules/categories/index.ts`
- `project/migrations/005_categories.js`
- `project/src/app/build-app.ts`
- `project/src/modules/categories/tests/category-use-cases.test.ts`
- `project/src/app/tests/categories.itest.ts`
- `backlog/features.json`
- `work/current.md`
- `work/artifacts/F-007/architect.md`
## Acceptance traceability
1. Duplicate slug returns HTTP 409
- Implemented through `categories_categories.slug UNIQUE` and `CATEGORY_SLUG_EXISTS` mapping.
- Covered by `project/src/app/tests/categories.itest.ts` (`returns HTTP 409 for duplicate slug`).
2. Category tree supports parent/child and blocks cycles
- Implemented with adjacency list `parent_id`, tree builder, self-parent guard, recursive descendant check.
- Covered by unit and integration tests.
3. Public URL is `/categoria/<slug>`, never internal id
- Implemented `GET /categoria/:slug` and serialized `url: /categoria/<slug>`.
- Covered by integration test.
4. `verify.sh` green
- Verified after implementation.
## Commands run
- `cd project && npm run typecheck` — PASS
- `cd project && npm test` — PASS: 55 passed, 25 skipped (DB integration skipped without `TEST_DATABASE_URL`)
- `cd project && npm run lint` — PASS after Prettier normalization
- `cd project && npm run build` — PASS
- `cd project && npm run lint:boundaries` — PASS: 61 files checked
- `./scripts/verify.sh` — PASS
## Notes
- No new npm dependencies.
- Category mutation routes are admin-only using the existing shared auth contract injected from the composition root.
- Public reads (`GET /categories/tree`, `GET /categoria/:slug`) are unauthenticated.
- Corrected delete behavior during review preparation: missing category now maps to `404 NOT_FOUND`; category with children maps to `409 CATEGORY_HAS_CHILDREN`.
- Documentation stage should update README/API notes because new user-facing endpoints were added.

View File

@@ -0,0 +1,22 @@
{
"feature_id": "F-007",
"stage": "close",
"agent": "leader",
"verdict": "APPROVED",
"gates": {
"review": "APPROVED",
"security": "APPROVED",
"qa": "APPROVED"
},
"verify_sh": "PASS",
"closed_summary": "Categories module delivered with tree structure, unique slug public URLs, SEO metadata, admin-only mutations, tests, and documentation.",
"artifacts": [
"work/artifacts/F-007/architect.md",
"work/artifacts/F-007/implementer.md",
"work/artifacts/F-007/reviewer.json",
"work/artifacts/F-007/security.json",
"work/artifacts/F-007/qa.json",
"work/artifacts/F-007/documenter.md"
],
"next_suggested": "F-008"
}

View File

@@ -0,0 +1,41 @@
{
"feature_id": "F-007",
"stage": "qa_gate",
"agent": "qa",
"verdict": "APPROVED",
"acceptance": [
{
"criterion": "Given duplicate slug When category created Then HTTP 409",
"status": "PASS",
"evidence": "Covered by src/app/tests/categories.itest.ts; integration test is skipped automatically when TEST_DATABASE_URL is absent. Repository unique constraint and error mapper reviewed."
},
{
"criterion": "Category tree supports parent/child and blocks cycles",
"status": "PASS",
"evidence": "src/modules/categories/tests/category-use-cases.test.ts PASS; integration tree/cycle test present."
},
{
"criterion": "Public URL is /categoria/<slug>, never internal id",
"status": "PASS",
"evidence": "GET /categoria/:slug implemented and integration test present; serialized response includes /categoria/<slug>."
},
{
"criterion": "verify.sh green",
"status": "PASS",
"evidence": "./scripts/verify.sh — PASS"
}
],
"commands": {
"targeted_tests": "cd project && npm test -- --run src/modules/categories/tests/category-use-cases.test.ts src/app/tests/categories.itest.ts — PASS: 3 passed, 3 skipped without TEST_DATABASE_URL",
"full_tests": "cd project && npm test — PASS: 55 passed, 25 skipped without TEST_DATABASE_URL",
"typecheck": "cd project && npm run typecheck — PASS",
"lint": "cd project && npm run lint — PASS",
"build": "cd project && npm run build — PASS",
"boundaries": "cd project && npm run lint:boundaries — PASS",
"verify": "./scripts/verify.sh — PASS"
},
"findings": [],
"notes": [
"Real PostgreSQL acceptance tests are implemented but not executed in this environment because TEST_DATABASE_URL is not set. This matches existing project test behavior."
]
}

View File

@@ -0,0 +1,26 @@
{
"feature_id": "F-007",
"stage": "review_gate",
"agent": "reviewer",
"verdict": "APPROVED",
"checked": [
"Hexagonal categories module structure",
"Composition root wiring",
"PostgreSQL migration naming and constraints",
"Slug uniqueness and error mapping",
"Tree parent/child handling and cycle prevention",
"Acceptance test coverage"
],
"findings": [],
"corrections_observed": [
"Delete category behavior distinguishes 404 NOT_FOUND from 409 CATEGORY_HAS_CHILDREN."
],
"evidence": {
"typecheck": "cd project && npm run typecheck — PASS",
"lint": "cd project && npm run lint — PASS",
"test": "cd project && npm test — PASS: 55 passed, 25 skipped without TEST_DATABASE_URL",
"build": "cd project && npm run build — PASS",
"boundaries": "cd project && npm run lint:boundaries — PASS",
"verify": "./scripts/verify.sh — PASS"
}
}

View File

@@ -0,0 +1,26 @@
{
"feature_id": "F-007",
"stage": "security_gate",
"agent": "security",
"verdict": "APPROVED",
"checked": [
"No new runtime or dev dependencies",
"Admin-only category mutation routes use server-side shared auth and requireRole('admin')",
"Public category reads expose slugs, not privileged data",
"SQL queries use parameterized pg placeholders; dynamic UPDATE columns are whitelisted",
"Slug, UUID and payload validation use Zod via parseJson",
"Recursive cycle detection uses parameterized CTE",
"Secret scan over F-007 files"
],
"findings": [],
"evidence": {
"npm_audit_runtime": "cd project && npm audit --audit-level=high --omit=dev — PASS: found 0 vulnerabilities",
"secret_scan": "grep over F-007 files found only a non-secret test password fixture",
"verify": "./scripts/verify.sh — PASS",
"tests": "cd project && npm test — PASS"
},
"notes": [
"No client-supplied role/user data is trusted.",
"Category deletion protects non-leaf categories with 409."
]
}