# Racing TypeORM Adapter Clean Architecture Audit + Strict Refactor Guide

Scope focus: all persistence adapter code under [`adapters/racing/persistence/typeorm/`](adapters/racing/persistence/typeorm:1), especially mappers (incl. JSON mappers) and repositories, plus the ORM entities and adapter-scoped errors they depend on.

This guide is intentionally strict and implementation-ready.

---

## Governing constraints (authoritative)

- **Strict inward dependencies**: [`Only dependency-inward is allowed.`](docs/architecture/DATA_FLOW.md:24)
- **Domain purity / no IO in domain objects**: [`Entities MUST NOT perform IO.`](docs/architecture/DOMAIN_OBJECTS.md:49)
- **Persisted objects must rehydrate, not create**: [`Existing entities are reconstructed via rehydrate().`](docs/architecture/DOMAIN_OBJECTS.md:57)
- **Adapters translate only (no orchestration/business logic)**: [`Adapters translate.`](docs/architecture/DATA_FLOW.md:437)
- **Persistence entity placement**: ORM entities live in adapters, not domain: [`entities/ ORM-Entities (nicht Domain!)`](docs/architecture/FILE_STRUCTURE.md:47)

---

## Adapter surface inventory (audited)

### Mappers
- [`LeagueOrmMapper`](adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper.ts:30)
- [`RaceOrmMapper`](adapters/racing/persistence/typeorm/mappers/RaceOrmMapper.ts:5)
- [`SeasonOrmMapper`](adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper.ts:4)
- [`LeagueScoringConfigOrmMapper`](adapters/racing/persistence/typeorm/mappers/LeagueScoringConfigOrmMapper.ts:42)
- [`ChampionshipConfigJsonMapper`](adapters/racing/persistence/typeorm/mappers/ChampionshipConfigJsonMapper.ts:17)
- [`PointsTableJsonMapper`](adapters/racing/persistence/typeorm/mappers/PointsTableJsonMapper.ts:7)

### Repositories
- [`TypeOrmLeagueRepository`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueRepository.ts:9)
- [`TypeOrmRaceRepository`](adapters/racing/persistence/typeorm/repositories/TypeOrmRaceRepository.ts:9)
- [`TypeOrmSeasonRepository`](adapters/racing/persistence/typeorm/repositories/TypeOrmSeasonRepository.ts:9)
- [`TypeOrmLeagueScoringConfigRepository`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueScoringConfigRepository.ts:9)

### ORM entities
- [`LeagueOrmEntity`](adapters/racing/persistence/typeorm/entities/LeagueOrmEntity.ts:4)
- [`RaceOrmEntity`](adapters/racing/persistence/typeorm/entities/RaceOrmEntity.ts:4)
- [`SeasonOrmEntity`](adapters/racing/persistence/typeorm/entities/SeasonOrmEntity.ts:4)
- [`LeagueScoringConfigOrmEntity`](adapters/racing/persistence/typeorm/entities/LeagueScoringConfigOrmEntity.ts:6)

### Adapter-scoped errors
- [`InvalidLeagueScoringConfigChampionshipsSchemaError`](adapters/racing/persistence/typeorm/errors/InvalidLeagueScoringConfigChampionshipsSchemaError.ts:1)

---

## Concrete violations (file + function, why, severity)

Severity rubric:
- **Blocker**: violates non-negotiable constraints; can cause domain invariants to run on persisted state, or leaks construction/orchestration into adapters.
- **Follow-up**: does not strictly violate constraints, but is unsafe/unclear and should be corrected to align with the canonical strict pattern defined below.

### 1) Rehydration violations (calling `create()` when loading from DB) — **Blocker**

- [`LeagueOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper.ts:46)
  - **Why**: Uses [`League.create()`](core/racing/domain/entities/League.ts:132) for persisted reconstruction.
  - **Rule violated**: persisted objects must reconstruct via [`rehydrate()`](docs/architecture/DOMAIN_OBJECTS.md:57) semantics (new vs existing).
  - **Impact**: running creation-time defaulting + validation on persisted state can mutate meaning (e.g., defaults merged in [`League.create()`](core/racing/domain/entities/League.ts:132)) and can throw domain validation errors due to persistence schema drift.

- [`RaceOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/RaceOrmMapper.ts:23)
  - **Why**: Uses [`Race.create()`](core/racing/domain/entities/Race.ts:81) for persisted reconstruction.
  - **Rule violated**: persisted objects must reconstruct via [`rehydrate()`](docs/architecture/DOMAIN_OBJECTS.md:57).
  - **Impact**: DB rows become subject to “new entity” validations; adapter loses ability to separate “invalid persisted schema” (adapter concern) from “invalid new command” (domain concern).

