module cleanup
This commit is contained in:
@@ -1,38 +1,20 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
|
||||
import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO';
|
||||
import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO';
|
||||
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
|
||||
import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO';
|
||||
import { JoinLeagueOutputDTO } from './dtos/JoinLeagueOutputDTO';
|
||||
import { TransferLeagueOwnershipOutputDTO } from './dtos/TransferLeagueOwnershipOutputDTO';
|
||||
import { LeagueJoinRequestWithDriverDTO } from './dtos/LeagueJoinRequestWithDriverDTO';
|
||||
import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
|
||||
import { GetLeagueAdminPermissionsInputDTO } from './dtos/GetLeagueAdminPermissionsInputDTO';
|
||||
import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO';
|
||||
import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO';
|
||||
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO';
|
||||
import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO';
|
||||
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
|
||||
import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO';
|
||||
import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO';
|
||||
import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO';
|
||||
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
|
||||
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
|
||||
import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO';
|
||||
import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
|
||||
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
|
||||
import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO';
|
||||
import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO';
|
||||
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
|
||||
import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO';
|
||||
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
|
||||
import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO';
|
||||
import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO';
|
||||
import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO';
|
||||
import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO';
|
||||
import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO';
|
||||
import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO';
|
||||
|
||||
// Core imports for entities
|
||||
import type { League } from '@core/racing/domain/entities/League';
|
||||
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
|
||||
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO';
|
||||
|
||||
// Core imports for view models
|
||||
import type { LeagueScoringConfigViewModel } from '@core/racing/application/presenters/ILeagueScoringConfigPresenter';
|
||||
@@ -46,18 +28,13 @@ import type { GetLeagueAdminPermissionsViewModel } from '@core/racing/applicatio
|
||||
import type { RemoveLeagueMemberViewModel } from '@core/racing/application/presenters/IRemoveLeagueMemberPresenter';
|
||||
import type { UpdateLeagueMemberRoleViewModel } from '@core/racing/application/presenters/IUpdateLeagueMemberRolePresenter';
|
||||
import type { GetLeagueOwnerSummaryViewModel } from '@core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter';
|
||||
import type { GetLeagueProtestsViewModel } from '@core/racing/application/presenters/IGetLeagueProtestsPresenter';
|
||||
import type { GetLeagueSeasonsViewModel } from '@core/racing/application/presenters/IGetLeagueSeasonsPresenter';
|
||||
import type { GetLeagueMembershipsViewModel } from '@core/racing/application/presenters/IGetLeagueMembershipsPresenter';
|
||||
import type { LeagueStandingsViewModel } from '@core/racing/application/presenters/ILeagueStandingsPresenter';
|
||||
import type { LeagueScheduleViewModel } from '@core/racing/application/presenters/ILeagueSchedulePresenter';
|
||||
import type { LeagueStatsViewModel } from '@core/racing/application/presenters/ILeagueStatsPresenter';
|
||||
import type { LeagueConfigFormViewModel } from '@core/racing/application/presenters/ILeagueFullConfigPresenter';
|
||||
import type { CreateLeagueViewModel } from '@core/racing/application/presenters/ICreateLeaguePresenter';
|
||||
import type { JoinLeagueViewModel } from '@core/racing/application/presenters/IJoinLeaguePresenter';
|
||||
import type { TransferLeagueOwnershipViewModel } from '@core/racing/application/presenters/ITransferLeagueOwnershipPresenter';
|
||||
|
||||
|
||||
// Core imports
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
@@ -92,13 +69,9 @@ import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPr
|
||||
import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter';
|
||||
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
|
||||
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
||||
import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter';
|
||||
import { GetLeagueOwnerSummaryPresenter } from './presenters/GetLeagueOwnerSummaryPresenter';
|
||||
import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter';
|
||||
import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter';
|
||||
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
|
||||
import { LeagueSchedulePresenter } from './presenters/LeagueSchedulePresenter';
|
||||
import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
|
||||
import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter';
|
||||
import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter';
|
||||
import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPresenter';
|
||||
@@ -106,6 +79,9 @@ import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMember
|
||||
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
|
||||
import { JoinLeaguePresenter } from './presenters/JoinLeaguePresenter';
|
||||
import { TransferLeagueOwnershipPresenter } from './presenters/TransferLeagueOwnershipPresenter';
|
||||
import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter';
|
||||
import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter';
|
||||
import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter';
|
||||
|
||||
// Tokens
|
||||
import { LOGGER_TOKEN } from './LeagueProviders';
|
||||
@@ -258,7 +234,9 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
return result.unwrap();
|
||||
const presenter = new GetLeagueProtestsPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel() as LeagueAdminProtestsDTO;
|
||||
}
|
||||
|
||||
async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> {
|
||||
@@ -267,7 +245,9 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
return result.unwrap().seasons;
|
||||
const presenter = new GetLeagueSeasonsPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel().seasons;
|
||||
}
|
||||
|
||||
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> {
|
||||
@@ -276,16 +256,19 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
return result.unwrap();
|
||||
const presenter = new GetLeagueMembershipsPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel().memberships as LeagueMembershipsDTO;
|
||||
}
|
||||
|
||||
async getLeagueStandings(leagueId: string): Promise<LeagueStandingsViewModel> {
|
||||
this.logger.debug('Getting league standings', { leagueId });
|
||||
|
||||
return await this.getLeagueStandingsUseCase.execute(leagueId);
|
||||
const result = await this.getLeagueStandingsUseCase.execute(leagueId);
|
||||
// The use case returns a view model directly, so we return it as-is
|
||||
return result as unknown as LeagueStandingsViewModel;
|
||||
}
|
||||
|
||||
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleViewModel> {
|
||||
async getLeagueSchedule(leagueId: string): Promise<ReturnType<LeagueSchedulePresenter['getViewModel']>> {
|
||||
this.logger.debug('Getting league schedule', { leagueId });
|
||||
const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
@@ -320,10 +303,48 @@ export class LeagueService {
|
||||
// In a full implementation, we'd have a use case that gets league basic info
|
||||
const ownerSummary = config ? await this.getLeagueOwnerSummary({ ownerId: 'placeholder', leagueId }) : null;
|
||||
|
||||
// Convert config from view model to DTO format manually with proper types
|
||||
const configForm = config ? {
|
||||
leagueId: config.leagueId,
|
||||
basics: {
|
||||
name: config.basics.name,
|
||||
description: config.basics.description,
|
||||
visibility: config.basics.visibility as 'public' | 'private',
|
||||
},
|
||||
structure: {
|
||||
mode: config.structure.mode as 'solo' | 'team',
|
||||
},
|
||||
championships: [], // TODO: Map championships from view model
|
||||
scoring: {
|
||||
type: 'standard' as const, // TODO: Map from view model
|
||||
points: 25, // TODO: Map from view model
|
||||
},
|
||||
dropPolicy: {
|
||||
strategy: config.dropPolicy.strategy as 'none' | 'worst_n',
|
||||
n: config.dropPolicy.n ?? 0,
|
||||
},
|
||||
timings: {
|
||||
raceDayOfWeek: 'sunday' as const, // TODO: Map from view model
|
||||
raceTimeHour: 20, // TODO: Map from view model
|
||||
raceTimeMinute: 0, // TODO: Map from view model
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: config.stewarding.decisionMode === 'steward_vote' ? 'committee_vote' as const : 'single_steward' as const,
|
||||
requireDefense: config.stewarding.requireDefense,
|
||||
defenseTimeLimit: config.stewarding.defenseTimeLimit,
|
||||
voteTimeLimit: config.stewarding.voteTimeLimit,
|
||||
protestDeadlineHours: config.stewarding.protestDeadlineHours,
|
||||
stewardingClosesHours: config.stewarding.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired,
|
||||
requiredVotes: config.stewarding.requiredVotes ?? 0,
|
||||
},
|
||||
} : null;
|
||||
|
||||
return {
|
||||
joinRequests,
|
||||
ownerSummary,
|
||||
config: { form: config },
|
||||
joinRequests: joinRequests.joinRequests,
|
||||
ownerSummary: ownerSummary?.summary || null,
|
||||
config: { form: configForm },
|
||||
protests,
|
||||
seasons,
|
||||
};
|
||||
@@ -411,7 +432,7 @@ export class LeagueService {
|
||||
};
|
||||
}
|
||||
const presenter = new TransferLeagueOwnershipPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
presenter.present({ success: true });
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,361 +1,207 @@
|
||||
Frontend data shapes: View Models, Presenters, and API Client (Strict)
|
||||
Frontend & Backend Output Shapes – Clean Architecture (Strict, Final)
|
||||
|
||||
This document defines the exact placement and responsibilities for frontend data shapes.
|
||||
It is designed to leave no room for interpretation.
|
||||
This document defines the exact responsibilities, naming, and placement of all data shapes involved in delivering data from Core → API → Frontend UI.
|
||||
|
||||
It resolves all ambiguity around Presenters, View Models, DTOs, and Output Ports.
|
||||
There is no overlap of terminology across layers.
|
||||
|
||||
⸻
|
||||
|
||||
1. Definitions
|
||||
1. Core Layer (Application / Use Cases)
|
||||
|
||||
API DTOs
|
||||
Core Output Ports (formerly “Presenters”)
|
||||
|
||||
Transport shapes owned by the API boundary (HTTP). They are not used directly by UI components.
|
||||
In the Core, a Presenter is not a UI concept.
|
||||
|
||||
View Models
|
||||
It is an Output Port that defines how a Use Case emits its result.
|
||||
|
||||
UI-owned shapes. They represent exactly what the UI needs and nothing else.
|
||||
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
|
||||
|
||||
Website Presenters
|
||||
Naming
|
||||
• *OutputPort
|
||||
• *Result (pure application result)
|
||||
|
||||
Pure mappers that convert API DTOs (or core use-case outputs) into View Models.
|
||||
Example
|
||||
|
||||
API Client
|
||||
export interface CompleteDriverOnboardingResult {
|
||||
readonly success: boolean;
|
||||
readonly driverId?: string;
|
||||
readonly error?: string;
|
||||
}
|
||||
|
||||
A thin HTTP wrapper that returns API DTOs only and performs no business logic.
|
||||
export interface CompleteDriverOnboardingOutputPort {
|
||||
present(result: CompleteDriverOnboardingResult): void;
|
||||
}
|
||||
|
||||
The Core does not know or care what happens after present() is called.
|
||||
|
||||
⸻
|
||||
|
||||
2. Directory layout (exact)
|
||||
2. API Layer (Delivery / Adapter)
|
||||
|
||||
apps/website
|
||||
├── app/ # Next.js routes/pages
|
||||
├── components/ # React components (UI only)
|
||||
├── lib/
|
||||
│ ├── api/ # API client (HTTP only)
|
||||
│ ├── dtos/ # API DTO types (transport shapes)
|
||||
│ ├── view-models/ # View Models (UI-owned shapes)
|
||||
│ ├── presenters/ # Presenters: DTO -> ViewModel mapping
|
||||
│ ├── services/ # UI orchestration (calls api + presenters)
|
||||
│ └── index.ts
|
||||
API Presenters (Response Mappers)
|
||||
|
||||
No additional folders for these concerns are allowed.
|
||||
API Presenters are Adapters.
|
||||
|
||||
They:
|
||||
• implement Core Output Ports
|
||||
• translate Core Results into API Response DTOs
|
||||
• store response state temporarily for the controller
|
||||
|
||||
They are not View Models.
|
||||
|
||||
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
|
||||
|
||||
Naming
|
||||
• *Presenter or *ResponseMapper
|
||||
• Output types end with Response or ApiResponse
|
||||
|
||||
⸻
|
||||
|
||||
3. View Models (placement and rules)
|
||||
3. Frontend Layer (apps/website)
|
||||
|
||||
Where they live
|
||||
View Models (UI-Owned, Final Form)
|
||||
|
||||
View Models MUST live in:
|
||||
A View Model represents fully prepared UI state.
|
||||
|
||||
apps/website/lib/view-models
|
||||
Only the frontend has Views — therefore only the frontend has View Models.
|
||||
|
||||
What they may contain
|
||||
• UI-ready primitives (strings, numbers, booleans)
|
||||
• UI-specific derived fields (e.g., isOwner, badgeLabel, formattedDate)
|
||||
• UI-specific structures (e.g., grouped arrays, flattened objects)
|
||||
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
|
||||
|
||||
What they must NOT contain
|
||||
• Domain entities or value objects
|
||||
• API transport metadata
|
||||
• Validation logic
|
||||
• Network or persistence concerns
|
||||
|
||||
Rule
|
||||
|
||||
Components consume only View Models.
|
||||
Naming
|
||||
• *ViewModel
|
||||
|
||||
⸻
|
||||
|
||||
4. API DTOs in the website (placement and rules)
|
||||
4. Website Presenters (DTO → ViewModel)
|
||||
|
||||
Clarification
|
||||
Website Presenters are pure mappers.
|
||||
|
||||
The website does have DTOs, but only API DTOs.
|
||||
They:
|
||||
• convert API Response DTOs into View Models
|
||||
• perform formatting and reshaping
|
||||
• are deterministic and side-effect free
|
||||
|
||||
These DTOs exist exclusively to type HTTP communication with the backend API.
|
||||
They are not UI models.
|
||||
They are not Core Presenters.
|
||||
|
||||
Rules
|
||||
• Input: API DTOs
|
||||
• Output: View Models
|
||||
• Must not:
|
||||
• call APIs
|
||||
• read storage
|
||||
• perform decisions
|
||||
|
||||
⸻
|
||||
|
||||
Where they live
|
||||
5. API Client (Frontend)
|
||||
|
||||
Website-side API DTO types MUST live in:
|
||||
The API Client is a thin HTTP layer.
|
||||
|
||||
apps/website/lib/dtos
|
||||
|
||||
What they represent
|
||||
• Exact transport shapes sent/received via HTTP
|
||||
• Backend API contracts
|
||||
• No UI assumptions
|
||||
|
||||
Who may use them
|
||||
• API client
|
||||
• Website presenters
|
||||
|
||||
Who must NOT use them
|
||||
• React components
|
||||
• Pages
|
||||
• UI logic
|
||||
|
||||
Rule
|
||||
|
||||
API DTOs stop at the presenter boundary.
|
||||
|
||||
Components must never consume API DTOs directly.
|
||||
|
||||
⸻
|
||||
|
||||
5. Presenters (website) (placement and rules)
|
||||
|
||||
Where they live
|
||||
|
||||
Website presenters MUST live in:
|
||||
|
||||
apps/website/lib/presenters
|
||||
|
||||
What they do
|
||||
• Convert API DTOs into View Models
|
||||
• Perform UI-friendly formatting and structuring
|
||||
• Are pure and deterministic
|
||||
|
||||
What they must NOT do
|
||||
• Make API calls
|
||||
• Read from localStorage/cookies directly
|
||||
• Contain business rules or decisions
|
||||
• Perform side effects
|
||||
|
||||
Rule
|
||||
|
||||
Presenters output View Models. Presenters never output API DTOs.
|
||||
|
||||
⸻
|
||||
|
||||
6. Do website presenters use View Models?
|
||||
|
||||
Yes. Strictly:
|
||||
|
||||
Website presenters MUST output View Models and MUST NOT output API DTOs.
|
||||
|
||||
Flow is always:
|
||||
|
||||
API DTO -> Presenter -> View Model -> Component
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
7. API client (website) (placement and rules)
|
||||
|
||||
Where it lives
|
||||
|
||||
The API client MUST live in:
|
||||
|
||||
apps/website/lib/api
|
||||
|
||||
What it does
|
||||
Rules
|
||||
• Sends HTTP requests
|
||||
• Returns API DTOs
|
||||
• Performs authentication header/cookie handling only at transport level
|
||||
• Does not map to View Models
|
||||
|
||||
What it must NOT do
|
||||
• Format or reshape responses for UI
|
||||
• Contain business rules
|
||||
• Contain decision logic
|
||||
|
||||
Rule
|
||||
|
||||
The API client has no knowledge of View Models.
|
||||
• Returns API DTOs only
|
||||
• Must not:
|
||||
• return View Models
|
||||
• contain business logic
|
||||
• format data for UI
|
||||
|
||||
⸻
|
||||
|
||||
8. Website service layer (strict orchestration)
|
||||
6. Website Services (Orchestration)
|
||||
|
||||
Where it lives
|
||||
Website Services orchestrate:
|
||||
• API Client calls
|
||||
• Website Presenter mappings
|
||||
|
||||
Website orchestration MUST live in:
|
||||
They are the only layer allowed to touch both.
|
||||
|
||||
apps/website/lib/services
|
||||
|
||||
What it does
|
||||
• Calls the API client
|
||||
• Calls presenters to map DTO -> View Model
|
||||
• Returns View Models to pages/components
|
||||
|
||||
What it must NOT do
|
||||
• Contain domain logic
|
||||
• Modify core invariants
|
||||
• Return API DTOs
|
||||
|
||||
Rule
|
||||
|
||||
Services are the only layer allowed to call both api/ and presenters/.
|
||||
|
||||
Components must not call the API client directly.
|
||||
Rules
|
||||
• Services:
|
||||
• call API Client
|
||||
• call Website Presenters
|
||||
• return View Models only
|
||||
• Components never touch API Client or DTOs
|
||||
|
||||
⸻
|
||||
|
||||
9. Allowed dependency directions (frontend)
|
||||
7. Final Data Flow (Unambiguous)
|
||||
|
||||
Within apps/website:
|
||||
Core Use Case
|
||||
→ OutputPort.present(Result)
|
||||
|
||||
components -> services -> (api + presenters) -> (dtos + view-models)
|
||||
API Presenter (Adapter)
|
||||
→ maps Result → ApiResponse
|
||||
|
||||
Strict rules:
|
||||
• components may import only view-models and services
|
||||
• presenters may import dtos and view-models only
|
||||
• api may import dtos only
|
||||
• services may import api, presenters, view-models
|
||||
API Controller
|
||||
→ returns ApiResponse (JSON)
|
||||
|
||||
Frontend API Client
|
||||
→ returns ApiResponse DTO
|
||||
|
||||
Website Presenter
|
||||
→ maps DTO → ViewModel
|
||||
|
||||
UI Component
|
||||
→ consumes ViewModel
|
||||
|
||||
Forbidden:
|
||||
• components importing api
|
||||
• components importing dtos
|
||||
• presenters importing api
|
||||
• api importing view-models
|
||||
• any website code importing core domain entities
|
||||
|
||||
⸻
|
||||
|
||||
10. Naming rules (strict)
|
||||
• View Models end with ViewModel
|
||||
• API DTOs end with Dto
|
||||
• Presenters end with Presenter
|
||||
• Services end with Service
|
||||
• One export per file
|
||||
• File name equals exported symbol (PascalCase)
|
||||
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.
|
||||
|
||||
⸻
|
||||
|
||||
11. Final "no ambiguity" summary
|
||||
• View Models live in apps/website/lib/view-models
|
||||
• API DTOs live in apps/website/lib/dtos
|
||||
• Presenters live in apps/website/lib/presenters and map DTO -> ViewModel
|
||||
• API client lives in apps/website/lib/api and returns DTOs only
|
||||
• Services live in apps/website/lib/services and return View Models only
|
||||
• Components consume View Models only and never touch API DTOs or API clients
|
||||
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
|
||||
|
||||
⸻
|
||||
|
||||
12. Clean Architecture Flow Diagram
|
||||
10. Final Merksatz
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[UI Components] --> B[Services]
|
||||
B --> C[API Client]
|
||||
B --> D[Presenters]
|
||||
C --> E[API DTOs]
|
||||
D --> E
|
||||
D --> F[View Models]
|
||||
A --> F
|
||||
The Core emits results.
|
||||
The API transports them.
|
||||
The Frontend interprets them.
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style B fill:#f3e5f5
|
||||
style C fill:#fff3e0
|
||||
style D fill:#e8f5e8
|
||||
style E fill:#ffebee
|
||||
style F fill:#e3f2fd
|
||||
```
|
||||
|
||||
**Flow Explanation:**
|
||||
- UI Components consume only View Models
|
||||
- Services orchestrate API calls and presenter mappings
|
||||
- API Client returns raw API DTOs
|
||||
- Presenters transform API DTOs into UI-ready View Models
|
||||
- Strict dependency direction: UI → Services → (API + Presenters) → (DTOs + ViewModels)
|
||||
|
||||
⸻
|
||||
|
||||
13. Enforcement Guidelines
|
||||
|
||||
**ESLint Rules:**
|
||||
- Direct imports from `apiClient` are forbidden - use services instead
|
||||
- Direct imports from `dtos` in UI components are forbidden - use ViewModels instead
|
||||
- Direct imports from `api/*` in UI components are forbidden - use services instead
|
||||
|
||||
**TypeScript Path Mappings:**
|
||||
- Use `@/lib/dtos` for API DTO imports
|
||||
- Use `@/lib/view-models` for View Model imports
|
||||
- Use `@/lib/presenters` for Presenter imports
|
||||
- Use `@/lib/services` for Service imports
|
||||
- Use `@/lib/api` for API client imports
|
||||
|
||||
**Import Restrictions:**
|
||||
- Components may import only view-models and services
|
||||
- Presenters may import dtos and view-models only
|
||||
- API may import dtos only
|
||||
- Services may import api, presenters, view-models
|
||||
- Forbidden: components importing api, components importing dtos, presenters importing api, api importing view-models
|
||||
|
||||
**Verification Commands:**
|
||||
```bash
|
||||
npm run build # Ensure TypeScript compiles
|
||||
npm run lint # Ensure ESLint rules pass
|
||||
npm run test # Ensure all tests pass
|
||||
```
|
||||
|
||||
⸻
|
||||
|
||||
14. Architecture Examples
|
||||
|
||||
**Before (Violates Rules):**
|
||||
```typescript
|
||||
// In a page component - BAD
|
||||
import { apiClient } from '@/lib/apiClient';
|
||||
import type { RaceResultDto } from '@/lib/dtos/RaceResultDto';
|
||||
|
||||
const RacePage = () => {
|
||||
const [data, setData] = useState<RaceResultDto[]>();
|
||||
// Direct API call and DTO usage in UI
|
||||
useEffect(() => {
|
||||
apiClient.getRaceResults().then(setData);
|
||||
}, []);
|
||||
return <div>{data?.map(d => d.position)}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
**After (Clean Architecture):**
|
||||
```typescript
|
||||
// In a page component - GOOD
|
||||
import { RaceResultsService } from '@/lib/services/RaceResultsService';
|
||||
import type { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel';
|
||||
|
||||
const RacePage = () => {
|
||||
const [data, setData] = useState<RaceResultViewModel[]>();
|
||||
useEffect(() => {
|
||||
RaceResultsService.getResults().then(setData);
|
||||
}, []);
|
||||
return <div>{data?.map(d => d.formattedPosition)}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
**Service Implementation:**
|
||||
```typescript
|
||||
// apps/website/lib/services/RaceResultsService.ts
|
||||
import { apiClient } from '@/lib/api';
|
||||
import { RaceResultsPresenter } from '@/lib/presenters/RaceResultsPresenter';
|
||||
|
||||
export class RaceResultsService {
|
||||
static async getResults(): Promise<RaceResultViewModel[]> {
|
||||
const dtos = await apiClient.getRaceResults();
|
||||
return RaceResultsPresenter.present(dtos);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Presenter Implementation:**
|
||||
```typescript
|
||||
// apps/website/lib/presenters/RaceResultsPresenter.ts
|
||||
import type { RaceResultDto } from '@/lib/dtos/RaceResultDto';
|
||||
import type { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel';
|
||||
|
||||
export class RaceResultsPresenter {
|
||||
static present(dtos: RaceResultDto[]): RaceResultViewModel[] {
|
||||
return dtos.map(dto => ({
|
||||
id: dto.id,
|
||||
formattedPosition: `${dto.position}${dto.position === 1 ? 'st' : dto.position === 2 ? 'nd' : dto.position === 3 ? 'rd' : 'th'}`,
|
||||
driverName: dto.driverName,
|
||||
// ... other UI-specific formatting
|
||||
}));
|
||||
}
|
||||
}
|
||||
```
|
||||
If a type tries to do more than one of these — it is incorrectly placed.
|
||||
Reference in New Issue
Block a user