do to formatters

This commit is contained in:
2026-01-24 01:22:43 +01:00
parent 891b3cf0ee
commit 705f9685b5
18 changed files with 361 additions and 760 deletions

View File

@@ -2,41 +2,25 @@
This document defines the **Builder** pattern for `apps/website`.
Builders exist to transform data between presentation model types.
Builders exist to transform raw API data into flat, serializable **ViewData**.
## 1) Definition
A **Builder** is a deterministic, side-effect free transformation between website presentation models.
A **Builder** is a deterministic, side-effect free transformation that bridges the boundary between the API (DTOs) and the Template (ViewData).
There are two types of builders:
### 1.1 ViewModel Builders
Transform API Transport DTOs into ViewModels.
**Purpose**: Prepare raw API data for client-side state management.
**Location**: `apps/website/lib/builders/view-models/**`
**Pattern**:
```typescript
export class AdminViewModelBuilder {
static build(dto: UserDto): AdminUserViewModel {
return new AdminUserViewModel(dto);
}
}
```
### 1.2 ViewData Builders
### 1.1 ViewData Builders
Transform API DTOs directly into ViewData for templates.
**Purpose**: Prepare API data for server-side rendering without ViewModels.
**Purpose**: Prepare API data for server-side rendering. They ensure that logic-rich behavior is stripped away, leaving only a "dumb" JSON structure safe for SSR and hydration.
**Location**: `apps/website/lib/builders/view-data/**`
**Pattern**:
```typescript
import { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
export class LeagueViewDataBuilder {
static build(apiDto: LeagueApiDto): LeagueDetailViewData extends ViewData {
static build(apiDto: LeagueApiDto): LeagueDetailViewData {
return {
leagueId: apiDto.id,
name: apiDto.name,
@@ -44,164 +28,90 @@ export class LeagueViewDataBuilder {
};
}
}
// Enforce static compliance without dummy instances
LeagueViewDataBuilder satisfies ViewDataBuilder<LeagueApiDto, LeagueDetailViewData>;
```
## 2) Non-negotiable rules
### ViewModel Builders
1. MUST be deterministic
2. MUST be side-effect free
3. MUST NOT perform HTTP
4. MUST NOT call API clients
5. MUST NOT access cookies/headers
6. Input: API Transport DTO
7. Output: ViewModel
8. MUST live in `lib/builders/view-models/**`
### ViewData Builders
1. MUST be deterministic
2. MUST be side-effect free
3. MUST NOT perform HTTP
4. MUST NOT call API clients
5. MUST NOT access cookies/headers
6. Input: API DTO
7. Output: ViewData
8. MUST live in `lib/builders/view-data/**`
1. MUST be deterministic.
2. MUST be side-effect free.
3. MUST NOT perform HTTP or call API clients.
4. Input: API DTO.
5. Output: ViewData (Plain JSON).
6. MUST live in `lib/builders/view-data/**`.
7. MUST use `static build()` and `satisfies ViewDataBuilder`.
8. MUST use **Formatters** for primitive output (strings/numbers).
## 3) Why two builder types?
## 3) Why no ViewModel Builders?
**ViewModel Builders** (API → Client State):
- Bridge the API boundary
- Convert transport types to client classes
- Add client-only fields if needed
- Run in client code
**ViewModels are self-building.**
**ViewData Builders** (API → Render Data):
- Bridge the presentation boundary
- Transform API data directly for templates
- Format values for display
- Run in server code (RSC)
A ViewModel is a class that wraps data to provide behavior. Instead of a separate builder class, ViewModels are instantiated directly from ViewData using their **Constructor**. This removes unnecessary "ceremony" and keeps the API unambiguous.
**❌ Redundant Pattern (Forbidden):**
```typescript
// Why have this extra class?
export class TeamViewModelBuilder {
static build(data: TeamViewData): TeamViewModel {
return new TeamViewModel(data);
}
}
```
**✅ Clean Pattern (Required):**
```typescript
// Just use the class itself in the ClientWrapper
const vm = new TeamViewModel(viewData);
```
## 4) Relationship to other patterns
```
API Transport DTO
ViewModel Builder (lib/builders/view-models/)
ViewModel (lib/view-models/)
(for client components)
API Transport DTO
ViewData Builder (lib/builders/view-data/)
ViewData (lib/templates/)
Formatters (lib/display-objects/) -- [primitive output]
Template (lib/templates/)
ViewData (Plain JSON)
Template (SSR)
ViewModel (lib/view-models/) -- [new ViewModel(viewData)]
Display Objects (lib/display-objects/) -- [rich API]
```
## 5) Naming convention
**ViewModel Builders**: `*ViewModelBuilder`
- `AdminViewModelBuilder`
- `RaceViewModelBuilder`
**ViewData Builders**: `*ViewDataBuilder`
- `LeagueViewDataBuilder`
- `RaceViewDataBuilder`
## 6) File structure
## 6) Usage example (Server Component)
```
lib/
builders/
view-models/
AdminViewModelBuilder.ts
RaceViewModelBuilder.ts
index.ts
view-data/
LeagueViewDataBuilder.ts
RaceViewDataBuilder.ts
index.ts
```
## 7) Usage examples
### ViewModel Builder (Client Component)
```typescript
'use client';
import { AdminViewModelBuilder } from '@/lib/builders/view-models/AdminViewModelBuilder';
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
export function AdminPage() {
const [users, setUsers] = useState<AdminUserViewModel[]>([]);
useEffect(() => {
const apiClient = new AdminApiClient();
const dto = await apiClient.getUsers();
const viewModels = dto.map(d => AdminViewModelBuilder.build(d));
setUsers(viewModels);
}, []);
// ... render with viewModels
}
```
### ViewData Builder (Server Component)
```typescript
import { LeagueViewDataBuilder } from '@/lib/builders/view-data/LeagueViewDataBuilder';
import { LeagueDetailPageQuery } from '@/lib/page-queries/LeagueDetailPageQuery';
export default async function LeagueDetailPage({ params }) {
const apiDto = await LeagueDetailPageQuery.execute(params.id);
// Transform to flat JSON for SSR
const viewData = LeagueViewDataBuilder.build(apiDto);
return <LeagueDetailTemplate viewData={viewData} />;
}
```
## 8) Common mistakes
**Wrong**: Using "Presenter" for DTO → ViewModel
```typescript
// DON'T
export class AdminPresenter {
static createViewModel(dto: UserDto): AdminUserViewModel { ... }
}
```
**Correct**: Use ViewModelBuilder
```typescript
export class AdminViewModelBuilder {
static build(dto: UserDto): AdminUserViewModel { ... }
}
```
**Wrong**: Using "Transformer" for ViewModel → ViewData
```typescript
// DON'T
export class RaceResultsDataTransformer {
static transform(...): TransformedData { ... }
}
```
**Correct**: Use ViewDataBuilder
```typescript
export class RaceResultsViewDataBuilder {
static build(...): RaceResultsViewData extends ViewData { ... }
}
```
## 9) Enforcement
## 7) Enforcement
These rules are enforced by ESLint:
- `gridpilot-rules/view-model-builder-contract`
- `gridpilot-rules/view-data-builder-contract`
- `gridpilot-rules/filename-view-model-builder-match`
- `gridpilot-rules/filename-view-data-builder-match`
- `gridpilot-rules/formatters-must-return-primitives`
See [`docs/architecture/website/WEBSITE_GUARDRAILS.md`](WEBSITE_GUARDRAILS.md) for details.
See [`docs/architecture/website/WEBSITE_CONTRACT.md`](WEBSITE_CONTRACT.md) for the authoritative contract.