- [`SeasonOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper.ts:26)
  - **Why**: Uses [`Season.create()`](core/racing/domain/entities/season/Season.ts:70) for persisted reconstruction.
  - **Rule violated**: persisted objects must reconstruct via [`rehydrate()`](docs/architecture/DOMAIN_OBJECTS.md:57).
  - **Impact**: same as above, plus schedule/scoring/drop/stewarding props are passed as `any`, making persisted-state validation unpredictable.

- Positive control (already compliant): [`LeagueScoringConfigOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/LeagueScoringConfigOrmMapper.ts:54)
  - Uses [`LeagueScoringConfig.rehydrate()`](core/racing/domain/entities/LeagueScoringConfig.ts:63) and validates schema before converting.
  - This is the baseline pattern to replicate.

### 2) Adapter “translation only” violations (construction / orchestration inside repositories) — **Blocker**

- [`TypeOrmLeagueRepository.constructor()`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueRepository.ts:10)
  - **Why**: Default-constructs a mapper via `new` ([`new LeagueOrmMapper()`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueRepository.ts:12)).
  - **Rule violated**: adapters must “translate only” ([`Adapters translate.`](docs/architecture/DATA_FLOW.md:437)); object graph construction belongs in the composition root (Nest module).
  - **Impact**: makes DI inconsistent, harder to test, and encourages mapper graphs to be built ad-hoc in infrastructure code rather than composed centrally.

- [`TypeOrmRaceRepository.constructor()`](adapters/racing/persistence/typeorm/repositories/TypeOrmRaceRepository.ts:10)
  - **Why**: Default-constructs mapper via [`new RaceOrmMapper()`](adapters/racing/persistence/typeorm/repositories/TypeOrmRaceRepository.ts:12).
  - **Rule violated**: [`Adapters translate.`](docs/architecture/DATA_FLOW.md:437)
  - **Impact**: same.

- [`TypeOrmSeasonRepository.constructor()`](adapters/racing/persistence/typeorm/repositories/TypeOrmSeasonRepository.ts:10)
  - **Why**: Default-constructs mapper via [`new SeasonOrmMapper()`](adapters/racing/persistence/typeorm/repositories/TypeOrmSeasonRepository.ts:12).
  - **Rule violated**: [`Adapters translate.`](docs/architecture/DATA_FLOW.md:437)

- Positive control (already aligned): [`TypeOrmLeagueScoringConfigRepository.constructor()`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueScoringConfigRepository.ts:10)
  - Requires mapper injection (no internal construction), and is enforced by [`TypeOrmLeagueScoringConfigRepository.test.ts`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueScoringConfigRepository.test.ts:17).

### 3) Persistence schema typing issues that currently force `unknown`/`any` translation — **Follow-up (but required by the canonical strict pattern)**

These aren’t explicitly spelled out in the architecture docs, but they directly undermine “adapters translate only” by making translation ambiguous and unsafe. They also block strict `rehydrate()` mapping because you can’t validate/interpret persisted JSON precisely.

- [`LeagueOrmEntity.settings`](adapters/racing/persistence/typeorm/entities/LeagueOrmEntity.ts:17)
  - Current: `Record<string, unknown>`
  - Problem: forces coercion and casts in [`LeagueOrmMapper.toOrmEntity()`](adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper.ts:31) and [`LeagueOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper.ts:46).

- [`SeasonOrmEntity.schedule`](adapters/racing/persistence/typeorm/entities/SeasonOrmEntity.ts:32), [`SeasonOrmEntity.scoringConfig`](adapters/racing/persistence/typeorm/entities/SeasonOrmEntity.ts:38), [`SeasonOrmEntity.dropPolicy`](adapters/racing/persistence/typeorm/entities/SeasonOrmEntity.ts:41), [`SeasonOrmEntity.stewardingConfig`](adapters/racing/persistence/typeorm/entities/SeasonOrmEntity.ts:44)
  - Current: `Record<string, unknown> | null`
  - Problem: propagates into pervasive `as any` in [`SeasonOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper.ts:26).

