207 lines
4.8 KiB
Markdown
207 lines
4.8 KiB
Markdown
# Builders (Strict)
|
|
|
|
This document defines the **Builder** pattern for `apps/website`.
|
|
|
|
Builders exist to transform data between presentation model types.
|
|
|
|
## 1) Definition
|
|
|
|
A **Builder** is a deterministic, side-effect free transformation between website presentation models.
|
|
|
|
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
|
|
Transform API DTOs directly into ViewData for templates.
|
|
|
|
**Purpose**: Prepare API data for server-side rendering without ViewModels.
|
|
|
|
**Location**: `apps/website/lib/builders/view-data/**`
|
|
|
|
**Pattern**:
|
|
```typescript
|
|
export class LeagueViewDataBuilder {
|
|
static build(apiDto: LeagueApiDto): LeagueDetailViewData {
|
|
return {
|
|
leagueId: apiDto.id,
|
|
name: apiDto.name,
|
|
// ... more fields
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
## 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/**`
|
|
|
|
## 3) Why two builder types?
|
|
|
|
**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
|
|
|
|
**ViewData Builders** (API → Render Data):
|
|
- Bridge the presentation boundary
|
|
- Transform API data directly for templates
|
|
- Format values for display
|
|
- Run in server code (RSC)
|
|
|
|
## 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/)
|
|
↓
|
|
Template (lib/templates/)
|
|
```
|
|
|
|
## 5) Naming convention
|
|
|
|
**ViewModel Builders**: `*ViewModelBuilder`
|
|
- `AdminViewModelBuilder`
|
|
- `RaceViewModelBuilder`
|
|
|
|
**ViewData Builders**: `*ViewDataBuilder`
|
|
- `LeagueViewDataBuilder`
|
|
- `RaceViewDataBuilder`
|
|
|
|
## 6) File structure
|
|
|
|
```
|
|
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);
|
|
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 { ... }
|
|
}
|
|
```
|
|
|
|
## 9) 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`
|
|
|
|
See [`docs/architecture/website/WEBSITE_GUARDRAILS.md`](WEBSITE_GUARDRAILS.md) for details. |