docs
This commit is contained in:
154
docs/architecture/website/BLOCKER_GUARDS.md
Normal file
154
docs/architecture/website/BLOCKER_GUARDS.md
Normal file
@@ -0,0 +1,154 @@
|
||||
Blockers & Guards
|
||||
|
||||
This document defines clear, non-overlapping responsibilities for Blockers (frontend) and Guards (backend).
|
||||
The goal is to prevent semantic drift, security confusion, and inconsistent implementations.
|
||||
|
||||
⸻
|
||||
|
||||
Core Principle
|
||||
|
||||
Guards enforce. Blockers prevent.
|
||||
• Guards protect the system.
|
||||
• Blockers protect the UX.
|
||||
|
||||
There are no exceptions to this rule.
|
||||
|
||||
⸻
|
||||
|
||||
Backend — Guards (NestJS)
|
||||
|
||||
Definition
|
||||
|
||||
A Guard is a backend mechanism that enforces access or execution rules.
|
||||
If a Guard denies execution, the request does not reach the application logic.
|
||||
|
||||
In NestJS, Guards implement CanActivate.
|
||||
|
||||
⸻
|
||||
|
||||
Responsibilities
|
||||
|
||||
Guards MAY:
|
||||
• block requests entirely
|
||||
• return HTTP errors (401, 403, 429)
|
||||
• enforce authentication and authorization
|
||||
• enforce rate limits
|
||||
• enforce feature availability
|
||||
• protect against abuse and attacks
|
||||
|
||||
Guards MUST:
|
||||
• be deterministic
|
||||
• be authoritative
|
||||
• be security-relevant
|
||||
|
||||
⸻
|
||||
|
||||
Restrictions
|
||||
|
||||
Guards MUST NOT:
|
||||
• depend on frontend state
|
||||
• contain UI logic
|
||||
• attempt to improve UX
|
||||
• assume the client behaved correctly
|
||||
|
||||
⸻
|
||||
|
||||
Common Backend Guards
|
||||
• AuthGuard
|
||||
• RolesGuard
|
||||
• PermissionsGuard
|
||||
• ThrottlerGuard (NestJS)
|
||||
• RateLimitGuard
|
||||
• CsrfGuard
|
||||
• FeatureFlagGuard
|
||||
|
||||
⸻
|
||||
|
||||
Summary (Backend)
|
||||
• Guards decide
|
||||
• Guards enforce
|
||||
• Guards secure the system
|
||||
|
||||
⸻
|
||||
|
||||
Frontend — Blockers
|
||||
|
||||
Definition
|
||||
|
||||
A Blocker is a frontend mechanism that prevents an action from being executed.
|
||||
Blockers exist solely to improve UX and reduce unnecessary requests.
|
||||
|
||||
Blockers are not security mechanisms.
|
||||
|
||||
⸻
|
||||
|
||||
Responsibilities
|
||||
|
||||
Blockers MAY:
|
||||
• prevent multiple submissions
|
||||
• disable actions temporarily
|
||||
• debounce or throttle interactions
|
||||
• hide or disable UI elements
|
||||
• prevent navigation under certain conditions
|
||||
|
||||
Blockers MUST:
|
||||
• be reversible
|
||||
• be local to the frontend
|
||||
• be treated as best-effort helpers
|
||||
|
||||
⸻
|
||||
|
||||
Restrictions
|
||||
|
||||
Blockers MUST NOT:
|
||||
• enforce security
|
||||
• claim authorization
|
||||
• block access permanently
|
||||
• replace backend Guards
|
||||
• make assumptions about backend state
|
||||
|
||||
⸻
|
||||
|
||||
Common Frontend Blockers
|
||||
• SubmitBlocker
|
||||
• AuthBlocker
|
||||
• RoleBlocker
|
||||
• ThrottleBlocker
|
||||
• NavigationBlocker
|
||||
• FeatureBlocker
|
||||
|
||||
⸻
|
||||
|
||||
Summary (Frontend)
|
||||
• Blockers prevent execution
|
||||
• Blockers improve UX
|
||||
• Blockers reduce mistakes and load
|
||||
|
||||
⸻
|
||||
|
||||
Clear Separation
|
||||
|
||||
Aspect Blocker (Frontend) Guard (Backend)
|
||||
Purpose Prevent execution Enforce rules
|
||||
Security ❌ No ✅ Yes
|
||||
Authority ❌ Best-effort ✅ Final
|
||||
Reversible ✅ Yes ❌ No
|
||||
Failure effect UI feedback HTTP error
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
Naming Rules (Hard)
|
||||
• Frontend uses *Blocker
|
||||
• Backend uses *Guard
|
||||
• Never mix the terms
|
||||
• Never implement Guards in the frontend
|
||||
• Never implement Blockers in the backend
|
||||
|
||||
⸻
|
||||
|
||||
Final Rule
|
||||
|
||||
If it must be enforced, it is a Guard.
|
||||
|
||||
If it only prevents UX mistakes, it is a Blocker.
|
||||
156
docs/architecture/website/COMMAND_MODELS.md
Normal file
156
docs/architecture/website/COMMAND_MODELS.md
Normal file
@@ -0,0 +1,156 @@
|
||||
Command Models
|
||||
|
||||
This document defines Command Models as a first-class concept in the frontend architecture.
|
||||
Command Models are UX-only write models used to collect, validate, and prepare user input
|
||||
before it is sent to the backend as a Command DTO.
|
||||
|
||||
Command Models are not View Models and not Domain Models.
|
||||
|
||||
⸻
|
||||
|
||||
Purpose
|
||||
|
||||
A Form Model answers the question:
|
||||
|
||||
“What does the UI need in order to safely submit user input?”
|
||||
|
||||
Command Models exist to:
|
||||
• centralize form state
|
||||
• reduce logic inside components
|
||||
• provide consistent client-side validation
|
||||
• build Command DTOs explicitly
|
||||
|
||||
⸻
|
||||
|
||||
Core Rules
|
||||
|
||||
Command Models:
|
||||
• exist only in the frontend
|
||||
• are write-only (never reused for reads)
|
||||
• are created per form
|
||||
• are discarded after submission
|
||||
|
||||
Command Models MUST NOT:
|
||||
• contain business logic
|
||||
• enforce domain rules
|
||||
• reference View Models
|
||||
• reference Domain Entities or Value Objects
|
||||
• be sent to the API directly
|
||||
|
||||
⸻
|
||||
|
||||
Relationship to Other Models
|
||||
|
||||
API DTO (read) → ViewModel → UI
|
||||
|
||||
UI Input → FormModel → Command DTO → API
|
||||
|
||||
• View Models are read-only
|
||||
• Command Models are write-only
|
||||
• No model is reused across read/write boundaries
|
||||
|
||||
⸻
|
||||
|
||||
Typical Responsibilities
|
||||
|
||||
A Form Model MAY:
|
||||
• store field values
|
||||
• track dirty / touched state
|
||||
• perform basic UX validation
|
||||
• expose isValid, canSubmit
|
||||
• build a Command DTO
|
||||
|
||||
A Form Model MUST NOT:
|
||||
• decide if an action is allowed
|
||||
• perform authorization checks
|
||||
• validate cross-aggregate rules
|
||||
|
||||
⸻
|
||||
|
||||
Validation Guidelines
|
||||
|
||||
Client-side validation is UX validation, not business validation.
|
||||
|
||||
Allowed validation examples:
|
||||
• required fields
|
||||
• min / max length
|
||||
• email format
|
||||
• numeric ranges
|
||||
|
||||
Forbidden validation examples:
|
||||
• “user is not allowed”
|
||||
• “league already exists”
|
||||
• “quota exceeded”
|
||||
|
||||
Server validation is the source of truth.
|
||||
|
||||
⸻
|
||||
|
||||
Example: Simple Form Model (with class-validator)
|
||||
|
||||
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
|
||||
|
||||
export class SignupFormModel {
|
||||
@IsEmail()
|
||||
email = '';
|
||||
|
||||
@IsNotEmpty()
|
||||
@MinLength(8)
|
||||
password = '';
|
||||
|
||||
isSubmitting = false;
|
||||
|
||||
reset(): void {
|
||||
this.email = '';
|
||||
this.password = '';
|
||||
}
|
||||
|
||||
toCommand(): SignupCommandDto {
|
||||
return {
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
Usage in UI Component
|
||||
|
||||
const form = useFormModel(SignupFormModel);
|
||||
|
||||
async function onSubmit() {
|
||||
if (!form.isValid()) return;
|
||||
|
||||
form.isSubmitting = true;
|
||||
|
||||
await authService.signup(form.toCommand());
|
||||
}
|
||||
|
||||
The component:
|
||||
• binds inputs to the Form Model
|
||||
• reacts to validation state
|
||||
• never builds DTOs manually
|
||||
|
||||
⸻
|
||||
|
||||
Testing
|
||||
|
||||
Command Models SHOULD be tested when they contain:
|
||||
• validation rules
|
||||
• non-trivial state transitions
|
||||
• command construction logic
|
||||
|
||||
Command Models do NOT need tests if they only hold fields without logic.
|
||||
|
||||
⸻
|
||||
|
||||
Summary
|
||||
• Command Models are UX helpers for writes
|
||||
• They protect components from complexity
|
||||
• They never replace backend validation
|
||||
• They never leak into read flows
|
||||
|
||||
Command Models help users.
|
||||
Use Cases protect the system.
|
||||
125
docs/architecture/website/DISPLAY_OBJECTS.md
Normal file
125
docs/architecture/website/DISPLAY_OBJECTS.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Display Objects
|
||||
|
||||
## Definition
|
||||
|
||||
A **Display Object** encapsulates **reusable, UI-only display logic**.
|
||||
|
||||
In this codebase, a Display Object is a **Frontend Value Object**:
|
||||
|
||||
- class-based
|
||||
- immutable
|
||||
- deterministic
|
||||
- side-effect free
|
||||
|
||||
It answers the question:
|
||||
|
||||
> “How should this specific piece of information be shown?”
|
||||
|
||||
Display Objects are **not screen-specific**.
|
||||
They exist to avoid duplicating presentation logic across View Models.
|
||||
|
||||
---
|
||||
|
||||
## Responsibilities
|
||||
|
||||
A Display Object MAY:
|
||||
|
||||
- format values (money, dates, durations)
|
||||
- handle localization and language-specific rules
|
||||
- map codes to labels
|
||||
- encapsulate UI display conventions
|
||||
- be reused across multiple View Models
|
||||
|
||||
In addition, a Display Object MAY:
|
||||
|
||||
- normalize presentation inputs (for example trimming/casing)
|
||||
- expose multiple explicit display variants (for example `shortLabel`, `longLabel`)
|
||||
|
||||
A Display Object MUST:
|
||||
|
||||
- be deterministic
|
||||
- be side-effect free
|
||||
- operate only on presentation data
|
||||
|
||||
A Display Object MUST:
|
||||
|
||||
- be implemented as a **class** with a small, explicit API
|
||||
- accept only primitives/plain data in its constructor (or static factory)
|
||||
- expose only primitive outputs (strings/numbers/booleans)
|
||||
|
||||
---
|
||||
|
||||
## Restrictions
|
||||
|
||||
A Display Object MUST NOT:
|
||||
|
||||
- contain business logic
|
||||
- enforce domain invariants
|
||||
- perform validation
|
||||
- influence system behavior
|
||||
- be sent back to the server
|
||||
- depend on backend or infrastructure concerns
|
||||
|
||||
In this repository, a Display Object MUST NOT:
|
||||
|
||||
- call `Intl.*`
|
||||
- call `Date.toLocaleString()` / `Date.toLocaleDateString()` / `Date.toLocaleTimeString()`
|
||||
|
||||
Reason: these are runtime-locale/timezone dependent and cause SSR/hydration mismatches.
|
||||
|
||||
If a rule affects system correctness or persistence,
|
||||
it does not belong in a Display Object.
|
||||
|
||||
---
|
||||
|
||||
## Ownership & Placement
|
||||
|
||||
- Display Objects belong to the **presentation layer**
|
||||
- They are frontend-only
|
||||
- They are not shared with the backend or core
|
||||
|
||||
Placement rule (strict):
|
||||
|
||||
- Display Objects live under `apps/website/lib/display-objects/*`.
|
||||
|
||||
---
|
||||
|
||||
## Relationship to View Models
|
||||
|
||||
- View Models MAY use Display Objects
|
||||
- Display Objects MUST NOT depend on View Models
|
||||
- Display Objects represent **parts**
|
||||
- View Models represent **screens**
|
||||
|
||||
Additional strict rules:
|
||||
|
||||
- View Models SHOULD compose Display Objects.
|
||||
- Display Objects MUST NOT be serialized or passed across boundaries.
|
||||
- They must not appear in server-to-client DTOs.
|
||||
- Templates should receive primitive display outputs, not Display Object instances.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Display Objects SHOULD be tested because they often contain:
|
||||
|
||||
- locale-specific behavior
|
||||
- formatting rules
|
||||
- edge cases visible to users
|
||||
|
||||
Additionally:
|
||||
|
||||
- test determinism by running the same inputs under Node and browser contexts (where applicable)
|
||||
- test boundary rules (no `Intl.*`, no `toLocale*`)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- Display Objects encapsulate **how something looks**
|
||||
- View Models encapsulate **what a screen needs**
|
||||
- Both are presentation concerns
|
||||
- Neither contains business truth
|
||||
|
||||
In one sentence: Display Objects are **Value Objects for UI display**, not utility functions.
|
||||
156
docs/architecture/website/FORM_SUBMISSION.md
Normal file
156
docs/architecture/website/FORM_SUBMISSION.md
Normal file
@@ -0,0 +1,156 @@
|
||||
Form Submission Flow (UI → System)
|
||||
|
||||
This document defines the only valid data flow when a user submits a form.
|
||||
It applies to all write operations (create, update, delete).
|
||||
|
||||
There are no exceptions.
|
||||
|
||||
⸻
|
||||
|
||||
Core Principle
|
||||
|
||||
Read and Write paths are different.
|
||||
|
||||
What is displayed is never sent back.
|
||||
|
||||
⸻
|
||||
|
||||
High-Level Flow
|
||||
|
||||
UI → Command DTO → API → Core Use Case → Persistence
|
||||
|
||||
• View Models are read-only
|
||||
• Display Objects are read-only
|
||||
• Commands are write-only
|
||||
|
||||
⸻
|
||||
|
||||
1. UI (Component)
|
||||
|
||||
Responsibility
|
||||
• Collect user input
|
||||
• Manage UX state (loading, disabled, local errors)
|
||||
|
||||
Rules
|
||||
• Only primitives are handled (string, number, boolean)
|
||||
• No DTO reuse
|
||||
• No ViewModel reuse
|
||||
• No domain objects
|
||||
|
||||
The UI does not decide whether an action is allowed.
|
||||
|
||||
⸻
|
||||
|
||||
2. Form Model (Optional)
|
||||
|
||||
Responsibility
|
||||
• Local form state
|
||||
• Client-side validation (required, min/max length)
|
||||
• Field-level errors
|
||||
|
||||
Rules
|
||||
• UX-only validation
|
||||
• No business rules
|
||||
• Never shared with API or Core
|
||||
|
||||
⸻
|
||||
|
||||
3. Command DTO (Frontend)
|
||||
|
||||
Responsibility
|
||||
• Express intent
|
||||
• Represent a write operation
|
||||
|
||||
Rules
|
||||
• Created fresh on submit
|
||||
• Never derived from a ViewModel
|
||||
• Never reused from read DTOs
|
||||
• Write-only
|
||||
|
||||
⸻
|
||||
|
||||
4. Frontend Service
|
||||
|
||||
Responsibility
|
||||
• Orchestrate the write action
|
||||
• Call the API Client
|
||||
• Propagate success or failure
|
||||
|
||||
Rules
|
||||
• No business logic
|
||||
• No validation
|
||||
• No UI decisions
|
||||
• No ViewModel creation for writes (except explicit success summaries)
|
||||
|
||||
⸻
|
||||
|
||||
5. API Client (Frontend)
|
||||
|
||||
Responsibility
|
||||
• Perform HTTP request
|
||||
• Handle transport-level failures
|
||||
|
||||
Rules
|
||||
• Stateless
|
||||
• No retries unless explicitly designed
|
||||
• Throws technical errors only
|
||||
|
||||
⸻
|
||||
|
||||
6. API Layer (Backend)
|
||||
|
||||
Responsibility
|
||||
• HTTP boundary
|
||||
• Transport validation (schema / class-validator)
|
||||
• Map API DTO → Core Command
|
||||
|
||||
Rules
|
||||
• No business logic
|
||||
• No persistence
|
||||
• No UI concerns
|
||||
|
||||
⸻
|
||||
|
||||
7. Core Use Case
|
||||
|
||||
Responsibility
|
||||
• Enforce business rules
|
||||
• Validate domain invariants
|
||||
• Change system state
|
||||
|
||||
Rules
|
||||
• Single source of truth
|
||||
• No UI logic
|
||||
• No HTTP knowledge
|
||||
|
||||
⸻
|
||||
|
||||
Response Handling
|
||||
|
||||
Success
|
||||
• API returns a Result DTO (IDs, status)
|
||||
• Frontend reacts by:
|
||||
• navigation
|
||||
• reload via GET
|
||||
• toast / confirmation
|
||||
|
||||
Failure
|
||||
• Business errors → user-visible message
|
||||
• Technical errors → error boundary / monitoring
|
||||
|
||||
⸻
|
||||
|
||||
Forbidden Patterns
|
||||
• ViewModel → Command
|
||||
• DisplayObject → API
|
||||
• DTO roundtrip
|
||||
• Domain Object in UI
|
||||
• Reusing read models for writes
|
||||
|
||||
⸻
|
||||
|
||||
Summary
|
||||
• Read Flow: DTO → ViewModel → UI
|
||||
• Write Flow: UI → Command DTO → Core
|
||||
|
||||
What is shown is never sent back.
|
||||
132
docs/architecture/website/LOGIN_FLOW_STATE_MACHINE.md
Normal file
132
docs/architecture/website/LOGIN_FLOW_STATE_MACHINE.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Login Flow State Machine Architecture
|
||||
|
||||
## Problem
|
||||
The current login page has unpredictable behavior due to:
|
||||
- Multiple useEffect runs with different session states
|
||||
- Race conditions between session loading and redirect logic
|
||||
- Client-side redirects that interfere with test expectations
|
||||
|
||||
## Solution: State Machine Pattern
|
||||
|
||||
### State Definitions
|
||||
|
||||
```typescript
|
||||
enum LoginState {
|
||||
UNAUTHENTICATED = "UNAUTHENTICATED",
|
||||
AUTHENTICATED_WITH_PERMISSIONS = "AUTHENTICATED_WITH_PERMISSIONS",
|
||||
AUTHENTICATED_WITHOUT_PERMISSIONS = "AUTHENTICATED_WITHOUT_PERMISSIONS",
|
||||
POST_AUTH_REDIRECT = "POST_AUTH_REDIRECT"
|
||||
}
|
||||
```
|
||||
|
||||
### State Transition Table
|
||||
|
||||
| Current State | Session | ReturnTo | Next State | Action |
|
||||
|---------------|---------|----------|------------|--------|
|
||||
| INITIAL | null | any | UNAUTHENTICATED | Show login form |
|
||||
| INITIAL | exists | '/dashboard' | AUTHENTICATED_WITH_PERMISSIONS | Redirect to dashboard |
|
||||
| INITIAL | exists | NOT '/dashboard' | AUTHENTICATED_WITHOUT_PERMISSIONS | Show permission error |
|
||||
| UNAUTHENTICATED | exists | any | POST_AUTH_REDIRECT | Redirect to returnTo |
|
||||
| AUTHENTICATED_WITHOUT_PERMISSIONS | exists | any | POST_AUTH_REDIRECT | Redirect to returnTo |
|
||||
|
||||
### Class-Based Controller
|
||||
|
||||
```typescript
|
||||
class LoginFlowController {
|
||||
// Immutable state
|
||||
private readonly session: AuthSessionDTO | null;
|
||||
private readonly returnTo: string;
|
||||
|
||||
// State machine
|
||||
private state: LoginState;
|
||||
|
||||
constructor(session: AuthSessionDTO | null, returnTo: string) {
|
||||
this.session = session;
|
||||
this.returnTo = returnTo;
|
||||
this.state = this.determineInitialState();
|
||||
}
|
||||
|
||||
private determineInitialState(): LoginState {
|
||||
if (!this.session) return LoginState.UNAUTHENTICATED;
|
||||
if (this.returnTo === '/dashboard') return LoginState.AUTHENTICATED_WITH_PERMISSIONS;
|
||||
return LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS;
|
||||
}
|
||||
|
||||
// Pure function - no side effects
|
||||
getState(): LoginState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
// Pure function - returns action, doesn't execute
|
||||
getNextAction(): LoginAction {
|
||||
switch (this.state) {
|
||||
case LoginState.UNAUTHENTICATED:
|
||||
return { type: 'SHOW_LOGIN_FORM' };
|
||||
case LoginState.AUTHENTICATED_WITH_PERMISSIONS:
|
||||
return { type: 'REDIRECT', path: '/dashboard' };
|
||||
case LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS:
|
||||
return { type: 'SHOW_PERMISSION_ERROR' };
|
||||
case LoginState.POST_AUTH_REDIRECT:
|
||||
return { type: 'REDIRECT', path: this.returnTo };
|
||||
}
|
||||
}
|
||||
|
||||
// Called after authentication
|
||||
transitionToPostAuth(): void {
|
||||
if (this.session) {
|
||||
this.state = LoginState.POST_AUTH_REDIRECT;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Predictable**: Same inputs always produce same outputs
|
||||
2. **Testable**: Can test each state transition independently
|
||||
3. **No Race Conditions**: State determined once at construction
|
||||
4. **Clear Intent**: Each state has a single purpose
|
||||
5. **Maintainable**: Easy to add new states or modify transitions
|
||||
|
||||
### Usage in Login Page
|
||||
|
||||
```typescript
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { session } = useAuth();
|
||||
|
||||
const returnTo = searchParams.get('returnTo') ?? '/dashboard';
|
||||
|
||||
// Create controller once
|
||||
const controller = useMemo(() =>
|
||||
new LoginFlowController(session, returnTo),
|
||||
[session, returnTo]
|
||||
);
|
||||
|
||||
// Get current state
|
||||
const state = controller.getState();
|
||||
const action = controller.getNextAction();
|
||||
|
||||
// Execute action (only once)
|
||||
useEffect(() => {
|
||||
if (action.type === 'REDIRECT') {
|
||||
router.replace(action.path);
|
||||
}
|
||||
}, [action, router]);
|
||||
|
||||
// Render based on state
|
||||
if (state === LoginState.UNAUTHENTICATED) {
|
||||
return <LoginForm />;
|
||||
}
|
||||
|
||||
if (state === LoginState.AUTHENTICATED_WITHOUT_PERMISSIONS) {
|
||||
return <PermissionError returnTo={returnTo} />;
|
||||
}
|
||||
|
||||
// Show loading while redirecting
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
```
|
||||
|
||||
This eliminates all the unpredictable behavior and makes the flow testable and maintainable.
|
||||
46
docs/architecture/website/VIEW_DATA.md
Normal file
46
docs/architecture/website/VIEW_DATA.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# ViewData (Website Templates)
|
||||
|
||||
ViewData is the **only** allowed input type for Templates in `apps/website`.
|
||||
|
||||
## 1) Definition
|
||||
|
||||
ViewData is a JSON-serializable, template-ready data structure:
|
||||
|
||||
- primitives (strings/numbers/booleans)
|
||||
- arrays and plain objects
|
||||
- `null` for missing values
|
||||
|
||||
## 2) What ViewData is NOT
|
||||
|
||||
ViewData is not:
|
||||
|
||||
- a Page DTO (raw transport)
|
||||
- a ViewModel (client-only class)
|
||||
- a Display Object instance
|
||||
|
||||
## 3) Construction rules
|
||||
|
||||
ViewData MUST be created in client code:
|
||||
|
||||
1) Initial SSR-safe render: `ViewData = fromDTO(PageDTO)`
|
||||
2) Post-hydration render: `ViewData = fromViewModel(ViewModel)`
|
||||
|
||||
Templates MUST NOT compute derived values.
|
||||
|
||||
## 4) Determinism rules
|
||||
|
||||
Any formatting used to produce ViewData MUST be deterministic.
|
||||
|
||||
Forbidden anywhere in formatting code paths:
|
||||
|
||||
- `Intl.*`
|
||||
- `Date.toLocaleString()` / `Date.toLocaleDateString()` / `Date.toLocaleTimeString()`
|
||||
|
||||
Reason: SSR and browser outputs can differ.
|
||||
|
||||
## 5) Relationship to Display Objects
|
||||
|
||||
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.
|
||||
|
||||
97
docs/architecture/website/VIEW_MODELS.md
Normal file
97
docs/architecture/website/VIEW_MODELS.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# View Models
|
||||
|
||||
## Definition
|
||||
|
||||
A **View Model** represents a **fully prepared UI state**.
|
||||
|
||||
It answers the question:
|
||||
|
||||
> “What does the UI need in order to render this screen without thinking?”
|
||||
|
||||
View Models are **UI-owned** classes.
|
||||
They do not represent business truth and do not enforce domain rules.
|
||||
|
||||
---
|
||||
|
||||
## Responsibilities
|
||||
|
||||
A View Model MAY:
|
||||
|
||||
- accept an API DTO as input
|
||||
- derive UI-specific fields
|
||||
- combine or reshape data for rendering
|
||||
- perform formatting (dates, numbers, labels)
|
||||
- handle localization and presentation logic
|
||||
- use Display Objects for reusable UI concerns
|
||||
|
||||
In the website SSR/RSC architecture, View Models MAY compute view-only derived values, but MUST NOT be the type passed into Templates.
|
||||
|
||||
A View Model MUST:
|
||||
|
||||
- be fully usable by the UI without further computation
|
||||
- expose only data and UI-oriented helpers
|
||||
- be created in a consistent, explicit way
|
||||
|
||||
---
|
||||
|
||||
## Restrictions
|
||||
|
||||
A View Model MUST NOT:
|
||||
|
||||
- contain business logic
|
||||
- validate domain rules
|
||||
- enforce permissions or authorization
|
||||
- contain domain entities or value objects
|
||||
- perform side effects
|
||||
- be sent back to the server
|
||||
|
||||
If a View Model decides whether something is *allowed* or *correct*,
|
||||
that logic belongs in the Core, not here.
|
||||
|
||||
---
|
||||
|
||||
## Ownership & Placement
|
||||
|
||||
- View Models belong to the **frontend**
|
||||
- They live close to the UI, not in shared or core layers
|
||||
- They are not shared with the backend
|
||||
|
||||
---
|
||||
|
||||
## Creation Rules
|
||||
|
||||
This repository distinguishes **Page DTO**, **ViewModel**, and **ViewData**:
|
||||
|
||||
- Page DTO: server-to-client payload (JSON-serializable)
|
||||
- ViewModel: client-only class (never serialized)
|
||||
- ViewData: template input (JSON-serializable)
|
||||
|
||||
Rules (website):
|
||||
|
||||
1) View Models are created in client code only.
|
||||
2) View Models are created from Page DTOs.
|
||||
3) Templates MUST NOT accept View Models; Templates accept ViewData only.
|
||||
4) View Models MUST compose Display Objects and produce ViewData (primitive outputs only).
|
||||
|
||||
Authoritative reference: [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1).
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
View Models SHOULD be tested when they contain:
|
||||
|
||||
- formatting logic
|
||||
- localization behavior
|
||||
- non-trivial derived fields
|
||||
|
||||
View Models do NOT need tests if they only expose data without logic.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- View Models describe **UI state**
|
||||
- They are **presentation-focused**, not business-focused
|
||||
- They reduce complexity in components
|
||||
- They form a stable contract for the UI
|
||||
30
docs/architecture/website/WEBSITE_DI_RULES.md
Normal file
30
docs/architecture/website/WEBSITE_DI_RULES.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Website DI Rules (Inversify)
|
||||
|
||||
This repo uses Inversify DI under [apps/website/lib/di](apps/website/lib/di/index.ts:1).
|
||||
|
||||
## 1) Non-negotiable safety rule
|
||||
|
||||
No stateful service instances may be shared across requests.
|
||||
|
||||
Reason: Next.js server execution is concurrent; shared state causes cross-request leakage.
|
||||
|
||||
## 2) Rules by module type
|
||||
|
||||
### 2.1 `page.tsx` (server)
|
||||
|
||||
- MUST NOT access the DI container directly.
|
||||
- MUST call a PageQuery only.
|
||||
|
||||
### 2.2 Page Queries (server)
|
||||
|
||||
- SHOULD prefer explicit construction (manual wiring).
|
||||
- MAY use DI only if all resolved services are stateless and safe for concurrent requests.
|
||||
|
||||
### 2.3 Client modules
|
||||
|
||||
- MAY use DI via `ContainerProvider` and hooks (example: `useInject`).
|
||||
|
||||
## 3) Container singleton warning
|
||||
|
||||
[`ContainerManager`](apps/website/lib/di/container.ts:61) holds a singleton container. Treat it as **unsafe for server request scope** unless proven otherwise.
|
||||
|
||||
30
docs/architecture/website/WEBSITE_GUARDRAILS.md
Normal file
30
docs/architecture/website/WEBSITE_GUARDRAILS.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Website Guardrails (Mandatory)
|
||||
|
||||
This document defines architecture guardrails that must be enforced via tests + ESLint.
|
||||
|
||||
## 1) RSC boundary guardrails
|
||||
|
||||
Fail CI if any `apps/website/app/**/page.tsx`:
|
||||
|
||||
- imports from `apps/website/lib/view-models/*`
|
||||
- calls `Intl.*` or `toLocale*`
|
||||
- performs sorting/filtering (`sort`, `filter`, `reduce`) beyond trivial null checks
|
||||
|
||||
## 2) Template purity guardrails
|
||||
|
||||
Fail CI if any `apps/website/templates/**`:
|
||||
|
||||
- imports from `apps/website/lib/view-models/*`
|
||||
- imports from `apps/website/lib/display-objects/*`
|
||||
- calls `Intl.*` or `toLocale*`
|
||||
|
||||
Templates accept ViewData only.
|
||||
|
||||
## 3) Display Object guardrails
|
||||
|
||||
Fail CI if any `apps/website/lib/display-objects/**`:
|
||||
|
||||
- calls `Intl.*` or `toLocale*`
|
||||
|
||||
Display Objects must be deterministic.
|
||||
|
||||
39
docs/architecture/website/WEBSITE_PAGE_QUERIES.md
Normal file
39
docs/architecture/website/WEBSITE_PAGE_QUERIES.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Website Page Queries (Server)
|
||||
|
||||
This document defines the only allowed server-side data fetching shape for `apps/website` routes.
|
||||
|
||||
## 1) Purpose
|
||||
|
||||
Page Queries are server-side composition classes that:
|
||||
|
||||
- call services that call `apps/api`
|
||||
- assemble a Page DTO
|
||||
- return an explicit result describing route outcome
|
||||
|
||||
They do not implement business rules.
|
||||
|
||||
## 2) Result type (no null)
|
||||
|
||||
Page Queries MUST return a discriminated union (`PageQueryResult`):
|
||||
|
||||
- `ok` with `{ dto }`
|
||||
- `notFound`
|
||||
- `redirect` with `{ to }`
|
||||
- `error` with `{ errorId }`
|
||||
|
||||
Pages MUST switch on this result and call:
|
||||
|
||||
- `notFound()` for `notFound`
|
||||
- `redirect()` for `redirect`
|
||||
|
||||
## 3) Forbidden responsibilities
|
||||
|
||||
Page Queries MUST NOT:
|
||||
|
||||
- format values for display
|
||||
- sort/filter (canonical or view-only)
|
||||
- instantiate ViewModels
|
||||
- instantiate Display Objects
|
||||
|
||||
If sorting/filtering is needed, it MUST be added to `apps/api`.
|
||||
|
||||
60
docs/architecture/website/WEBSITE_RSC_PRESENTATION.md
Normal file
60
docs/architecture/website/WEBSITE_RSC_PRESENTATION.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Website RSC Presentation Architecture (Strict)
|
||||
|
||||
This document defines the only allowed presentation architecture for `apps/website` (Next.js App Router).
|
||||
|
||||
It is **website-only** and does not change `apps/api` or `core` architecture.
|
||||
|
||||
## 1) Core rule: API owns business truth
|
||||
|
||||
- `apps/api` is the only source of truth for business rules and canonical filtering/sorting.
|
||||
- `apps/website` is presentation infrastructure: composition, routing, caching, and rendering.
|
||||
|
||||
## 2) The three website presentation data types
|
||||
|
||||
### 2.1 Page DTO
|
||||
|
||||
**Purpose:** server-to-client payload.
|
||||
|
||||
**Rules:**
|
||||
|
||||
- JSON-serializable only.
|
||||
- Contains raw values only (ISO date strings, numbers, codes).
|
||||
- MUST NOT contain class instances.
|
||||
|
||||
### 2.2 ViewModel
|
||||
|
||||
**Purpose:** client-only presentation model.
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Class-based.
|
||||
- Instantiated only in `'use client'` modules.
|
||||
- Composes Display Objects.
|
||||
- NEVER passed into Templates.
|
||||
|
||||
### 2.3 ViewData
|
||||
|
||||
**Purpose:** Template input.
|
||||
|
||||
**Rules:**
|
||||
|
||||
- JSON-serializable only.
|
||||
- Contains only values ready to render (mostly strings/numbers).
|
||||
- Built from Page DTO (initial render) and from ViewModel (post-hydration).
|
||||
|
||||
## 3) Required per-route structure
|
||||
|
||||
Every route MUST follow:
|
||||
|
||||
1) `page.tsx` (server): calls a PageQuery and passes Page DTO
|
||||
2) `*PageClient.tsx` (client): builds ViewData and renders Template
|
||||
3) `*Template.tsx` (pure UI): renders ViewData only
|
||||
|
||||
## 4) Authoritative specification
|
||||
|
||||
This document is an entry point only.
|
||||
|
||||
The authoritative, test-enforced spec lives at:
|
||||
|
||||
- [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1)
|
||||
|
||||
Reference in New Issue
Block a user