- [`RaceOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/RaceOrmMapper.ts:23) and [`SeasonOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper.ts:26)
  - Current: `as any` casts for status/sessionType/schedule/etc.
  - Problem: translation is not explicit/verified; makes persisted schema errors show up as domain behavior or runtime surprises.

### 4) Persistence boundary error mapping gaps — **Blocker**

- [`LeagueOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper.ts:46), [`RaceOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/RaceOrmMapper.ts:23), [`SeasonOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper.ts:26)
  - **Why**: because these call `create()` and use unsafe casts, the adapter does not reliably distinguish:
    - invalid persisted schema (adapter concern) vs
    - invalid incoming command/request (domain concern).
  - **Rule violated (intent)**: separation of roles per [`Adapters translate.`](docs/architecture/DATA_FLOW.md:437) and persisted rehydration rule ([`rehydrate()`](docs/architecture/DOMAIN_OBJECTS.md:57)).
  - **Impact**: domain errors (e.g. [`RacingDomainValidationError`](core/racing/domain/entities/League.ts:148)) can leak due to persistence drift, making debugging and recovery much harder.

---

## Canonical strict pattern for this repo (target state)

This is the “golden path” all Racing TypeORM adapters should follow.

### A) ORM entity shape rules (including JSON columns)

- ORM entities are allowed in adapters per [`entities/ ORM-Entities (nicht Domain!)`](docs/architecture/FILE_STRUCTURE.md:47).
- JSON columns must be **typed to a concrete serialized shape** (no `unknown`, no `Record<string, unknown>`).
  - Example target types:
    - `LeagueOrmEntity.settings: SerializedLeagueSettings`
    - `SeasonOrmEntity.schedule: SerializedSeasonSchedule | null`
    - `SeasonOrmEntity.scoringConfig: SerializedSeasonScoringConfig | null`
- Serialized types must be:
  - JSON-safe (no `Date`, `Map`, class instances)
  - versionable (allow future `schemaVersion?: number`)
  - strict enough to validate at runtime
- Use `null` for absent optional columns (as already done in [`RaceOrmEntity.trackId`](adapters/racing/persistence/typeorm/entities/RaceOrmEntity.ts:17)).

### B) Mapper rules (pure translation, no side effects)

- Mappers are pure, deterministic translators:
  - `domain -> orm` and `orm -> domain`
  - no IO, no logging, no Date.now(), no random ids
- **No `as any`** in mapping. If types don’t line up, fix the schema types or add a safe interpreter.
- **No `create()` on load**. `orm -> domain` must call `rehydrate()` semantics for entities ([`rehydrate()` rule](docs/architecture/DOMAIN_OBJECTS.md:57)).
- On `orm -> domain`:
  - validate persisted schema and throw an **adapter-scoped persistence schema error**, not a domain validation error.
  - treat invalid persisted schema as infrastructure failure (data corruption/migration mismatch).
- On `domain -> orm`:
  - serialize domain objects via explicit “serialize” helpers (no `as unknown as Record<...>`).
- JSON mappers:
  - must be pure
  - must return serialized DTO-like shapes, not domain objects except as output of `fromJson`
  - should avoid calling value-object constructors directly unless the VO explicitly treats constructor as a safe “rehydrate”; otherwise introduce `fromJson()`/`rehydrate()` for VOs.

### C) Repository rules (IO + mapper only; composition root in Nest module)

- Repositories implement core ports and do:
  - DB IO (TypeORM queries)
  - mapping via injected mappers
- Repositories must not:
  - construct mappers internally (`new` in constructor defaults)
  - embed business logic/orchestration (that belongs to application services / use cases per [`Use Cases decide... Adapters translate.`](docs/architecture/DATA_FLOW.md:437))
- Construction belongs in the Nest composition root:
  - e.g. [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:57) should provide mapper instances and inject them into repositories.

### D) Error handling rules (adapter-scoped errors vs domain errors)

- Domain errors (e.g. [`RacingDomainValidationError`](core/racing/domain/entities/League.ts:148)) are for rejecting invalid **commands/new state transitions**.
- Persistence adapters must throw adapter-scoped errors for:
  - invalid persisted JSON schema
  - impossible enum values/statuses stored in DB
  - missing required persisted columns
- Pattern baseline:
  - follow [`InvalidLeagueScoringConfigChampionshipsSchemaError`](adapters/racing/persistence/typeorm/errors/InvalidLeagueScoringConfigChampionshipsSchemaError.ts:1) (adapter-owned, descriptive name, extends `Error`).
- Repositories generally should not catch/translate DB errors unless you have a stable policy (e.g. unique violations) — keep this explicit and adapter-scoped if introduced.

---

## Controlled refactor plan (2–3 slices)

Each slice is designed to be reviewable and to keep the system runnable.

### Slice 1 — Mappers: `rehydrate()` + typed JSON schemas (DB-free unit tests)

**Goal**
- Make all `orm -> domain` mapping use rehydration semantics and strict, typed persisted schemas.
- Remove `as any` from mapper paths by fixing schema types and adding validators.

**Files to touch (exact)**
- Mapper implementations:
  - [`LeagueOrmMapper`](adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper.ts:30)
  - [`RaceOrmMapper`](adapters/racing/persistence/typeorm/mappers/RaceOrmMapper.ts:5)
  - [`SeasonOrmMapper`](adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper.ts:4)
  - (keep as reference) [`LeagueScoringConfigOrmMapper`](adapters/racing/persistence/typeorm/mappers/LeagueScoringConfigOrmMapper.ts:42)
  - JSON mappers as needed:
    - [`PointsTableJsonMapper`](adapters/racing/persistence/typeorm/mappers/PointsTableJsonMapper.ts:7)
    - [`ChampionshipConfigJsonMapper`](adapters/racing/persistence/typeorm/mappers/ChampionshipConfigJsonMapper.ts:17)
- ORM entities (type JSON columns to serialized types):
  - [`LeagueOrmEntity`](adapters/racing/persistence/typeorm/entities/LeagueOrmEntity.ts:4)
  - [`SeasonOrmEntity`](adapters/racing/persistence/typeorm/entities/SeasonOrmEntity.ts:4)
- Add adapter-scoped schema error types (new files under the existing folder):
  - create new errors in [`adapters/racing/persistence/typeorm/errors/`](adapters/racing/persistence/typeorm/errors/InvalidLeagueScoringConfigChampionshipsSchemaError.ts:1)
- Core changes required to satisfy rehydration rule (yes, this reaches into core because the adapter cannot comply otherwise):
  - add `static rehydrate(...)` to:
    - [`League`](core/racing/domain/entities/League.ts:93)
    - [`Race`](core/racing/domain/entities/Race.ts:18)
    - [`Season`](core/racing/domain/entities/season/Season.ts:14)

**Acceptance tests (DB-free)**
- Add mapper unit tests in the same pattern as [`LeagueScoringConfigOrmMapper.test.ts`](adapters/racing/persistence/typeorm/mappers/LeagueScoringConfigOrmMapper.test.ts:10):
  - new: `LeagueOrmMapper.test.ts` verifies [`LeagueOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper.ts:46) calls `rehydrate()` and does not call [`League.create()`](core/racing/domain/entities/League.ts:132).
  - new: `RaceOrmMapper.test.ts` verifies [`RaceOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/RaceOrmMapper.ts:23) uses `rehydrate()` and never calls [`Race.create()`](core/racing/domain/entities/Race.ts:81).
  - new: `SeasonOrmMapper.test.ts` verifies [`SeasonOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper.ts:26) uses `rehydrate()` and never calls [`Season.create()`](core/racing/domain/entities/season/Season.ts:70).
