docs
This commit is contained in:
@@ -1,168 +0,0 @@
|
|||||||
# Contract Testing Quick Start Guide
|
|
||||||
|
|
||||||
## 🚀 Quick Setup
|
|
||||||
|
|
||||||
### 1. Run the Full Contract Test Suite
|
|
||||||
```bash
|
|
||||||
npm run test:contracts
|
|
||||||
```
|
|
||||||
|
|
||||||
This single command will:
|
|
||||||
- ✅ Validate API contracts
|
|
||||||
- ✅ Generate OpenAPI spec
|
|
||||||
- ✅ Generate TypeScript types
|
|
||||||
- ✅ Check for breaking changes
|
|
||||||
- ✅ Verify website type compatibility
|
|
||||||
|
|
||||||
### 2. Individual Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate API contracts only
|
|
||||||
npm run test:api:contracts
|
|
||||||
|
|
||||||
# Generate types (after making DTO changes)
|
|
||||||
npm run api:sync-types
|
|
||||||
|
|
||||||
# Check compatibility (detect breaking changes)
|
|
||||||
npm run test:contract:compatibility
|
|
||||||
|
|
||||||
# Verify website can consume types
|
|
||||||
npm run website:type-check
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 What Gets Created
|
|
||||||
|
|
||||||
### Generated Types
|
|
||||||
- **Location**: `apps/website/lib/types/generated/`
|
|
||||||
- **Files**: One `.ts` file per DTO (e.g., `RaceDTO.ts`, `DriverDTO.ts`)
|
|
||||||
- **Usage**: Import directly in website code
|
|
||||||
|
|
||||||
### Test Files
|
|
||||||
- **API Tests**: `apps/api/src/shared/testing/contractValidation.test.ts`
|
|
||||||
- **Website Tests**: `apps/website/lib/types/contractConsumption.test.ts`
|
|
||||||
|
|
||||||
### CI/CD
|
|
||||||
- **Workflow**: `.github/workflows/contract-testing.yml`
|
|
||||||
- **Triggers**: Pull requests and main branch pushes
|
|
||||||
|
|
||||||
## 🎯 Common Workflows
|
|
||||||
|
|
||||||
### Making API Changes
|
|
||||||
|
|
||||||
1. **Update DTO in API**:
|
|
||||||
```typescript
|
|
||||||
// apps/api/src/domain/race/dtos/RaceDTO.ts
|
|
||||||
export class RaceDTO {
|
|
||||||
@ApiProperty()
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
|
||||||
description?: string; // New optional field
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Run contract tests**:
|
|
||||||
```bash
|
|
||||||
npm run test:contracts
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **If tests pass**, commit your changes. The CI will regenerate types automatically.
|
|
||||||
|
|
||||||
### Updating Website Code
|
|
||||||
|
|
||||||
1. **Import generated types**:
|
|
||||||
```typescript
|
|
||||||
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
|
||||||
|
|
||||||
function RaceComponent({ race }: { race: RaceDTO }) {
|
|
||||||
return <div>{race.name}</div>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **TypeScript will catch errors** if contracts change.
|
|
||||||
|
|
||||||
### Detecting Breaking Changes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# This will show you exactly what changed
|
|
||||||
npm run test:contract:compatibility
|
|
||||||
```
|
|
||||||
|
|
||||||
Output example:
|
|
||||||
```
|
|
||||||
🚨 BREAKING CHANGES DETECTED:
|
|
||||||
• RaceDTO.status: Property status was removed (BREAKING)
|
|
||||||
|
|
||||||
❌ REMOVED:
|
|
||||||
• OldDTO: DTO OldDTO was removed
|
|
||||||
|
|
||||||
➕ ADDED:
|
|
||||||
• NewDTO: New DTO NewDTO was added
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Troubleshooting
|
|
||||||
|
|
||||||
### "Cannot find module" errors
|
|
||||||
```bash
|
|
||||||
# Regenerate types
|
|
||||||
npm run api:sync-types
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type generation fails
|
|
||||||
1. Check DTOs have `@ApiProperty` decorators
|
|
||||||
2. Verify OpenAPI spec is valid: `cat apps/api/openapi.json`
|
|
||||||
3. Run individual steps:
|
|
||||||
```bash
|
|
||||||
npm run api:generate-spec
|
|
||||||
npm run api:generate-types
|
|
||||||
```
|
|
||||||
|
|
||||||
### CI fails on breaking changes
|
|
||||||
- Review what changed
|
|
||||||
- Update website code to handle new types
|
|
||||||
- Or revert the breaking change if unintended
|
|
||||||
|
|
||||||
## 📋 Checklist Before Committing
|
|
||||||
|
|
||||||
- [ ] Run `npm run test:contracts` locally
|
|
||||||
- [ ] All tests pass
|
|
||||||
- [ ] No breaking changes (or they're intentional)
|
|
||||||
- [ ] Website code updated to handle new types
|
|
||||||
- [ ] Generated types are committed (if needed)
|
|
||||||
|
|
||||||
## 🎓 Key Concepts
|
|
||||||
|
|
||||||
### What is a "Contract"?
|
|
||||||
A contract is the agreement between API and website about what data looks like:
|
|
||||||
- DTO definitions
|
|
||||||
- Property types
|
|
||||||
- Required vs optional fields
|
|
||||||
|
|
||||||
### What are "Breaking Changes"?
|
|
||||||
Changes that would break the website:
|
|
||||||
- Removing required fields
|
|
||||||
- Changing field types
|
|
||||||
- Adding required fields to existing DTOs
|
|
||||||
|
|
||||||
### Why Generate Types?
|
|
||||||
- **Type Safety**: Catch errors at compile time
|
|
||||||
- **Auto-completion**: Better IDE experience
|
|
||||||
- **Documentation**: Types serve as living documentation
|
|
||||||
- **Consistency**: Single source of truth
|
|
||||||
|
|
||||||
## 🚨 Important Notes
|
|
||||||
|
|
||||||
1. **Never manually edit generated files** - they're auto-generated
|
|
||||||
2. **Always run tests before committing** - prevents breaking changes
|
|
||||||
3. **The CI will regenerate types** - but local verification is faster
|
|
||||||
4. **Breaking changes need review** - consider versioning strategy
|
|
||||||
|
|
||||||
## 📚 More Resources
|
|
||||||
|
|
||||||
- Full documentation: `docs/CONTRACT_TESTING.md`
|
|
||||||
- API examples: `apps/api/src/shared/testing/contractValidation.test.ts`
|
|
||||||
- Website examples: `apps/website/lib/types/contractConsumption.test.ts`
|
|
||||||
- CI/CD workflow: `.github/workflows/contract-testing.yml`
|
|
||||||
60
docs/architecture/website/CLIENT_STATE.md
Normal file
60
docs/architecture/website/CLIENT_STATE.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Client State (Strict)
|
||||||
|
|
||||||
|
This document defines the only allowed usage of client state in `apps/website`.
|
||||||
|
|
||||||
|
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
|
||||||
|
|
||||||
|
## 1) Core rule
|
||||||
|
|
||||||
|
Client state is allowed only for **UI concerns**.
|
||||||
|
|
||||||
|
The API remains the single source of truth.
|
||||||
|
|
||||||
|
## 2) Allowed client state
|
||||||
|
|
||||||
|
Client state MAY represent:
|
||||||
|
|
||||||
|
- selection (table rows, active tab)
|
||||||
|
- open/closed dialogs and drawers
|
||||||
|
- input values before submission
|
||||||
|
- loading flags
|
||||||
|
- best-effort optimistic flags
|
||||||
|
|
||||||
|
## 3) Forbidden client state
|
||||||
|
|
||||||
|
Client state MUST NOT be used as:
|
||||||
|
|
||||||
|
- business truth
|
||||||
|
- security truth
|
||||||
|
- permission truth
|
||||||
|
|
||||||
|
Examples of forbidden behavior:
|
||||||
|
|
||||||
|
- client code deciding that a user is allowed based on local flags
|
||||||
|
- client code persisting an authoritative list state that overrides server truth
|
||||||
|
|
||||||
|
## 4) Conflict resolution rule (hard)
|
||||||
|
|
||||||
|
If client state and API truth disagree, **API truth wins**.
|
||||||
|
|
||||||
|
Correct handling is:
|
||||||
|
|
||||||
|
- show the API result
|
||||||
|
- revalidate and reload server-rendered truth
|
||||||
|
|
||||||
|
## 5) Relationship to Blockers
|
||||||
|
|
||||||
|
Blockers exist to prevent UX mistakes.
|
||||||
|
|
||||||
|
- Blockers are not security.
|
||||||
|
- Blockers may reduce unnecessary requests.
|
||||||
|
- The API still enforces rules.
|
||||||
|
|
||||||
|
See [`BLOCKER_GUARDS.md`](docs/architecture/website/BLOCKER_GUARDS.md:1).
|
||||||
|
|
||||||
|
## 6) Canonical placement in this repo
|
||||||
|
|
||||||
|
- `apps/website/lib/blockers/**`
|
||||||
|
- `apps/website/lib/hooks/**`
|
||||||
|
- `apps/website/lib/command-models/**`
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ They exist to avoid duplicating presentation logic across View Models.
|
|||||||
A Display Object MAY:
|
A Display Object MAY:
|
||||||
|
|
||||||
- format values (money, dates, durations)
|
- format values (money, dates, durations)
|
||||||
- handle localization and language-specific rules
|
- handle localization only when localization inputs are deterministic (for example: mapping stable codes to stable labels)
|
||||||
- map codes to labels
|
- map codes to labels
|
||||||
- encapsulate UI display conventions
|
- encapsulate UI display conventions
|
||||||
- be reused across multiple View Models
|
- be reused across multiple View Models
|
||||||
@@ -67,6 +67,20 @@ In this repository, a Display Object MUST NOT:
|
|||||||
|
|
||||||
Reason: these are runtime-locale/timezone dependent and cause SSR/hydration mismatches.
|
Reason: these are runtime-locale/timezone dependent and cause SSR/hydration mismatches.
|
||||||
|
|
||||||
|
## Localization rule (strict)
|
||||||
|
|
||||||
|
Localization MUST NOT depend on runtime locale APIs.
|
||||||
|
|
||||||
|
Allowed approaches:
|
||||||
|
|
||||||
|
- API returns the exact labels/strings for the current user context.
|
||||||
|
- Website maps stable codes to stable labels using a deterministic table.
|
||||||
|
|
||||||
|
Forbidden approaches:
|
||||||
|
|
||||||
|
- any usage of `Intl.*`
|
||||||
|
- any usage of `toLocale*`
|
||||||
|
|
||||||
If a rule affects system correctness or persistence,
|
If a rule affects system correctness or persistence,
|
||||||
it does not belong in a Display Object.
|
it does not belong in a Display Object.
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ It applies to all write operations (create, update, delete).
|
|||||||
|
|
||||||
There are no exceptions.
|
There are no exceptions.
|
||||||
|
|
||||||
|
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
|
||||||
|
|
||||||
⸻
|
⸻
|
||||||
|
|
||||||
Core Principle
|
Core Principle
|
||||||
@@ -13,6 +15,21 @@ Read and Write paths are different.
|
|||||||
|
|
||||||
What is displayed is never sent back.
|
What is displayed is never sent back.
|
||||||
|
|
||||||
|
## Non-negotiable write boundary
|
||||||
|
|
||||||
|
All writes MUST enter the system through **Next.js Server Actions**.
|
||||||
|
|
||||||
|
Forbidden:
|
||||||
|
|
||||||
|
- client components performing write HTTP requests
|
||||||
|
- client components calling API clients for mutations
|
||||||
|
|
||||||
|
Allowed:
|
||||||
|
|
||||||
|
- client submits intent (FormData, button action)
|
||||||
|
- server action performs UX validation
|
||||||
|
- server action calls the API
|
||||||
|
|
||||||
⸻
|
⸻
|
||||||
|
|
||||||
High-Level Flow
|
High-Level Flow
|
||||||
@@ -153,4 +170,4 @@ Summary
|
|||||||
• Read Flow: DTO → ViewModel → UI
|
• Read Flow: DTO → ViewModel → UI
|
||||||
• Write Flow: UI → Command DTO → Core
|
• Write Flow: UI → Command DTO → Core
|
||||||
|
|
||||||
What is shown is never sent back.
|
What is shown is never sent back.
|
||||||
|
|||||||
56
docs/architecture/website/PRESENTERS.md
Normal file
56
docs/architecture/website/PRESENTERS.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Presenters (Strict)
|
||||||
|
|
||||||
|
This document defines the **Presenter** boundary for `apps/website`.
|
||||||
|
|
||||||
|
Presenters exist to prevent responsibility drift into:
|
||||||
|
|
||||||
|
- server routes
|
||||||
|
- Page Queries
|
||||||
|
- Templates
|
||||||
|
|
||||||
|
## 1) Definition
|
||||||
|
|
||||||
|
A **Presenter** is a deterministic, side-effect free transformation between website presentation models.
|
||||||
|
|
||||||
|
Allowed transformations:
|
||||||
|
|
||||||
|
- Page DTO → ViewData
|
||||||
|
- Page DTO → ViewModel
|
||||||
|
- ViewModel → ViewData
|
||||||
|
|
||||||
|
## 2) Non-negotiable rules
|
||||||
|
|
||||||
|
1. Presenters MUST be deterministic.
|
||||||
|
2. Presenters MUST be side-effect free.
|
||||||
|
3. Presenters MUST NOT perform HTTP.
|
||||||
|
4. Presenters MUST NOT call API clients.
|
||||||
|
5. Presenters MUST NOT access cookies/headers.
|
||||||
|
6. Presenters MAY use Display Objects.
|
||||||
|
7. Presenters MUST NOT import Templates.
|
||||||
|
|
||||||
|
## 3) Where Presenters run
|
||||||
|
|
||||||
|
Presenters run in **client code only**.
|
||||||
|
|
||||||
|
Presenters MUST be defined in `'use client'` modules.
|
||||||
|
|
||||||
|
If a computation affects routing decisions (redirect, notFound), it belongs in a Page Query or server route composition, not in a Presenter.
|
||||||
|
|
||||||
|
## 4) Relationship to Display Objects
|
||||||
|
|
||||||
|
Display Objects implement reusable formatting/mapping.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Presenters may orchestrate Display Objects.
|
||||||
|
- Display Object instances MUST NOT appear in ViewData.
|
||||||
|
|
||||||
|
See [`DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:1) and [`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:1).
|
||||||
|
|
||||||
|
## 5) Canonical placement in this repo (strict)
|
||||||
|
|
||||||
|
Presenters MUST live colocated with ViewModels under:
|
||||||
|
|
||||||
|
- `apps/website/lib/view-models/**`
|
||||||
|
|
||||||
|
Reason: this repo already treats `apps/website/lib/view-models/**` as the client-only presentation module boundary.
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
ViewData is the **only** allowed input type for Templates in `apps/website`.
|
ViewData is the **only** allowed input type for Templates in `apps/website`.
|
||||||
|
|
||||||
|
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
|
||||||
|
|
||||||
## 1) Definition
|
## 1) Definition
|
||||||
|
|
||||||
ViewData is a JSON-serializable, template-ready data structure:
|
ViewData is a JSON-serializable, template-ready data structure:
|
||||||
@@ -14,7 +16,7 @@ ViewData is a JSON-serializable, template-ready data structure:
|
|||||||
|
|
||||||
ViewData is not:
|
ViewData is not:
|
||||||
|
|
||||||
- a Page DTO (raw transport)
|
- an API Transport DTO (raw transport)
|
||||||
- a ViewModel (client-only class)
|
- a ViewModel (client-only class)
|
||||||
- a Display Object instance
|
- a Display Object instance
|
||||||
|
|
||||||
@@ -25,8 +27,13 @@ ViewData MUST be created in client code:
|
|||||||
1) Initial SSR-safe render: `ViewData = fromDTO(PageDTO)`
|
1) Initial SSR-safe render: `ViewData = fromDTO(PageDTO)`
|
||||||
2) Post-hydration render: `ViewData = fromViewModel(ViewModel)`
|
2) Post-hydration render: `ViewData = fromViewModel(ViewModel)`
|
||||||
|
|
||||||
|
These transformations are Presenters.
|
||||||
|
See [`PRESENTERS.md`](docs/architecture/website/PRESENTERS.md:1).
|
||||||
|
|
||||||
Templates MUST NOT compute derived values.
|
Templates MUST NOT compute derived values.
|
||||||
|
|
||||||
|
Presenters MUST NOT call the API.
|
||||||
|
|
||||||
## 4) Determinism rules
|
## 4) Determinism rules
|
||||||
|
|
||||||
Any formatting used to produce ViewData MUST be deterministic.
|
Any formatting used to produce ViewData MUST be deterministic.
|
||||||
@@ -38,9 +45,11 @@ Forbidden anywhere in formatting code paths:
|
|||||||
|
|
||||||
Reason: SSR and browser outputs can differ.
|
Reason: SSR and browser outputs can differ.
|
||||||
|
|
||||||
|
Localization MUST NOT depend on runtime locale APIs.
|
||||||
|
If localized strings are required, they MUST be provided as deterministic inputs (for example via API-provided labels or a deterministic code-to-label map) and passed through Presenters into ViewData.
|
||||||
|
|
||||||
## 5) Relationship to Display Objects
|
## 5) Relationship to Display Objects
|
||||||
|
|
||||||
Display Objects are used to implement formatting/mapping, but their instances MUST NOT be stored inside ViewData.
|
Display Objects are used to implement formatting/mapping, but their instances MUST NOT be stored inside ViewData.
|
||||||
|
|
||||||
Only primitive outputs produced by Display Objects may appear in ViewData.
|
Only primitive outputs produced by Display Objects may appear in ViewData.
|
||||||
|
|
||||||
|
|||||||
253
docs/architecture/website/WEBSITE_CONTRACT.md
Normal file
253
docs/architecture/website/WEBSITE_CONTRACT.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# Website Architecture Contract (Strict)
|
||||||
|
|
||||||
|
This document is the **authoritative contract** for `apps/website`.
|
||||||
|
|
||||||
|
If any other website document conflicts with this one, **this one wins**.
|
||||||
|
|
||||||
|
## 1) Purpose (non-negotiable)
|
||||||
|
|
||||||
|
The website is a **delivery layer**.
|
||||||
|
|
||||||
|
It does **not**:
|
||||||
|
|
||||||
|
- contain business rules
|
||||||
|
- make authorization decisions
|
||||||
|
- own or persist business truth
|
||||||
|
|
||||||
|
It **only**:
|
||||||
|
|
||||||
|
- renders truth from `apps/api`
|
||||||
|
- collects user intent
|
||||||
|
- forwards user intent to `apps/api`
|
||||||
|
|
||||||
|
The API is the single source of truth.
|
||||||
|
|
||||||
|
## 2) System context (hard boundary)
|
||||||
|
|
||||||
|
The website never bypasses the API.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Browser
|
||||||
|
↓
|
||||||
|
Next.js App Router (RSC + Server Actions)
|
||||||
|
↓
|
||||||
|
HTTP
|
||||||
|
↓
|
||||||
|
Backend API (Use Cases, Domain, Database)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) Website presentation model types (strict)
|
||||||
|
|
||||||
|
### 3.1 API Transport DTO
|
||||||
|
|
||||||
|
Definition: the shape returned by the backend API over HTTP.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- API Transport DTOs MUST be contained inside infrastructure.
|
||||||
|
- API Transport DTOs MUST NOT be imported by Templates.
|
||||||
|
|
||||||
|
Canonical placement in this repo:
|
||||||
|
|
||||||
|
- `apps/website/lib/types/**` (transport DTOs consumed by services and page queries)
|
||||||
|
|
||||||
|
### 3.2 Page DTO
|
||||||
|
|
||||||
|
Definition: the website-owned, server-to-client payload for a route.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- JSON-serializable only.
|
||||||
|
- Contains **raw** values only (IDs, ISO strings, numbers, codes).
|
||||||
|
- MUST NOT contain class instances.
|
||||||
|
- Created by Page Queries.
|
||||||
|
- Passed from server routes into client code.
|
||||||
|
|
||||||
|
Canonical placement in this repo:
|
||||||
|
|
||||||
|
- `apps/website/lib/page-queries/**` (composition and Page DTO construction)
|
||||||
|
|
||||||
|
### 3.3 ViewModel
|
||||||
|
|
||||||
|
Definition: the client-only, UI-owned class representing fully prepared UI state.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Instantiated only in `'use client'` modules.
|
||||||
|
- Never serialized.
|
||||||
|
- MUST NOT be passed into Templates.
|
||||||
|
|
||||||
|
See [`VIEW_MODELS.md`](docs/architecture/website/VIEW_MODELS.md:1).
|
||||||
|
|
||||||
|
Canonical placement in this repo:
|
||||||
|
|
||||||
|
- `apps/website/lib/view-models/**`
|
||||||
|
|
||||||
|
### 3.4 ViewData
|
||||||
|
|
||||||
|
Definition: the only allowed input type for Templates.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- JSON-serializable only.
|
||||||
|
- Contains only template-ready values (mostly strings/numbers/booleans).
|
||||||
|
- MUST NOT contain class instances.
|
||||||
|
|
||||||
|
See [`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:1).
|
||||||
|
|
||||||
|
Canonical placement in this repo:
|
||||||
|
|
||||||
|
- `apps/website/templates/**` (Templates that accept ViewData only)
|
||||||
|
|
||||||
|
## 4) Presentation helpers (strict)
|
||||||
|
|
||||||
|
### 4.1 Presenter
|
||||||
|
|
||||||
|
Definition: a deterministic, side-effect free transformation.
|
||||||
|
|
||||||
|
Presenters map between website presentation models:
|
||||||
|
|
||||||
|
- Page DTO → ViewData
|
||||||
|
- Page DTO → ViewModel
|
||||||
|
- ViewModel → ViewData
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- MUST be deterministic.
|
||||||
|
- MUST be side-effect free.
|
||||||
|
- MUST NOT call HTTP.
|
||||||
|
- MUST NOT call the API.
|
||||||
|
- MAY use Display Objects.
|
||||||
|
|
||||||
|
See [`PRESENTERS.md`](docs/architecture/website/PRESENTERS.md:1).
|
||||||
|
|
||||||
|
Canonical placement in this repo:
|
||||||
|
|
||||||
|
- colocated with ViewModels in `apps/website/lib/view-models/**`
|
||||||
|
|
||||||
|
### 4.2 Display Object
|
||||||
|
|
||||||
|
Definition: deterministic, reusable, UI-only formatting/mapping logic.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Class-based, immutable, deterministic.
|
||||||
|
- MUST NOT call `Intl.*`.
|
||||||
|
- MUST NOT call any `toLocale*`.
|
||||||
|
- MUST NOT implement business rules.
|
||||||
|
|
||||||
|
See [`DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:1).
|
||||||
|
|
||||||
|
Canonical placement in this repo:
|
||||||
|
|
||||||
|
- `apps/website/lib/display-objects/**`
|
||||||
|
|
||||||
|
## 5) Read flow (strict)
|
||||||
|
|
||||||
|
```text
|
||||||
|
RSC page.tsx
|
||||||
|
↓
|
||||||
|
PageQuery (server)
|
||||||
|
↓
|
||||||
|
API service / API client (infra)
|
||||||
|
↓
|
||||||
|
API Transport DTO
|
||||||
|
↓
|
||||||
|
Page DTO
|
||||||
|
↓
|
||||||
|
Presenter (client)
|
||||||
|
↓
|
||||||
|
ViewModel (optional, client)
|
||||||
|
↓
|
||||||
|
Presenter (client)
|
||||||
|
↓
|
||||||
|
ViewData
|
||||||
|
↓
|
||||||
|
Template
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6) Write flow (strict)
|
||||||
|
|
||||||
|
All writes MUST enter through **Next.js Server Actions**.
|
||||||
|
|
||||||
|
Forbidden:
|
||||||
|
|
||||||
|
- client components performing write HTTP requests
|
||||||
|
- client components calling API clients for mutations
|
||||||
|
|
||||||
|
Allowed:
|
||||||
|
|
||||||
|
- client submits intent (FormData, button action)
|
||||||
|
- server action performs UX validation
|
||||||
|
- server action calls the API
|
||||||
|
|
||||||
|
See [`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:1).
|
||||||
|
|
||||||
|
## 7) Authorization (strict)
|
||||||
|
|
||||||
|
- The website may hide/disable UI for UX.
|
||||||
|
- The website MUST NOT enforce security.
|
||||||
|
- The API enforces authentication and authorization.
|
||||||
|
|
||||||
|
See [`BLOCKER_GUARDS.md`](docs/architecture/website/BLOCKER_GUARDS.md:1).
|
||||||
|
|
||||||
|
## 7.1) Client state (strict)
|
||||||
|
|
||||||
|
Client-side state is allowed only for UI concerns.
|
||||||
|
|
||||||
|
Allowed:
|
||||||
|
|
||||||
|
- selection
|
||||||
|
- open/closed dialogs
|
||||||
|
- transient form state
|
||||||
|
- optimistic flags and loading spinners
|
||||||
|
|
||||||
|
Forbidden:
|
||||||
|
|
||||||
|
- treating client state as business truth
|
||||||
|
- using client state as an authorization decision
|
||||||
|
- persisting client state as the source of truth
|
||||||
|
|
||||||
|
Hard rule:
|
||||||
|
|
||||||
|
- any truth returned by the API MUST overwrite client assumptions.
|
||||||
|
|
||||||
|
Canonical placement in this repo:
|
||||||
|
|
||||||
|
- `apps/website/lib/blockers/**` for UX-only prevention helpers
|
||||||
|
- `apps/website/lib/hooks/**` for React-only utilities
|
||||||
|
- `apps/website/lib/command-models/**` for transient form models
|
||||||
|
|
||||||
|
See [`CLIENT_STATE.md`](docs/architecture/website/CLIENT_STATE.md:1).
|
||||||
|
|
||||||
|
## 8) DI contract (Inversify) (strict)
|
||||||
|
|
||||||
|
The DI system under [`apps/website/lib/di/index.ts`](apps/website/lib/di/index.ts:1) is **client-first**.
|
||||||
|
|
||||||
|
Server execution is concurrent. Any shared singleton container can leak cross-request state.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
1. Server `app/**/page.tsx` MUST NOT access the container.
|
||||||
|
2. Page Queries SHOULD prefer manual wiring.
|
||||||
|
3. Client modules MAY use DI via [`ContainerProvider`](apps/website/lib/di/index.ts:11) and hooks.
|
||||||
|
4. [`ContainerManager.getContainer()`](apps/website/lib/di/container.ts:74) is **client-only**.
|
||||||
|
5. Any server DI usage MUST be request-scoped (a fresh container per request).
|
||||||
|
|
||||||
|
Hard constraint:
|
||||||
|
|
||||||
|
- A singleton Inversify container MUST NOT be used to serve concurrent server requests.
|
||||||
|
|
||||||
|
See [`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:1).
|
||||||
|
|
||||||
|
## 9) Non-negotiable rules (final)
|
||||||
|
|
||||||
|
1. The API is the brain.
|
||||||
|
2. The website is a terminal.
|
||||||
|
3. API Transport DTOs never reach Templates.
|
||||||
|
4. ViewModels never go to the API.
|
||||||
|
5. Templates accept ViewData only.
|
||||||
|
6. Page Queries do not format; they only compose.
|
||||||
|
7. Presenters are pure and deterministic.
|
||||||
|
8. Server Actions are the only write entry point.
|
||||||
|
9. Authorization always belongs to the API.
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
This repo uses Inversify DI under [apps/website/lib/di](apps/website/lib/di/index.ts:1).
|
This repo uses Inversify DI under [apps/website/lib/di](apps/website/lib/di/index.ts:1).
|
||||||
|
|
||||||
|
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
|
||||||
|
|
||||||
## 1) Non-negotiable safety rule
|
## 1) Non-negotiable safety rule
|
||||||
|
|
||||||
No stateful service instances may be shared across requests.
|
No stateful service instances may be shared across requests.
|
||||||
@@ -20,6 +22,8 @@ Reason: Next.js server execution is concurrent; shared state causes cross-reques
|
|||||||
- SHOULD prefer explicit construction (manual wiring).
|
- SHOULD prefer explicit construction (manual wiring).
|
||||||
- MAY use DI only if all resolved services are stateless and safe for concurrent requests.
|
- MAY use DI only if all resolved services are stateless and safe for concurrent requests.
|
||||||
|
|
||||||
|
If DI is used on the server, it MUST be request-scoped (a new container per request) and MUST NOT reuse a shared singleton container.
|
||||||
|
|
||||||
### 2.3 Client modules
|
### 2.3 Client modules
|
||||||
|
|
||||||
- MAY use DI via `ContainerProvider` and hooks (example: `useInject`).
|
- MAY use DI via `ContainerProvider` and hooks (example: `useInject`).
|
||||||
@@ -28,3 +32,7 @@ Reason: Next.js server execution is concurrent; shared state causes cross-reques
|
|||||||
|
|
||||||
[`ContainerManager`](apps/website/lib/di/container.ts:61) holds a singleton container. Treat it as **unsafe for server request scope** unless proven otherwise.
|
[`ContainerManager`](apps/website/lib/di/container.ts:61) holds a singleton container. Treat it as **unsafe for server request scope** unless proven otherwise.
|
||||||
|
|
||||||
|
Strict rule:
|
||||||
|
|
||||||
|
- [`ContainerManager.getContainer()`](apps/website/lib/di/container.ts:74) is client-only.
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
This document defines architecture guardrails that must be enforced via tests + ESLint.
|
This document defines architecture guardrails that must be enforced via tests + ESLint.
|
||||||
|
|
||||||
|
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
|
||||||
|
|
||||||
## 1) RSC boundary guardrails
|
## 1) RSC boundary guardrails
|
||||||
|
|
||||||
Fail CI if any `apps/website/app/**/page.tsx`:
|
Fail CI if any `apps/website/app/**/page.tsx`:
|
||||||
|
|
||||||
- imports from `apps/website/lib/view-models/*`
|
- imports from `apps/website/lib/view-models/*`
|
||||||
|
- imports from Presenter code (presenters live colocated with ViewModels)
|
||||||
- calls `Intl.*` or `toLocale*`
|
- calls `Intl.*` or `toLocale*`
|
||||||
- performs sorting/filtering (`sort`, `filter`, `reduce`) beyond trivial null checks
|
- performs sorting/filtering (`sort`, `filter`, `reduce`) beyond trivial null checks
|
||||||
|
|
||||||
@@ -15,6 +18,7 @@ Fail CI if any `apps/website/app/**/page.tsx`:
|
|||||||
Fail CI if any `apps/website/templates/**`:
|
Fail CI if any `apps/website/templates/**`:
|
||||||
|
|
||||||
- imports from `apps/website/lib/view-models/*`
|
- imports from `apps/website/lib/view-models/*`
|
||||||
|
- imports from presenter code (presenters live colocated with ViewModels)
|
||||||
- imports from `apps/website/lib/display-objects/*`
|
- imports from `apps/website/lib/display-objects/*`
|
||||||
- calls `Intl.*` or `toLocale*`
|
- calls `Intl.*` or `toLocale*`
|
||||||
|
|
||||||
@@ -27,4 +31,3 @@ Fail CI if any `apps/website/lib/display-objects/**`:
|
|||||||
- calls `Intl.*` or `toLocale*`
|
- calls `Intl.*` or `toLocale*`
|
||||||
|
|
||||||
Display Objects must be deterministic.
|
Display Objects must be deterministic.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ This document defines the only allowed presentation architecture for `apps/websi
|
|||||||
|
|
||||||
It is **website-only** and does not change `apps/api` or `core` architecture.
|
It is **website-only** and does not change `apps/api` or `core` architecture.
|
||||||
|
|
||||||
|
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
|
||||||
|
|
||||||
## 1) Core rule: API owns business truth
|
## 1) Core rule: API owns business truth
|
||||||
|
|
||||||
- `apps/api` is the only source of truth for business rules and canonical filtering/sorting.
|
- `apps/api` is the only source of truth for business rules and canonical filtering/sorting.
|
||||||
@@ -42,6 +44,9 @@ It is **website-only** and does not change `apps/api` or `core` architecture.
|
|||||||
- Contains only values ready to render (mostly strings/numbers).
|
- Contains only values ready to render (mostly strings/numbers).
|
||||||
- Built from Page DTO (initial render) and from ViewModel (post-hydration).
|
- Built from Page DTO (initial render) and from ViewModel (post-hydration).
|
||||||
|
|
||||||
|
The mapping between Page DTO, ViewModel, and ViewData is performed by Presenters.
|
||||||
|
See [`PRESENTERS.md`](docs/architecture/website/PRESENTERS.md:1).
|
||||||
|
|
||||||
## 3) Required per-route structure
|
## 3) Required per-route structure
|
||||||
|
|
||||||
Every route MUST follow:
|
Every route MUST follow:
|
||||||
@@ -50,6 +55,9 @@ Every route MUST follow:
|
|||||||
2) `*PageClient.tsx` (client): builds ViewData and renders Template
|
2) `*PageClient.tsx` (client): builds ViewData and renders Template
|
||||||
3) `*Template.tsx` (pure UI): renders ViewData only
|
3) `*Template.tsx` (pure UI): renders ViewData only
|
||||||
|
|
||||||
|
All writes enter through Server Actions.
|
||||||
|
See [`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:1).
|
||||||
|
|
||||||
## 4) Authoritative specification
|
## 4) Authoritative specification
|
||||||
|
|
||||||
This document is an entry point only.
|
This document is an entry point only.
|
||||||
@@ -58,3 +66,6 @@ The authoritative, test-enforced spec lives at:
|
|||||||
|
|
||||||
- [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1)
|
- [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1)
|
||||||
|
|
||||||
|
Final contract:
|
||||||
|
|
||||||
|
- [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1)
|
||||||
|
|||||||
Reference in New Issue
Block a user