fix issues in core

This commit is contained in:
2025-12-23 11:49:47 +01:00
parent 2854ae3c5c
commit 11492d1ff2
26 changed files with 257 additions and 53 deletions

View File

@@ -1 +1,2 @@
./html-dumps ./html-dumps
apps/companion

View File

@@ -1,6 +1,6 @@
import { AuthenticationState } from '../../domain/value-objects/AuthenticationState'; import { AuthenticationState } from '../../domain/value-objects/AuthenticationState';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import { SessionLifetime } from '../../domain/value-objects/SessionLifetime'; import { SessionLifetime } from '../../domain/value-objects/SessionLifetime';
import type { SessionValidatorPort } from '../ports/SessionValidatorPort'; import type { SessionValidatorPort } from '../ports/SessionValidatorPort';

View File

@@ -2,7 +2,7 @@ import { vi, Mock } from 'vitest';
import { ClearSessionUseCase } from './ClearSessionUseCase'; import { ClearSessionUseCase } from './ClearSessionUseCase';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
describe('ClearSessionUseCase', () => { describe('ClearSessionUseCase', () => {
let useCase: ClearSessionUseCase; let useCase: ClearSessionUseCase;

View File

@@ -1,4 +1,4 @@
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CompleteRaceCreationUseCase } from 'apps/companion/main/automation/application/use-cases/CompleteRaceCreationUseCase'; import { CompleteRaceCreationUseCase } from 'apps/companion/main/automation/application/use-cases/CompleteRaceCreationUseCase';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { RaceCreationResult } from 'apps/companion/main/automation/domain/value-objects/RaceCreationResult'; import { RaceCreationResult } from 'apps/companion/main/automation/domain/value-objects/RaceCreationResult';
import { CheckoutPrice } from 'apps/companion/main/automation/domain/value-objects/CheckoutPrice'; import { CheckoutPrice } from 'apps/companion/main/automation/domain/value-objects/CheckoutPrice';
import type { CheckoutServicePort } from 'apps/companion/main/automation/application/ports/CheckoutServicePort'; import type { CheckoutServicePort } from 'apps/companion/main/automation/application/ports/CheckoutServicePort';

View File

@@ -1,4 +1,4 @@
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { RaceCreationResult } from '../../domain/value-objects/RaceCreationResult'; import { RaceCreationResult } from '../../domain/value-objects/RaceCreationResult';
import type { CheckoutServicePort } from '../ports/CheckoutServicePort'; import type { CheckoutServicePort } from '../ports/CheckoutServicePort';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { ConfirmCheckoutUseCase } from 'apps/companion/main/automation/application/use-cases/ConfirmCheckoutUseCase'; import { ConfirmCheckoutUseCase } from 'apps/companion/main/automation/application/use-cases/ConfirmCheckoutUseCase';
import type { CheckoutServicePort } from 'apps/companion/main/automation/application/ports/CheckoutServicePort'; import type { CheckoutServicePort } from 'apps/companion/main/automation/application/ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from 'apps/companion/main/automation/application/ports/CheckoutConfirmationPort'; import type { CheckoutConfirmationPort } from 'apps/companion/main/automation/application/ports/CheckoutConfirmationPort';

View File

@@ -1,4 +1,4 @@
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import type { CheckoutServicePort } from '../ports/CheckoutServicePort'; import type { CheckoutServicePort } from '../ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from '../ports/CheckoutConfirmationPort'; import type { CheckoutConfirmationPort } from '../ports/CheckoutConfirmationPort';

View File

@@ -1,4 +1,4 @@
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, vi } from 'vitest';
import { VerifyAuthenticatedPageUseCase } from 'apps/companion/main/automation/application/use-cases/VerifyAuthenticatedPageUseCase'; import { VerifyAuthenticatedPageUseCase } from 'apps/companion/main/automation/application/use-cases/VerifyAuthenticatedPageUseCase';
import { AuthenticationServicePort as IAuthenticationService } from 'apps/companion/main/automation/application/ports/AuthenticationServicePort'; import { AuthenticationServicePort as IAuthenticationService } from 'apps/companion/main/automation/application/ports/AuthenticationServicePort';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { BrowserAuthenticationState } from 'apps/companion/main/automation/domain/value-objects/BrowserAuthenticationState'; import { BrowserAuthenticationState } from 'apps/companion/main/automation/domain/value-objects/BrowserAuthenticationState';
import { AuthenticationState } from 'apps/companion/main/automation/domain/value-objects/AuthenticationState'; import { AuthenticationState } from 'apps/companion/main/automation/domain/value-objects/AuthenticationState';

View File

@@ -1,5 +1,5 @@
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState'; import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';

View File

@@ -1,5 +1,5 @@
import type { IDomainValidationService } from '@core/shared/domain'; import type { IDomainValidationService } from '@core/shared/domain';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
/** /**
* Configuration for page state validation. * Configuration for page state validation.

View File

@@ -1,7 +1,7 @@
import { StepId } from '../value-objects/StepId'; import { StepId } from '../value-objects/StepId';
import type { IDomainValidationService } from '@core/shared/domain'; import type { IDomainValidationService } from '@core/shared/domain';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { SessionState } from '../value-objects/SessionState'; import { SessionState } from '../value-objects/SessionState';
export interface ValidationResult { export interface ValidationResult {

View File

@@ -1,4 +1,4 @@
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { CheckoutPrice } from '../../../domain/value-objects/CheckoutPrice'; import { CheckoutPrice } from '../../../domain/value-objects/CheckoutPrice';
import { CheckoutState } from '../../../domain/value-objects/CheckoutState'; import { CheckoutState } from '../../../domain/value-objects/CheckoutState';
import type { CheckoutInfoDTO } from '../../../application/dto/CheckoutInfoDTO'; import type { CheckoutInfoDTO } from '../../../application/dto/CheckoutInfoDTO';

View File

@@ -5,7 +5,7 @@ import type { AuthenticationServicePort } from '../../../../application/ports/Au
import type { LoggerPort } from '../../../../application/ports/LoggerPort'; import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState'; import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState'; import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession'; import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { SessionCookieStore } from './SessionCookieStore'; import { SessionCookieStore } from './SessionCookieStore';
import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow'; import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';

View File

@@ -3,7 +3,7 @@ import type { Page, Locator } from 'playwright';
import { AuthenticationState } from 'apps/companion/main/automation/domain/value-objects/AuthenticationState'; import { AuthenticationState } from 'apps/companion/main/automation/domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from 'apps/companion/main/automation/domain/value-objects/BrowserAuthenticationState'; import { BrowserAuthenticationState } from 'apps/companion/main/automation/domain/value-objects/BrowserAuthenticationState';
import type { LoggerPort as Logger } from 'apps/companion/main/automation/application/ports/LoggerPort'; import type { LoggerPort as Logger } from 'apps/companion/main/automation/application/ports/LoggerPort';
import type { Result } from '@gridpilot/shared/application/Result'; import type { Result } from '@core/shared/application/Result';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession'; import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { IPlaywrightAuthFlow } from './PlaywrightAuthFlow'; import { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
import { PlaywrightAuthSessionService } from './PlaywrightAuthSessionService'; import { PlaywrightAuthSessionService } from './PlaywrightAuthSessionService';

View File

@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
import * as path from 'path'; import * as path from 'path';
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState'; import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
import { CookieConfiguration } from '../../../../domain/value-objects/CookieConfiguration'; import { CookieConfiguration } from '../../../../domain/value-objects/CookieConfiguration';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { LoggerPort } from '../../../../application/ports/LoggerPort'; import type { LoggerPort } from '../../../../application/ports/LoggerPort';
interface Cookie { interface Cookie {

View File

@@ -16,7 +16,7 @@ import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO'
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO'; import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort'; import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
import type { LoggerPort } from '../../../../application/ports/LoggerPort'; import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { IRACING_SELECTORS, IRACING_URLS, IRACING_TIMEOUTS, BLOCKED_KEYWORDS } from '../dom/IRacingSelectors'; import { IRACING_SELECTORS, IRACING_URLS, IRACING_TIMEOUTS, BLOCKED_KEYWORDS } from '../dom/IRacingSelectors';
import { SessionCookieStore } from '../auth/SessionCookieStore'; import { SessionCookieStore } from '../auth/SessionCookieStore';
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession'; import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';

View File

@@ -18,7 +18,7 @@ import type {
PageStateValidation, PageStateValidation,
PageStateValidationResult, PageStateValidationResult,
} from '../../../../domain/services/PageStateValidator'; } from '../../../../domain/services/PageStateValidator';
import type { Result } from '@gridpilot/shared/application/Result'; import type { Result } from '@core/shared/application/Result';
interface WizardStepOrchestratorDeps { interface WizardStepOrchestratorDeps {
config: Required<PlaywrightConfig>; config: Required<PlaywrightConfig>;

View File

@@ -5,7 +5,7 @@
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { Result } from '@gridpilot/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { CheckoutConfirmationPort } from '../../../application/ports/CheckoutConfirmationPort'; import type { CheckoutConfirmationPort } from '../../../application/ports/CheckoutConfirmationPort';
import type { CheckoutConfirmationRequestDTO } from '../../../application/dto/CheckoutConfirmationRequestDTO'; import type { CheckoutConfirmationRequestDTO } from '../../../application/dto/CheckoutConfirmationRequestDTO';
import { CheckoutConfirmation } from '../../../domain/value-objects/CheckoutConfirmation'; import { CheckoutConfirmation } from '../../../domain/value-objects/CheckoutConfirmation';

View File

@@ -189,7 +189,7 @@ describe('DashboardOverviewUseCase', () => {
getMembership: async (leagueId: string, driverIdParam: string): Promise<LeagueMembership | null> => { getMembership: async (leagueId: string, driverIdParam: string): Promise<LeagueMembership | null> => {
return ( return (
memberships.find( memberships.find(
m => m.leagueId === leagueId && m.driverId === driverIdParam, m => m.leagueId.toString() === leagueId && m.driverId.toString() === driverIdParam,
) ?? null ) ?? null
); );
}, },

View File

@@ -1,20 +1,19 @@
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import type { FeedItem } from '@core/social/domain/types/FeedItem';
import { Driver } from '../../domain/entities/Driver';
import { League } from '../../domain/entities/League'; import { League } from '../../domain/entities/League';
import { Race } from '../../domain/entities/Race'; import { Race } from '../../domain/entities/Race';
import { Result as RaceResult } from '../../domain/entities/Result';
import { Driver } from '../../domain/entities/Driver';
import { Standing } from '../../domain/entities/Standing'; import { Standing } from '../../domain/entities/Standing';
import type { FeedItem } from '@core/social/domain/types/FeedItem'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
export interface DashboardOverviewInput { export interface DashboardOverviewInput {
driverId: string; driverId: string;
@@ -49,7 +48,7 @@ export interface DashboardRaceSummary {
export interface DashboardRecentRaceResultSummary { export interface DashboardRecentRaceResultSummary {
race: Race; race: Race;
league: League | null; league: League | null;
result: RaceResult; result: Result;
} }
export interface DashboardLeagueStandingSummary { export interface DashboardLeagueStandingSummary {
@@ -126,7 +125,7 @@ export class DashboardOverviewUseCase {
}); });
} }
const leagueMap = new Map(allLeagues.map(league => [league.id, league])); const leagueMap = new Map(allLeagues.map(league => [league.id.toString(), league]));
const driverStats = this.getDriverStats(driverId); const driverStats = this.getDriverStats(driverId);
@@ -142,7 +141,7 @@ export class DashboardOverviewUseCase {
}; };
const driverLeagues = await this.getDriverLeagues(allLeagues, driverId); const driverLeagues = await this.getDriverLeagues(allLeagues, driverId);
const driverLeagueIds = new Set(driverLeagues.map(league => league.id)); const driverLeagueIds = new Set(driverLeagues.map(league => league.id.toString()));
const now = new Date(); const now = new Date();
const upcomingRaces = allRaces const upcomingRaces = allRaces
@@ -150,7 +149,7 @@ export class DashboardOverviewUseCase {
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); .sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
const upcomingRacesInDriverLeagues = upcomingRaces.filter(race => const upcomingRacesInDriverLeagues = upcomingRaces.filter(race =>
driverLeagueIds.has(race.leagueId), driverLeagueIds.has(race.leagueId.toString()),
); );
const { myUpcomingRaces, otherUpcomingRaces } = const { myUpcomingRaces, otherUpcomingRaces } =
@@ -226,10 +225,10 @@ export class DashboardOverviewUseCase {
for (const league of allLeagues) { for (const league of allLeagues) {
const membership = await this.leagueMembershipRepository.getMembership( const membership = await this.leagueMembershipRepository.getMembership(
league.id, league.id.toString(),
driverId, driverId,
); );
if (membership && membership.status === 'active') { if (membership && membership.status.toString() === 'active') {
driverLeagues.push(league); driverLeagues.push(league);
} }
} }
@@ -284,13 +283,13 @@ export class DashboardOverviewUseCase {
driverId: string, driverId: string,
): DashboardRecentRaceResultSummary[] { ): DashboardRecentRaceResultSummary[] {
const raceById = new Map(allRaces.map(race => [race.id, race])); const raceById = new Map(allRaces.map(race => [race.id, race]));
const leagueById = new Map(allLeagues.map(league => [league.id, league])); const leagueById = new Map(allLeagues.map(league => [league.id.toString(), league]));
const driverResults = allResults.filter(result => result.driverId === driverId); const driverResults = allResults.filter(result => result.driverId.toString() === driverId);
const enriched = driverResults const enriched = driverResults
.map(result => { .map(result => {
const race = raceById.get(result.raceId); const race = raceById.get(result.raceId.toString());
if (!race) return null; if (!race) return null;
const league = leagueById.get(race.leagueId) ?? null; const league = leagueById.get(race.leagueId) ?? null;
@@ -321,9 +320,9 @@ export class DashboardOverviewUseCase {
const summaries: DashboardLeagueStandingSummary[] = []; const summaries: DashboardLeagueStandingSummary[] = [];
for (const league of driverLeagues.slice(0, 3)) { for (const league of driverLeagues.slice(0, 3)) {
const standings = await this.standingRepository.findByLeagueId(league.id); const standings = await this.standingRepository.findByLeagueId(league.id.toString());
const driverStanding = standings.find( const driverStanding = standings.find(
(standing: Standing) => standing.driverId === driverId, (standing: Standing) => standing.driverId.toString() === driverId,
); );
summaries.push({ summaries.push({
@@ -347,7 +346,7 @@ export class DashboardOverviewUseCase {
} }
for (const standing of leagueStandingsSummaries) { for (const standing of leagueStandingsSummaries) {
activeLeagueIds.add(standing.league.id); activeLeagueIds.add(standing.league.id.toString());
} }
return activeLeagueIds.size; return activeLeagueIds.size;

View File

@@ -1,11 +1,10 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { FileProtestUseCase, type FileProtestInput, type FileProtestResult, type FileProtestErrorCode } from './FileProtestUseCase';
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { Result } from '@core/shared/application/Result'; import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import { FileProtestUseCase, type FileProtestErrorCode, type FileProtestInput, type FileProtestResult } from './FileProtestUseCase';
describe('FileProtestUseCase', () => { describe('FileProtestUseCase', () => {
let mockProtestRepo: { let mockProtestRepo: {

View File

@@ -1 +0,0 @@
export * from './Result';

View File

@@ -9,7 +9,7 @@ export * from './domain/entities/Team';
export * from './domain/entities/Track'; export * from './domain/entities/Track';
export * from './domain/entities/Car'; export * from './domain/entities/Car';
export * from './domain/entities/Protest'; export * from './domain/entities/Protest';
export * from './domain/entities/Penalty'; export * from './domain/entities/penalty/Penalty';
export * from './domain/repositories/IDriverRepository'; export * from './domain/repositories/IDriverRepository';
export * from './domain/repositories/ILeagueRepository'; export * from './domain/repositories/ILeagueRepository';

View File

@@ -0,0 +1,206 @@
Delivery Adapters vs App Code (Strict Clean Architecture)
This document clarifies where Controllers live, why they are adapters, and how to structure them cleanly in large systems.
It resolves the common confusion between:
• architectural role (what something is)
• physical placement (where something lives in the repo)
This document is framework-agnostic in its principles and feature-based in its structure.
1. Clean Architecture Layers (Authoritative)
Clean Architecture defines roles, not folders.
ENTITIES
USE CASES
INTERFACE ADAPTERS
FRAMEWORKS & DRIVERS
Everything that follows maps code to these roles.
2. What a Controller Is (Architecturally)
A Controller is an Interface Adapter.
Why:
• It receives external input (HTTP)
• It translates that input into Use Case input
• It invokes a Use Case
• It performs no business logic
By definition:
Anything that translates between external input and the application boundary is an adapter.
So:
✅ Controllers are Interface Adapters
3. Why Controllers Do NOT Live in adapters/
Although Controllers are adapters by role, they are also:
• Framework-specific
• Delivery-specific
• Not reusable outside their app
They:
• use routing decorators
• depend on HTTP concepts
• depend on a specific framework
Therefore:
Controllers belong to the delivery application, not to shared adapters.
4. Adapter vs App — The Key Distinction
Concept Meaning
Adapter Architectural role (translator)
App Delivery mechanism (HTTP, UI, CLI)
“Adapter” answers what it does.
“App” answers where it runs.
Both are correct at the same time.
5. Feature-Based Structure (Mandatory for Scale)
Flat technical folders do not scale.
Everything is organized by feature / bounded context.
6. Canonical Project Structure (Strict)
Root
core/ # Application + Domain (pure)
adapters/ # Reusable infrastructure adapters
apps/ # Delivery applications
Core (Feature-Based)
core/
└── racing/
├── domain/
│ ├── entities/
│ ├── value-objects/
│ └── services/
└── application/
├── use-cases/
├── inputs/
├── results/
└── ports/
├── gateways/
└── output/
• No framework imports
• No DTOs
• No controllers
Reusable Adapters (Framework-Agnostic Implementations)
adapters/
└── racing/
├── persistence/
│ ├── TypeOrmRaceRepository.ts
│ └── TypeOrmDriverRepository.ts
├── presentation/
│ └── presenters/
│ └── GetDashboardOverviewPresenter.ts
├── messaging/
│ └── EventPublisher.ts
└── logging/
└── StructuredLogger.ts
These adapters:
• implement Core ports
• are reusable across apps
• do not depend on routing or UI
API App (Delivery Application)
apps/api/
└── racing/
├── controllers/
│ └── DashboardController.ts
├── services/
│ └── DashboardApplicationService.ts
├── dto/
│ └── DashboardOverviewResponseDto.ts
└── module.ts
Responsibilities:
• Controllers translate HTTP → Input
• Application Services orchestrate Use Case + Presenter
• DTOs represent HTTP contracts
• Module wires dependencies
7. Responsibilities by Layer (No Overlap)
Controllers
• HTTP only
• No business logic
• No mapping logic
• Call Application Services only
Application Services (API)
• Instantiate Output Adapters
• Invoke Use Cases
• Return response DTOs
Presenters
• Implement Output Ports
• Map Result → DTO/ViewModel
• Hold state per execution
8. Forbidden Patterns
❌ Controllers inside adapters/
❌ Use Cases inside apps/api
❌ DTOs inside core
❌ Controllers calling Use Cases directly
❌ Business logic in Controllers or Services
9. Final Mental Model
Controllers are adapters by responsibility.
Apps define where adapters live.
This separation allows:
• strict Clean Architecture
• multiple delivery mechanisms
• feature-level scalability
10. One-Line Summary
Controller = Adapter (role), App = Delivery (location).
This document is the authoritative reference for controller placement and adapter roles.