- Add schema validation tests:
  - invalid JSON column shapes throw adapter error types (similar to [`InvalidLeagueScoringConfigChampionshipsSchemaError`](adapters/racing/persistence/typeorm/errors/InvalidLeagueScoringConfigChampionshipsSchemaError.ts:1)), not domain validation errors.

**Definition of done**
- No `create()` calls in any `toDomain()` for persisted entities.
- No `as any` in mapper implementations.
- JSON columns are typed to explicit `Serialized*` types, and validated on load.

---

### Slice 2 — Repository wiring cleanup (DI, no `new` inside repos)

**Goal**
- Repositories remain IO + mapper only.
- Mapper graphs are constructed in the Nest module composition root.

**Files to touch (exact)**
- Repositories:
  - [`TypeOrmLeagueRepository`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueRepository.ts:9)
  - [`TypeOrmRaceRepository`](adapters/racing/persistence/typeorm/repositories/TypeOrmRaceRepository.ts:9)
  - [`TypeOrmSeasonRepository`](adapters/racing/persistence/typeorm/repositories/TypeOrmSeasonRepository.ts:9)
  - (already OK, keep consistent) [`TypeOrmLeagueScoringConfigRepository`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueScoringConfigRepository.ts:9)
- Composition root wiring:
  - [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:57)
