fix issues in core
This commit is contained in:
@@ -1 +1,2 @@
|
||||
./html-dumps
|
||||
./html-dumps
|
||||
apps/companion
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AuthenticationState } from '../../domain/value-objects/AuthenticationState';
|
||||
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 { SessionLifetime } from '../../domain/value-objects/SessionLifetime';
|
||||
import type { SessionValidatorPort } from '../ports/SessionValidatorPort';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { vi, Mock } from 'vitest';
|
||||
import { ClearSessionUseCase } from './ClearSessionUseCase';
|
||||
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@gridpilot/shared/application/Result';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
describe('ClearSessionUseCase', () => {
|
||||
let useCase: ClearSessionUseCase;
|
||||
|
||||
@@ -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 { Logger } from '@core/shared/application';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
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 { CheckoutPrice } from 'apps/companion/main/automation/domain/value-objects/CheckoutPrice';
|
||||
import type { CheckoutServicePort } from 'apps/companion/main/automation/application/ports/CheckoutServicePort';
|
||||
|
||||
@@ -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 type { CheckoutServicePort } from '../ports/CheckoutServicePort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 type { CheckoutServicePort } from 'apps/companion/main/automation/application/ports/CheckoutServicePort';
|
||||
import type { CheckoutConfirmationPort } from 'apps/companion/main/automation/application/ports/CheckoutConfirmationPort';
|
||||
|
||||
@@ -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 { CheckoutServicePort } from '../ports/CheckoutServicePort';
|
||||
import type { CheckoutConfirmationPort } from '../ports/CheckoutConfirmationPort';
|
||||
|
||||
@@ -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 { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { VerifyAuthenticatedPageUseCase } from 'apps/companion/main/automation/application/use-cases/VerifyAuthenticatedPageUseCase';
|
||||
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 { AuthenticationState } from 'apps/companion/main/automation/domain/value-objects/AuthenticationState';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 type { Logger } from '@core/shared/application';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StepId } from '../value-objects/StepId';
|
||||
|
||||
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';
|
||||
|
||||
export interface ValidationResult {
|
||||
|
||||
@@ -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 { CheckoutState } from '../../../domain/value-objects/CheckoutState';
|
||||
import type { CheckoutInfoDTO } from '../../../application/dto/CheckoutInfoDTO';
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { AuthenticationServicePort } from '../../../../application/ports/Au
|
||||
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
|
||||
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
|
||||
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 { SessionCookieStore } from './SessionCookieStore';
|
||||
import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Page, Locator } from 'playwright';
|
||||
import { AuthenticationState } from 'apps/companion/main/automation/domain/value-objects/AuthenticationState';
|
||||
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 { Result } from '@gridpilot/shared/application/Result';
|
||||
import type { Result } from '@core/shared/application/Result';
|
||||
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
|
||||
import { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
|
||||
import { PlaywrightAuthSessionService } from './PlaywrightAuthSessionService';
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
|
||||
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';
|
||||
|
||||
interface Cookie {
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO'
|
||||
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
|
||||
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
|
||||
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 { SessionCookieStore } from '../auth/SessionCookieStore';
|
||||
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
PageStateValidation,
|
||||
PageStateValidationResult,
|
||||
} from '../../../../domain/services/PageStateValidator';
|
||||
import type { Result } from '@gridpilot/shared/application/Result';
|
||||
import type { Result } from '@core/shared/application/Result';
|
||||
|
||||
interface WizardStepOrchestratorDeps {
|
||||
config: Required<PlaywrightConfig>;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { BrowserWindow } 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 { CheckoutConfirmationRequestDTO } from '../../../application/dto/CheckoutConfirmationRequestDTO';
|
||||
import { CheckoutConfirmation } from '../../../domain/value-objects/CheckoutConfirmation';
|
||||
|
||||
@@ -189,7 +189,7 @@ describe('DashboardOverviewUseCase', () => {
|
||||
getMembership: async (leagueId: string, driverIdParam: string): Promise<LeagueMembership | null> => {
|
||||
return (
|
||||
memberships.find(
|
||||
m => m.leagueId === leagueId && m.driverId === driverIdParam,
|
||||
m => m.leagueId.toString() === leagueId && m.driverId.toString() === driverIdParam,
|
||||
) ?? null
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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 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 { 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 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 {
|
||||
driverId: string;
|
||||
@@ -49,7 +48,7 @@ export interface DashboardRaceSummary {
|
||||
export interface DashboardRecentRaceResultSummary {
|
||||
race: Race;
|
||||
league: League | null;
|
||||
result: RaceResult;
|
||||
result: Result;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -142,7 +141,7 @@ export class DashboardOverviewUseCase {
|
||||
};
|
||||
|
||||
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 upcomingRaces = allRaces
|
||||
@@ -150,7 +149,7 @@ export class DashboardOverviewUseCase {
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
|
||||
const upcomingRacesInDriverLeagues = upcomingRaces.filter(race =>
|
||||
driverLeagueIds.has(race.leagueId),
|
||||
driverLeagueIds.has(race.leagueId.toString()),
|
||||
);
|
||||
|
||||
const { myUpcomingRaces, otherUpcomingRaces } =
|
||||
@@ -226,10 +225,10 @@ export class DashboardOverviewUseCase {
|
||||
|
||||
for (const league of allLeagues) {
|
||||
const membership = await this.leagueMembershipRepository.getMembership(
|
||||
league.id,
|
||||
league.id.toString(),
|
||||
driverId,
|
||||
);
|
||||
if (membership && membership.status === 'active') {
|
||||
if (membership && membership.status.toString() === 'active') {
|
||||
driverLeagues.push(league);
|
||||
}
|
||||
}
|
||||
@@ -284,13 +283,13 @@ export class DashboardOverviewUseCase {
|
||||
driverId: string,
|
||||
): DashboardRecentRaceResultSummary[] {
|
||||
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
|
||||
.map(result => {
|
||||
const race = raceById.get(result.raceId);
|
||||
const race = raceById.get(result.raceId.toString());
|
||||
if (!race) return null;
|
||||
|
||||
const league = leagueById.get(race.leagueId) ?? null;
|
||||
@@ -321,9 +320,9 @@ export class DashboardOverviewUseCase {
|
||||
const summaries: DashboardLeagueStandingSummary[] = [];
|
||||
|
||||
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(
|
||||
(standing: Standing) => standing.driverId === driverId,
|
||||
(standing: Standing) => standing.driverId.toString() === driverId,
|
||||
);
|
||||
|
||||
summaries.push({
|
||||
@@ -347,7 +346,7 @@ export class DashboardOverviewUseCase {
|
||||
}
|
||||
|
||||
for (const standing of leagueStandingsSummaries) {
|
||||
activeLeagueIds.add(standing.league.id);
|
||||
activeLeagueIds.add(standing.league.id.toString());
|
||||
}
|
||||
|
||||
return activeLeagueIds.size;
|
||||
|
||||
@@ -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 { 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', () => {
|
||||
let mockProtestRepo: {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './Result';
|
||||
@@ -9,7 +9,7 @@ export * from './domain/entities/Team';
|
||||
export * from './domain/entities/Track';
|
||||
export * from './domain/entities/Car';
|
||||
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/ILeagueRepository';
|
||||
|
||||
206
docs/architecture/ADAPTERS.md
Normal file
206
docs/architecture/ADAPTERS.md
Normal 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.
|
||||
Reference in New Issue
Block a user