diff --git a/.eslintrc.json b/.eslintrc.json index aa7e1bad6..63de98b7c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -91,6 +91,10 @@ { "selector": "ExportDefaultDeclaration", "message": "Default exports are forbidden. Use named exports instead." + }, + { + "selector": "TSInterfaceDeclaration[id.name=/^I[A-Z]/]", + "message": "Interface names should not start with 'I'. Use descriptive names without the 'I' prefix (e.g., 'LiverCompositor' instead of 'ILiveryCompositor')." } ] } diff --git a/docs/architecture/DATA_FLOW.md b/docs/architecture/DATA_FLOW.md index 26bc4cf6c..5dcfab248 100644 --- a/docs/architecture/DATA_FLOW.md +++ b/docs/architecture/DATA_FLOW.md @@ -1,207 +1,442 @@ -Frontend & Backend Output Shapes – Clean Architecture (Strict, Final) +Clean Architecture – Application Services, Use Cases, Ports, and Data Flow (Strict, Final) -This document defines the exact responsibilities, naming, and placement of all data shapes involved in delivering data from Core → API → Frontend UI. +This document defines the final, non-ambiguous Clean Architecture setup for the project. -It resolves all ambiguity around Presenters, View Models, DTOs, and Output Ports. -There is no overlap of terminology across layers. +It explicitly covers: + • Use Cases vs Application Services + • Input & Output Ports (and what does not exist) + • API responsibilities + • Frontend responsibilities + • Naming, placement, and dependency rules + • End-to-end flow with concrete paths and code examples + +There are no hybrid concepts, no overloaded terms, and no optional interpretations. ⸻ -1. Core Layer (Application / Use Cases) +1. Architectural Layers (Final) -Core Output Ports (formerly “Presenters”) +Domain → Business truth +Application → Use Cases + Application Services +Adapters → API, Persistence, External Systems +Frontend → UI, View Models, UX logic -In the Core, a Presenter is not a UI concept. +Only dependency-inward is allowed. -It is an Output Port that defines how a Use Case emits its result. +⸻ + +2. Domain Layer (Core / Domain) + +What lives here + • Entities (classes) + • Value Objects (classes) + • Domain Services (stateless business logic) + • Domain Events + • Domain Errors / Invariants + +What NEVER lives here + • DTOs + • Models + • Ports + • Use Cases + • Application Services + • Framework code + +⸻ + +3. Application Layer (Core / Application) + +The Application Layer has two distinct responsibilities: + 1. Use Cases – business decisions + 2. Application Services – orchestration of multiple use cases + +⸻ + +4. Use Cases (Application / Use Cases) + +Definition + +A Use Case represents one business intent. + +Examples: + • CreateLeague + • ApproveSponsorship + • CompleteDriverOnboarding Rules - • Core Output Ports: - • define what data is emitted - • do not store state - • do not expose getters - • do not reference DTOs or View Models - • Core never pulls data back from an output port - • Core calls present() and stops + • A Use Case: + • contains business logic + • enforces invariants + • operates on domain entities + • communicates ONLY via ports + • A Use Case: + • does NOT orchestrate multiple workflows + • does NOT know HTTP, UI, DB, queues -Naming - • *OutputPort - • *Result (pure application result) +Structure + +core/racing/application/use-cases/ + └─ CreateLeagueUseCase.ts Example -export interface CompleteDriverOnboardingResult { - readonly success: boolean; - readonly driverId?: string; - readonly error?: string; +export class CreateLeagueUseCase { + constructor( + private readonly leagueRepository: LeagueRepositoryPort, + private readonly output: CreateLeagueOutputPort + ) {} + + execute(input: CreateLeagueInputPort): void { + // business rules & invariants + + const league = League.create(input.name, input.maxMembers); + this.leagueRepository.save(league); + + this.output.presentSuccess(league.id); + } } -export interface CompleteDriverOnboardingOutputPort { - present(result: CompleteDriverOnboardingResult): void; + +⸻ + +5. Ports (Application / Ports) + +The Only Two Kinds of Ports + +Everything crossing the Application boundary is a Port. + +Input Ports + +Input Ports describe what a use case needs. + +export interface CreateLeagueInputPort { + readonly name: string; + readonly maxMembers: number; } -The Core does not know or care what happens after present() is called. +Rules: + • Interfaces only + • No behavior + • No validation logic ⸻ -2. API Layer (Delivery / Adapter) +Output Ports -API Presenters (Response Mappers) +Output Ports describe how a use case emits outcomes. -API Presenters are Adapters. +export interface CreateLeagueOutputPort { + presentSuccess(leagueId: string): void; + presentFailure(reason: string): void; +} -They: - • implement Core Output Ports - • translate Core Results into API Response DTOs - • store response state temporarily for the controller +Rules: + • No return values + • No getters + • No state + • Use methods, not result objects -They are not View Models. +⸻ + +6. Application Services (Application / Services) + +Definition + +An Application Service orchestrates multiple Use Cases. + +It exists because: + • No single Use Case should know the whole workflow + • Orchestration is not business logic Rules - • API Presenters: - • implement a Core Output Port - • map Core Results → API Responses - • may store state internally - • API Presenters must not: - • contain business logic - • reference frontend View Models + • Application Services: + • call multiple Use Cases + • define execution order + • handle partial failure & compensation + • Application Services: + • do NOT contain business rules + • do NOT modify entities directly -Naming - • *Presenter or *ResponseMapper - • Output types end with Response or ApiResponse +Structure + +core/racing/application/services/ + └─ LeagueSetupService.ts + +Example (with Edge Cases) + +export class LeagueSetupService { + constructor( + private readonly createLeague: CreateLeagueUseCase, + private readonly createSeason: CreateSeasonUseCase, + private readonly assignOwner: AssignLeagueOwnerUseCase, + private readonly notify: SendLeagueWelcomeNotificationUseCase + ) {} + + execute(input: LeagueSetupInputPort): void { + const leagueId = this.createLeague.execute(input); + + try { + this.createSeason.execute({ leagueId }); + this.assignOwner.execute({ leagueId, ownerId: input.ownerId }); + this.notify.execute({ leagueId, ownerId: input.ownerId }); + } catch (error) { + // compensation / rollback logic + throw error; + } + } +} + +Edge cases that belong ONLY here: + • Partial failure handling + • Workflow order + • Optional steps + • Retry / idempotency logic ⸻ -3. Frontend Layer (apps/website) +7. API Layer (apps/api) -View Models (UI-Owned, Final Form) +Responsibilities + • Transport (HTTP) + • Validation (request shape) + • Mapping to Input Ports + • Calling Application Services + • Adapting Output Ports -A View Model represents fully prepared UI state. +Structure -Only the frontend has Views — therefore only the frontend has View Models. +apps/api/leagues/ +├─ LeagueController.ts +├─ presenters/ +│ └─ CreateLeaguePresenter.ts +└─ dto/ + ├─ CreateLeagueRequestDto.ts + └─ CreateLeagueResponseDto.ts -Rules - • View Models: - • live only in apps/website - • accept API Response DTOs as input - • expose UI-ready data and helpers - • View Models must not: - • contain domain logic - • validate business rules - • perform side effects - • be sent back to the server - -Naming - • *ViewModel ⸻ -4. Website Presenters (DTO → ViewModel) +API Presenter (Adapter) + +export class CreateLeaguePresenter implements CreateLeagueOutputPort { + private response!: CreateLeagueResponseDto; + + presentSuccess(leagueId: string): void { + this.response = { success: true, leagueId }; + } + + presentFailure(reason: string): void { + this.response = { success: false, errorMessage: reason }; + } + + getResponse(): CreateLeagueResponseDto { + return this.response; + } +} + + +⸻ + +8. Frontend Layer (apps/website) + +The frontend layer contains UI-specific data shapes. None of these cross into the Core. + +There are three distinct frontend data concepts: + 1. API DTOs (transport) + 2. Command Models (user input / form state) + 3. View Models (UI display state) + +⸻ + +8.1 API DTOs (Transport Contracts) + +API DTOs represent exact HTTP contracts exposed by the backend. +They are usually generated from OpenAPI or manually mirrored. + +apps/website/lib/dtos/ + └─ CreateLeagueResponseDto.ts + +Rules: + • Exact mirror of backend response + • No UI logic + • No derived values + • Never used directly by components + +⸻ + +8.2 Command Models (User Input / Form State) + +Command Models represent user intent before submission. +They are frontend-only and exist to manage: + • form state + • validation feedback + • step-based wizards + +They are NOT: + • domain objects + • API DTOs + • View Models + +apps/website/lib/commands/ + └─ CreateLeagueCommandModel.ts + +Rules: + • Classes (stateful) + • May contain client-side validation + • May contain UX-specific helpers (step validation, dirty flags) + • Must expose a method to convert to an API Request DTO + +Example responsibility: + • hold incomplete or invalid user input + • guide the user through multi-step flows + • prepare data for submission + +Command Models: + • are consumed by components + • are passed into services + • are never sent directly over HTTP + +⸻ + +8.3 View Models (UI Display State) + +View Models represent fully prepared UI state after data is loaded. + +apps/website/lib/view-models/ + └─ CreateLeagueViewModel.ts + +Rules: + • Classes only + • UI logic allowed (formatting, labels, derived flags) + • No domain logic + • No mutation after construction + +⸻ + +8.4 Website Presenters (DTO → ViewModel) Website Presenters are pure mappers. -They: - • convert API Response DTOs into View Models - • perform formatting and reshaping - • are deterministic and side-effect free +export class CreateLeaguePresenter { + present(dto: CreateLeagueResponseDto): CreateLeagueViewModel { + return new CreateLeagueViewModel(dto); + } +} -They are not Core Presenters. - -Rules +Rules: • Input: API DTOs • Output: View Models - • Must not: - • call APIs - • read storage - • perform decisions + • No side effects + • No API calls ⸻ -5. API Client (Frontend) - -The API Client is a thin HTTP layer. - -Rules - • Sends HTTP requests - • Returns API DTOs only - • Must not: - • return View Models - • contain business logic - • format data for UI - -⸻ - -6. Website Services (Orchestration) +8.5 Website Services (Orchestration) Website Services orchestrate: + • Command Models • API Client calls - • Website Presenter mappings + • Presenter mappings -They are the only layer allowed to touch both. +export class LeagueService { + async createLeague(command: CreateLeagueCommandModel): Promise { + const dto = await this.api.createLeague(command.toRequestDto()); + return this.presenter.present(dto); + } +} -Rules - • Services: - • call API Client - • call Website Presenters - • return View Models only - • Components never touch API Client or DTOs +Rules: + • Services accept Command Models + • Services return View Models + • Components never call API clients directly ⸻ -7. Final Data Flow (Unambiguous) +View Models (UI State) -Core Use Case - → OutputPort.present(Result) +apps/website/lib/view-models/ + └─ CreateLeagueViewModel.ts -API Presenter (Adapter) - → maps Result → ApiResponse +export class CreateLeagueViewModel { + constructor(private readonly dto: CreateLeagueResponseDto) {} -API Controller - → returns ApiResponse (JSON) + get message(): string { + return this.dto.success + ? 'League created successfully' + : this.dto.errorMessage ?? 'Creation failed'; + } +} -Frontend API Client - → returns ApiResponse DTO +Rules: + • Classes only + • UI logic allowed + • No domain logic -Website Presenter - → maps DTO → ViewModel +⸻ + +Website Presenter (DTO → ViewModel) + +export class CreateLeaguePresenter { + present(dto: CreateLeagueResponseDto): CreateLeagueViewModel { + return new CreateLeagueViewModel(dto); + } +} + + +⸻ + +Website Service (Orchestration) + +export class LeagueService { + constructor( + private readonly api: LeaguesApiClient, + private readonly presenter: CreateLeaguePresenter + ) {} + + async createLeague(input: unknown): Promise { + const dto = await this.api.createLeague(input); + return this.presenter.present(dto); + } +} + + +⸻ + +9. Full End-to-End Flow (Final) UI Component - → consumes ViewModel + → Website Service + → API Client + → HTTP Request DTO + → API Controller + → Application Service + → Use Case(s) + → Domain + → Output Port + → API Presenter + → HTTP Response DTO + → Website Presenter + → View Model + → UI ⸻ -8. Terminology Rules (Strict) - -Term Layer Meaning -OutputPort Core Use case output contract -Result Core Pure application result -Presenter (API) apps/api Maps Result → API Response -Response / ApiResponse apps/api HTTP transport shape -Presenter (Website) apps/website Maps DTO → ViewModel -ViewModel apps/website UI-ready state - -No term is reused with a different meaning. +10. Final Non-Negotiable Rules + • Core knows ONLY Ports + Domain + • Core has NO Models, DTOs, or ViewModels + • API talks ONLY to Application Services + • Controllers NEVER call Use Cases directly + • Frontend Components see ONLY View Models + • DTOs never cross into UI components ⸻ -9. Non-Negotiable Rules - • Core has no DTOs - • Core has no View Models - • API has no View Models - • Frontend has no Core Results - • View Models exist only in the frontend - • Presenters mean different things per layer, but: - • Core = Output Port - • API = Adapter - • Website = Mapper +11. Final Merksatz -⸻ +Use Cases decide. +Application Services orchestrate. +Adapters translate. +UI presents. -10. Final Merksatz - -The Core emits results. -The API transports them. -The Frontend interprets them. - -If a type tries to do more than one of these — it is incorrectly placed. \ No newline at end of file +If a class violates more than one of these roles, it is incorrectly placed. \ No newline at end of file diff --git a/docs/architecture/FILE_STRUCTURE.md b/docs/architecture/FILE_STRUCTURE.md new file mode 100644 index 000000000..ca4d06b13 --- /dev/null +++ b/docs/architecture/FILE_STRUCTURE.md @@ -0,0 +1,115 @@ +# File Structure + +## Core + +``` +core/ # * Business- & Anwendungslogik (framework-frei) +├── shared/ # * Gemeinsame Core-Bausteine +│ ├── domain/ # * Domain-Basistypen +│ │ ├── Entity.ts # * Basisklasse für Entities +│ │ ├── ValueObject.ts # * Basisklasse für Value Objects +│ │ └── DomainError.ts # * Domain-spezifische Fehler +│ └── application/ +│ └── ApplicationError.ts # * Use-Case-/Application-Fehler +│ +├── racing/ # * Beispiel-Domain (Bounded Context) +│ ├── domain/ # * Fachliche Wahrheit +│ │ ├── entities/ # * Aggregate Roots & Entities +│ │ │ ├── League.ts # * Aggregate Root +│ │ │ └── Race.ts # * Entity +│ │ ├── value-objects/ # * Unveränderliche Fachwerte +│ │ │ └── LeagueName.ts # * Beispiel VO +│ │ ├── services/ # * Domain Services (Regeln, kein Ablauf) +│ │ │ └── ChampionshipScoringService.ts # * Regel über mehrere Entities +│ │ └── errors/ # * Domain-Invariantenfehler +│ │ └── RacingDomainError.ts +│ │ +│ └── application/ # * Anwendungslogik +│ ├── ports/ # * EINZIGE Schnittstellen des Cores +│ │ ├── input/ # * Input Ports (Use-Case-Grenzen) +│ │ │ └── CreateLeagueInputPort.ts +│ │ └── output/ # * Output Ports (Use-Case-Ergebnisse) +│ │ └── CreateLeagueOutputPort.ts +│ │ +│ ├── use-cases/ # * Einzelne Business-Intents +│ │ └── CreateLeagueUseCase.ts +│ │ +│ └── services/ # * Application Services (Orchestrierung) +│ └── LeagueSetupService.ts # * Koordiniert mehrere Use Cases +``` + +## Adapters + +``` +adapters/ # * Alle äußeren Implementierungen +├── persistence/ # * Datenhaltung +│ ├── typeorm/ # Konkrete DB-Technologie +│ │ ├── entities/ # ORM-Entities (nicht Domain!) +│ │ └── repositories/ # * Implementieren Core-Ports +│ │ └── LeagueRepository.ts +│ └── inmemory/ # Test-/Dev-Implementierungen +│ └── LeagueRepository.ts +│ +├── notifications/ # Externe Systeme +│ └── EmailNotificationAdapter.ts # Implementiert Notification-Port +│ +├── logging/ # Logging / Telemetrie +│ └── ConsoleLoggerAdapter.ts # Adapter für Logger-Port +│ +└── bootstrap/ # Initialisierung / Seeding + └── EnsureInitialData.ts # App-Start-Logik +``` + +## API + +``` +apps/api/ # * Delivery Layer (HTTP) +├── app.module.ts # * Framework-Zusammenbau +│ +├── leagues/ # * Feature-Modul +│ ├── LeagueController.ts # * HTTP → Application Service +│ │ +│ ├── dto/ # * Transport-DTOs (HTTP) +│ │ ├── CreateLeagueRequestDto.ts # * Request-Shape +│ │ └── CreateLeagueResponseDto.ts # * Response-Shape +│ │ +│ └── presenters/ # * Output-Port-Adapter +│ └── CreateLeaguePresenter.ts # * Core Output → HTTP Response +│ +└── shared/ # API-spezifisch + └── filters/ # Exception-Handling +``` + +## Frontend +``` +apps/website/ # * Frontend (UI) +├── app/ # * Next.js Routen +│ └── leagues/ # * Page-Level +│ └── page.tsx +│ +├── components/ # * Reine UI-Komponenten +│ └── LeagueForm.tsx +│ +├── lib/ +│ ├── api/ # * HTTP-Client +│ │ └── LeaguesApiClient.ts # * Gibt NUR API DTOs zurück +│ │ +│ ├── dtos/ # * API-Vertrags-Typen +│ │ └── CreateLeagueResponseDto.ts +│ │ +│ ├── commands/ # * Command Models (Form State) +│ │ └── CreateLeagueCommandModel.ts +│ │ +│ ├── presenters/ # * DTO → ViewModel Mapper +│ │ └── CreateLeaguePresenter.ts +│ │ +│ ├── view-models/ # * UI-State +│ │ └── CreateLeagueViewModel.ts +│ │ +│ ├── services/ # * Frontend-Orchestrierung +│ │ └── LeagueService.ts +│ │ +│ └── blockers/ # UX-Schutz (Throttle, Submit) +│ ├── SubmitBlocker.ts +│ └── ThrottleBlocker.ts +``` \ No newline at end of file