- Integration tests that new repos directly (update constructor signatures):
  - [`PostgresLeagueScheduleRepositorySlice.int.test.ts`](apps/api/src/persistence/postgres/typeorm/PostgresLeagueScheduleRepositorySlice.int.test.ts:55)
- Mapper tests that assume default constructors (update as needed):
  - [`RacingOrmMappers.test.ts`](apps/api/src/persistence/postgres/typeorm/RacingOrmMappers.test.ts:20)

**Acceptance tests**
- Add repository constructor tests mirroring the existing pattern in [`TypeOrmLeagueScoringConfigRepository.test.ts`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueScoringConfigRepository.test.ts:17):
  - new: `TypeOrmLeagueRepository.test.ts` asserts no internal `new LeagueOrmMapper()`.
  - new: `TypeOrmRaceRepository.test.ts` asserts no internal `new RaceOrmMapper()`.
  - new: `TypeOrmSeasonRepository.test.ts` asserts no internal `new SeasonOrmMapper()`.
- `tsc` should enforce injection by making mapper a required constructor param (strongest guard).

**Definition of done**
- No repository has a default `new Mapper()` in constructor params.
- Nest module provides mapper instances and injects them into repositories.

---

### Slice 3 (optional) — Postgres integration tests + minimal vertical verification

**Goal**
- Verify the new strict schemas + rehydrate semantics survive real persistence roundtrips.

**Files to touch (exact)**
- Existing integration test:
  - [`PostgresLeagueScheduleRepositorySlice.int.test.ts`](apps/api/src/persistence/postgres/typeorm/PostgresLeagueScheduleRepositorySlice.int.test.ts:31)
- Potentially add one focused mapper+repo roundtrip test per aggregate if gaps remain:
  - extend [`PostgresLeagueScheduleRepositorySlice.int.test.ts`](apps/api/src/persistence/postgres/typeorm/PostgresLeagueScheduleRepositorySlice.int.test.ts:55) rather than adding many files.

**Acceptance tests**
- With `DATABASE_URL` set, integration suite passes and persists/reads:
  - League with settings JSON
  - Season with nullable JSON configs
  - Race with status/sessionType mapping
  - LeagueScoringConfig with championships JSON (already covered)

---

## Notes on current wiring (for context)

- Nest composition root is already the correct place for construction:
  - [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:57)
- But it currently relies on repository default constructors for some mappers:
  - e.g. provides [`TypeOrmLeagueRepository`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueRepository.ts:9) via `new TypeOrmLeagueRepository(dataSource)` in [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:66)
- League scoring config is already composed explicitly (good reference):
  - constructs `PointsTableJsonMapper -> ChampionshipConfigJsonMapper -> LeagueScoringConfigOrmMapper` in [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:126)

---

## Top blockers (short list)

- Persisted entity mapping calls `create()` instead of `rehydrate()`:
  - [`LeagueOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper.ts:46)
  - [`RaceOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/RaceOrmMapper.ts:23)
  - [`SeasonOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper.ts:26)
- Repositories build mappers internally (construction not confined to composition root):
  - [`TypeOrmLeagueRepository.constructor()`](adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueRepository.ts:10)
  - [`TypeOrmRaceRepository.constructor()`](adapters/racing/persistence/typeorm/repositories/TypeOrmRaceRepository.ts:10)
  - [`TypeOrmSeasonRepository.constructor()`](adapters/racing/persistence/typeorm/repositories/TypeOrmSeasonRepository.ts:10)
- Untyped JSON columns and `as any` casts prevent strict translation and reliable schema error handling:
  - [`LeagueOrmEntity.settings`](adapters/racing/persistence/typeorm/entities/LeagueOrmEntity.ts:17)
  - [`SeasonOrmEntity.schedule`](adapters/racing/persistence/typeorm/entities/SeasonOrmEntity.ts:32)
  - [`RaceOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/RaceOrmMapper.ts:23)
  - [`SeasonOrmMapper.toDomain()`](adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper.ts:26)