feat(media): full Clean Architecture scaffold

Media is now a complete vertical-feature package mirroring auth/blog
structure: entities (models + errors), application (repositories +
use-cases), infrastructure (real Payload-backed + mock siblings),
interface-adapters (per-use-case controllers), DI (symbols + module +
container + bind-production), integrations/api (mediaRouter), factory,
contract suite, and feature integration tests.

Wired into:
- packages/core-api/src/root.ts (added `media: mediaRouter`)
- apps/web-next/src/server/bind-production.ts (calls bindProductionMedia)
- tsconfig.base.json (added @repo/media/api and ./di/bind-production aliases)

56 new tests in @repo/media (13 test files); core-api router test updated
to assert media. procedures. All 26 turbo tasks green.

Refactor log: §2, §4.1, §4.2, §5.1, §6.1
Spec: §6.5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 00:29:05 +02:00
parent 69623b995d
commit 8a36d803b3
42 changed files with 1121 additions and 23 deletions

View File

@@ -91,6 +91,65 @@ Entity model moves (git mv — history preserved):
## 2. Files added (with purpose)
### Task 8: Media — full Clean Architecture scaffold (~30 new files)
**Entities:**
- `packages/media/src/entities/models/media.ts` — Zod schema + `Media` type (id, alt, url, filename, mimeType, filesize, width?, height?)
- `packages/media/src/entities/models/media.test.ts` — 4 schema validation tests
- `packages/media/src/entities/errors/media.ts``MediaNotFoundError`
- `packages/media/src/entities/errors/media.test.ts` — 3 error class tests
- `packages/media/src/entities/errors/common.ts``InputParseError` (media copy)
**Application:**
- `packages/media/src/application/repositories/media.repository.interface.ts``IMediaRepository` with `getMedia`, `listMedia`, `deleteMedia`
- `packages/media/src/application/use-cases/get-media.use-case.ts` — factory function; throws `MediaNotFoundError` when missing
- `packages/media/src/application/use-cases/get-media.use-case.test.ts` — 2 tests
- `packages/media/src/application/use-cases/list-media.use-case.ts` — factory function; delegates to repo
- `packages/media/src/application/use-cases/list-media.use-case.test.ts` — 3 tests
- `packages/media/src/application/use-cases/delete-media.use-case.ts` — factory function; throws `MediaNotFoundError` if not found, then deletes
- `packages/media/src/application/use-cases/delete-media.use-case.test.ts` — 2 tests
**Infrastructure:**
- `packages/media/src/infrastructure/repositories/media.repository.mock.ts``MockMediaRepository` (in-memory; has `_store` test-helper method)
- `packages/media/src/infrastructure/repositories/media.repository.mock.test.ts` — runs contract suite against mock
- `packages/media/src/infrastructure/repositories/media.repository.ts``MediaRepository` (Payload-backed; uses `getPayload().find/findByID/delete`)
- `packages/media/src/infrastructure/repositories/media.repository.test.ts` — contract suite + impl-specific mapping tests (9 tests total)
**Interface-adapters:**
- `packages/media/src/interface-adapters/controllers/get-media.controller.ts` — factory + `IGetMediaController` alias; Zod input validation
- `packages/media/src/interface-adapters/controllers/get-media.controller.test.ts` — 4 tests
- `packages/media/src/interface-adapters/controllers/list-media.controller.ts` — factory + `IListMediaController` alias
- `packages/media/src/interface-adapters/controllers/list-media.controller.test.ts` — 3 tests
- `packages/media/src/interface-adapters/controllers/delete-media.controller.ts` — factory + `IDeleteMediaController` alias
- `packages/media/src/interface-adapters/controllers/delete-media.controller.test.ts` — 3 tests
**DI:**
- `packages/media/src/di/symbols.ts``MEDIA_SYMBOLS` (7 keys: repository + 3 use cases + 3 controllers)
- `packages/media/src/di/module.ts``MediaModule`: `.to(MockMediaRepository)` default + `.toDynamicValue()` bindings for all use cases and controllers
- `packages/media/src/di/container.ts``mediaContainer` singleton
- `packages/media/src/di/container.test.ts` — 7 tests (all symbols resolve; default binding is mock)
- `packages/media/src/di/bind-production.ts``bindProductionMedia(config)` swaps mock for real `MediaRepository`
**Integrations:**
- `packages/media/src/integrations/api/router.ts``mediaRouter` (getMedia, listMedia, deleteMedia); controllers resolved via `mediaContainer.get`
- `packages/media/src/integrations/api/index.ts` — re-exports `mediaRouter` + `MediaRouter` type
**Contract + factory:**
- `packages/media/src/__contracts__/media-repository.contract.ts` — 6 contract assertions (undefined for missing id, empty list, delete-and-check, limit=0, offset, empty string id)
- `packages/media/src/__factories__/media.factory.ts` — ADAPTED: now imports `Media` from `../entities/models/media` instead of defining an inline interface
- `packages/media/src/__factories__/index.ts` — ADAPTED: re-exports `mediaFactory` + `Media` type from correct paths
**Feature test:**
- `packages/media/tests/media.feature.test.ts` — 7 tests; full slice via direct injection (no container)
**Files modified in other packages:**
- `packages/media/package.json` — added `inversify`, `reflect-metadata`, `zod`, `@repo/core-shared`, `@trpc/server` to deps; added `./api` and `./di/bind-production` to exports
- `packages/core-api/package.json` — added `@repo/media: workspace:*` to dependencies
- `packages/core-api/src/root.ts` — added `media: mediaRouter` to `appRouter`; imported from `@repo/media/api`
- `packages/core-api/src/router.test.ts` — updated assertion to include `media.` prefix check
- `apps/web-next/src/server/bind-production.ts` — added `bindProductionMedia(resolvedConfig)` call
- `tsconfig.base.json` — added `@repo/media/api` and `@repo/media/di/bind-production` path aliases
### Task 4: Real implementations + tests
- `packages/auth/src/infrastructure/repositories/users.repository.ts` — real Payload-backed `UsersRepository` (implements `IUsersRepository` via `getPayload`)
@@ -150,7 +209,7 @@ Entity model moves (git mv — history preserved):
### 4.1 Use cases — factory function pattern
Applied to all 3 auth use cases (`sign-in`, `sign-up`, `sign-out`) in Task 4, all 3 blog use cases (`get-articles`, `create-article`, `get-article-by-slug` NEW) in Task 5, both marketing-pages use cases (`get-page-by-slug`, `get-site-settings`) in Task 6, and the single navigation use case (`get-header`) in Task 7:
Applied to all 3 auth use cases (`sign-in`, `sign-up`, `sign-out`) in Task 4, all 3 blog use cases (`get-articles`, `create-article`, `get-article-by-slug` NEW) in Task 5, both marketing-pages use cases (`get-page-by-slug`, `get-site-settings`) in Task 6, the single navigation use case (`get-header`) in Task 7, and all 3 media use cases (`get-media`, `list-media`, `delete-media` NEW) in Task 8:
- Use cases are now factory functions: `(deps) => async (input) => result`
- Each file exports `export type I*UseCase = ReturnType<typeof *UseCase>` for DI typing
@@ -162,7 +221,7 @@ Applied to all 3 auth use cases (`sign-in`, `sign-up`, `sign-out`) in Task 4, al
### 4.2 Controllers — one per use case
Applied to all 3 auth controllers (`sign-in`, `sign-up`, `sign-out`) in Task 4; blog controllers split in Task 5; marketing-pages controllers split in Task 6:
Applied to all 3 auth controllers (`sign-in`, `sign-up`, `sign-out`) in Task 4; blog controllers split in Task 5; marketing-pages controllers split in Task 6; 3 new media controllers (`get-media`, `list-media`, `delete-media`) created from scratch in Task 8:
- Controllers were already split for auth (one file per use case) — Task 4 refactors them to factory functions
- Blog: the multi-method `articles.controller.ts` is deleted and replaced by 3 single-responsibility files
@@ -187,7 +246,7 @@ Pattern now in place across auth, blog, marketing-pages, navigation (media skipp
### 5.1 Inversify `.toDynamicValue` bindings
Applied to `packages/auth/src/di/module.ts` (Task 4), `packages/blog/src/di/module.ts` (Task 5), and `packages/marketing-pages/src/di/module.ts` (Task 6):
Applied to `packages/auth/src/di/module.ts` (Task 4), `packages/blog/src/di/module.ts` (Task 5), `packages/marketing-pages/src/di/module.ts` (Task 6), and `packages/media/src/di/module.ts` (Task 8):
**auth:**
- `AUTH_SYMBOLS` expanded with 6 new keys: `ISignInUseCase`, `ISignUpUseCase`, `ISignOutUseCase`, `ISignInController`, `ISignUpController`, `ISignOutController`
@@ -213,6 +272,13 @@ Applied to `packages/auth/src/di/module.ts` (Task 4), `packages/blog/src/di/modu
- Repository binding remains `.to(MockHeaderRepository)` as default
- tRPC router updated to resolve controller via `navigationContainer.get<IGetHeaderController>(NAVIGATION_SYMBOLS.IGetHeaderController)`
**media (Task 8 — new from scratch):**
- `MEDIA_SYMBOLS` defined with 7 keys: `IMediaRepository`, `IGetMediaUseCase`, `IListMediaUseCase`, `IDeleteMediaUseCase`, `IGetMediaController`, `IListMediaController`, `IDeleteMediaController`
- Repository bound with `.to(MockMediaRepository)` as default
- All 3 use cases + all 3 controllers bound with `.toDynamicValue((ctx) => factoryFn(ctx.container.get(...)))`
- `mediaRouter` added to `appRouter` in `packages/core-api/src/root.ts`
- `bindProductionMedia(config)` added to `apps/web-next/src/server/bind-production.ts`
### 5.2 Mock siblings registered as default bindings
- `MockUsersRepository` and `MockAuthenticationService` remain the default bindings in `AuthModule`
@@ -223,6 +289,8 @@ Applied to `packages/auth/src/di/module.ts` (Task 4), `packages/blog/src/di/modu
### 6.1 Direct injection (no container rebinding)
Pattern also applied to all new media tests (Task 8) — use case and controller tests construct `MockMediaRepository` directly and inject into factory functions; the feature test (`tests/media.feature.test.ts`) builds the full chain via `buildChain()` helper without touching the DI container.
Applied to all auth use-case tests, controller tests, the router test, and the feature integration test:
- **Before:** `beforeEach` unbinds and rebinds symbols on `authContainer`