diff --git a/docs/CONTRACT_TESTING_QUICKSTART.md b/docs/CONTRACT_TESTING_QUICKSTART.md deleted file mode 100644 index a03224bf4..000000000 --- a/docs/CONTRACT_TESTING_QUICKSTART.md +++ /dev/null @@ -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
{race.name}
; - } - ``` - -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` \ No newline at end of file diff --git a/docs/architecture/website/CLIENT_STATE.md b/docs/architecture/website/CLIENT_STATE.md new file mode 100644 index 000000000..5b2ccbc38 --- /dev/null +++ b/docs/architecture/website/CLIENT_STATE.md @@ -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/**` + diff --git a/docs/architecture/website/DISPLAY_OBJECTS.md b/docs/architecture/website/DISPLAY_OBJECTS.md index 65b0b1a87..e470c43d1 100644 --- a/docs/architecture/website/DISPLAY_OBJECTS.md +++ b/docs/architecture/website/DISPLAY_OBJECTS.md @@ -25,7 +25,7 @@ They exist to avoid duplicating presentation logic across View Models. A Display Object MAY: - 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 - encapsulate UI display conventions - 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. +## 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, it does not belong in a Display Object. diff --git a/docs/architecture/website/FORM_SUBMISSION.md b/docs/architecture/website/FORM_SUBMISSION.md index 6bd3d4694..d37ce2f3d 100644 --- a/docs/architecture/website/FORM_SUBMISSION.md +++ b/docs/architecture/website/FORM_SUBMISSION.md @@ -5,6 +5,8 @@ It applies to all write operations (create, update, delete). There are no exceptions. +Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1). + βΈ» Core Principle @@ -13,6 +15,21 @@ Read and Write paths are different. 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 @@ -153,4 +170,4 @@ Summary β€’ Read Flow: DTO β†’ ViewModel β†’ UI β€’ Write Flow: UI β†’ Command DTO β†’ Core -What is shown is never sent back. \ No newline at end of file +What is shown is never sent back. diff --git a/docs/architecture/website/PRESENTERS.md b/docs/architecture/website/PRESENTERS.md new file mode 100644 index 000000000..cf52d6443 --- /dev/null +++ b/docs/architecture/website/PRESENTERS.md @@ -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. diff --git a/docs/architecture/website/VIEW_DATA.md b/docs/architecture/website/VIEW_DATA.md index 00281c744..f08e27549 100644 --- a/docs/architecture/website/VIEW_DATA.md +++ b/docs/architecture/website/VIEW_DATA.md @@ -2,6 +2,8 @@ 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 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: -- a Page DTO (raw transport) +- an API Transport DTO (raw transport) - a ViewModel (client-only class) - a Display Object instance @@ -25,8 +27,13 @@ ViewData MUST be created in client code: 1) Initial SSR-safe render: `ViewData = fromDTO(PageDTO)` 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. +Presenters MUST NOT call the API. + ## 4) Determinism rules 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. +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 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. - diff --git a/docs/architecture/website/WEBSITE_CONTRACT.md b/docs/architecture/website/WEBSITE_CONTRACT.md new file mode 100644 index 000000000..d67af84fa --- /dev/null +++ b/docs/architecture/website/WEBSITE_CONTRACT.md @@ -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. diff --git a/docs/architecture/website/WEBSITE_DI_RULES.md b/docs/architecture/website/WEBSITE_DI_RULES.md index 849e6bb5a..5ecfeaecd 100644 --- a/docs/architecture/website/WEBSITE_DI_RULES.md +++ b/docs/architecture/website/WEBSITE_DI_RULES.md @@ -2,6 +2,8 @@ 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 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). - 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 - 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. +Strict rule: + +- [`ContainerManager.getContainer()`](apps/website/lib/di/container.ts:74) is client-only. + diff --git a/docs/architecture/website/WEBSITE_GUARDRAILS.md b/docs/architecture/website/WEBSITE_GUARDRAILS.md index 126bf25b5..9c512cb86 100644 --- a/docs/architecture/website/WEBSITE_GUARDRAILS.md +++ b/docs/architecture/website/WEBSITE_GUARDRAILS.md @@ -2,11 +2,14 @@ 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 Fail CI if any `apps/website/app/**/page.tsx`: - imports from `apps/website/lib/view-models/*` +- imports from Presenter code (presenters live colocated with ViewModels) - calls `Intl.*` or `toLocale*` - 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/**`: - imports from `apps/website/lib/view-models/*` +- imports from presenter code (presenters live colocated with ViewModels) - imports from `apps/website/lib/display-objects/*` - calls `Intl.*` or `toLocale*` @@ -27,4 +31,3 @@ Fail CI if any `apps/website/lib/display-objects/**`: - calls `Intl.*` or `toLocale*` Display Objects must be deterministic. - diff --git a/docs/architecture/website/WEBSITE_RSC_PRESENTATION.md b/docs/architecture/website/WEBSITE_RSC_PRESENTATION.md index 0cd661ec6..3153b85d0 100644 --- a/docs/architecture/website/WEBSITE_RSC_PRESENTATION.md +++ b/docs/architecture/website/WEBSITE_RSC_PRESENTATION.md @@ -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. +Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1). + ## 1) Core rule: API owns business truth - `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). - 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 Every route MUST follow: @@ -50,6 +55,9 @@ Every route MUST follow: 2) `*PageClient.tsx` (client): builds ViewData and renders Template 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 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) +Final contract: + +- [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1)