117 lines
3.4 KiB
Markdown
117 lines
3.4 KiB
Markdown
# Builders (Strict)
|
|
|
|
This document defines the **Builder** pattern for `apps/website`.
|
|
|
|
Builders exist to transform raw API data into flat, serializable **ViewData**.
|
|
|
|
## 1) Definition
|
|
|
|
A **Builder** is a deterministic, side-effect free transformation that bridges the boundary between the API (DTOs) and the Template (ViewData).
|
|
|
|
### 1.1 ViewData Builders
|
|
Transform API DTOs directly into ViewData for templates.
|
|
|
|
**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 {
|
|
return {
|
|
leagueId: apiDto.id,
|
|
name: apiDto.name,
|
|
// ... more fields
|
|
};
|
|
}
|
|
}
|
|
|
|
// Enforce static compliance without dummy instances
|
|
LeagueViewDataBuilder satisfies ViewDataBuilder<LeagueApiDto, LeagueDetailViewData>;
|
|
```
|
|
|
|
## 2) Non-negotiable rules
|
|
|
|
### ViewData Builders
|
|
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 no ViewModel Builders?
|
|
|
|
**ViewModels are self-building.**
|
|
|
|
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
|
|
↓
|
|
ViewData Builder (lib/builders/view-data/)
|
|
↓
|
|
Formatters (lib/display-objects/) -- [primitive output]
|
|
↓
|
|
ViewData (Plain JSON)
|
|
↓
|
|
Template (SSR)
|
|
↓
|
|
ViewModel (lib/view-models/) -- [new ViewModel(viewData)]
|
|
↓
|
|
Display Objects (lib/display-objects/) -- [rich API]
|
|
```
|
|
|
|
## 5) Naming convention
|
|
|
|
**ViewData Builders**: `*ViewDataBuilder`
|
|
- `LeagueViewDataBuilder`
|
|
- `RaceViewDataBuilder`
|
|
|
|
## 6) Usage example (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} />;
|
|
}
|
|
```
|
|
|
|
## 7) Enforcement
|
|
|
|
These rules are enforced by ESLint:
|
|
- `gridpilot-rules/view-data-builder-contract`
|
|
- `gridpilot-rules/filename-view-data-builder-match`
|
|
- `gridpilot-rules/formatters-must-return-primitives`
|
|
|
|
See [`docs/architecture/website/WEBSITE_CONTRACT.md`](WEBSITE_CONTRACT.md) for the authoritative contract. |