refactor driver module (wip)

This commit is contained in:
2025-12-22 10:24:40 +01:00
parent e7dbec4a85
commit 9da528d5bd
108 changed files with 842 additions and 947 deletions

View File

@@ -65,7 +65,7 @@ export class RaceController {
@Query('driverId') driverId: string,
): Promise<RaceDetailDTO> {
const presenter = await this.raceService.getRaceDetail({ raceId, driverId });
return presenter.viewModel;
return await presenter.viewModel;
}
@Get(':raceId/results')
@@ -74,7 +74,7 @@ export class RaceController {
@ApiResponse({ status: 200, description: 'Race results detail', type: RaceResultsDetailDTO })
async getRaceResultsDetail(@Param('raceId') raceId: string): Promise<RaceResultsDetailDTO> {
const presenter = await this.raceService.getRaceResultsDetail(raceId);
return presenter.viewModel;
return await presenter.viewModel;
}
@Get(':raceId/sof')

View File

@@ -1,5 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import type { RaceDetailOutputPort } from '@core/racing/application/ports/output/RaceDetailOutputPort';
import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort';
import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort';
import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort';
@@ -130,14 +129,15 @@ export class RaceService {
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailPresenter> {
this.logger.debug('[RaceService] Fetching race detail:', params);
const presenter = new RaceDetailPresenter(this.driverRatingProvider, this.imageService, params);
this.getRaceDetailUseCase.setOutput(presenter);
const result = await this.getRaceDetailUseCase.execute(params);
if (result.isErr()) {
throw new Error('Failed to get race detail');
}
const presenter = new RaceDetailPresenter(this.driverRatingProvider, this.imageService);
await presenter.present(result.value as RaceDetailOutputPort, params);
return presenter;
}

View File

@@ -47,4 +47,8 @@ export class AllRacesPageDataPresenter {
return this.model;
}
get viewModel(): AllRacesPageDataResponseModel {
return this.responseModel;
}
}

View File

@@ -1,4 +1,3 @@
import { Result } from '@core/shared/application/Result';
import { GetAllRacesPresenter } from './GetAllRacesPresenter';
import type { GetAllRacesResult } from '@core/racing/application/use-cases/GetAllRacesUseCase';
@@ -6,7 +5,7 @@ describe('GetAllRacesPresenter', () => {
it('should map races and distinct leagues into the DTO', async () => {
const presenter = new GetAllRacesPresenter();
const output: GetAllRacesOutputPort = {
const output: GetAllRacesResult = {
races: [
{
id: 'race-1',
@@ -14,9 +13,8 @@ describe('GetAllRacesPresenter', () => {
track: 'Track A',
car: 'Car A',
status: 'scheduled',
scheduledAt: '2025-01-01T10:00:00.000Z',
scheduledAt: new Date('2025-01-01T10:00:00.000Z'),
strengthOfField: 1500,
leagueName: 'League One',
},
{
id: 'race-2',
@@ -24,9 +22,8 @@ describe('GetAllRacesPresenter', () => {
track: 'Track B',
car: 'Car B',
status: 'completed',
scheduledAt: '2025-01-02T10:00:00.000Z',
strengthOfField: null,
leagueName: 'League One',
scheduledAt: new Date('2025-01-02T10:00:00.000Z'),
strengthOfField: undefined,
},
{
id: 'race-3',
@@ -34,16 +31,22 @@ describe('GetAllRacesPresenter', () => {
track: 'Track C',
car: 'Car C',
status: 'running',
scheduledAt: '2025-01-03T10:00:00.000Z',
scheduledAt: new Date('2025-01-03T10:00:00.000Z'),
strengthOfField: 1800,
leagueName: 'League Two',
},
],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leagues: [
{ id: 'league-1', name: 'League One' },
{ id: 'league-2', name: 'League Two' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any,
totalCount: 3,
};
await presenter.present(output);
const viewModel = presenter.getViewModel();
const viewModel = presenter.getResponseModel();
expect(viewModel).not.toBeNull();
expect(viewModel!.races).toHaveLength(3);
@@ -61,13 +64,16 @@ describe('GetAllRacesPresenter', () => {
it('should handle empty races by returning empty leagues', async () => {
const presenter = new GetAllRacesPresenter();
const output: GetAllRacesOutputPort = {
races: [],
const output: GetAllRacesResult = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
races: [] as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leagues: [] as any,
totalCount: 0,
};
await presenter.present(output);
const viewModel = presenter.getViewModel();
const viewModel = presenter.getResponseModel();
expect(viewModel).not.toBeNull();
expect(viewModel!.races).toHaveLength(0);

View File

@@ -53,4 +53,8 @@ export class GetAllRacesPresenter implements UseCaseOutputPort<GetAllRacesResult
return this.model;
}
get viewModel(): GetAllRacesResponseModel {
return this.responseModel;
}
}

View File

@@ -44,4 +44,8 @@ export class GetTotalRacesPresenter {
return this.model;
}
get viewModel(): GetTotalRacesResponseModel {
return this.responseModel;
}
}

View File

@@ -50,4 +50,8 @@ export class ImportRaceResultsApiPresenter {
return this.model;
}
get viewModel(): ImportRaceResultsApiResponseModel {
return this.responseModel;
}
}

View File

@@ -1,9 +1,5 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRaceDetailResult,
GetRaceDetailErrorCode,
} from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { GetRaceDetailResult } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import type { GetRaceDetailParamsDTO } from '../dtos/GetRaceDetailParamsDTO';
@@ -16,47 +12,26 @@ import type { RaceDetailUserResultDTO } from '../dtos/RaceDetailUserResultDTO';
export type GetRaceDetailResponseModel = RaceDetailDTO;
export type GetRaceDetailApplicationError = ApplicationErrorCode<
GetRaceDetailErrorCode,
{ message: string }
>;
export class RaceDetailPresenter {
private model: GetRaceDetailResponseModel | null = null;
export class RaceDetailPresenter implements UseCaseOutputPort<GetRaceDetailResult> {
private result: GetRaceDetailResult | null = null;
constructor(
private readonly driverRatingProvider: DriverRatingProvider,
private readonly imageService: IImageServicePort,
private readonly params: GetRaceDetailParamsDTO,
) {}
reset(): void {
this.model = null;
present(result: GetRaceDetailResult): void {
this.result = result;
}
async present(
result: Result<GetRaceDetailResult, GetRaceDetailApplicationError>,
params: GetRaceDetailParamsDTO,
): Promise<void> {
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'RACE_NOT_FOUND') {
this.model = {
race: null,
league: null,
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
userResult: null,
} as RaceDetailDTO;
return;
}
throw new Error(error.details?.message ?? 'Failed to get race detail');
async getResponseModel(): Promise<GetRaceDetailResponseModel | null> {
if (!this.result) {
return null;
}
const output = result.unwrap();
const output = this.result;
const params = this.params;
const raceDTO: RaceDetailRaceDTO | null = output.race
? {
@@ -118,7 +93,7 @@ export class RaceDetailPresenter {
}
: null;
this.model = {
return {
race: raceDTO,
league: leagueDTO,
entryList: entryListDTO,
@@ -127,16 +102,11 @@ export class RaceDetailPresenter {
} as RaceDetailDTO;
}
getResponseModel(): GetRaceDetailResponseModel | null {
return this.model;
}
get responseModel(): GetRaceDetailResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.model;
get viewModel(): Promise<GetRaceDetailResponseModel> {
return this.getResponseModel().then(model => {
if (!model) throw new Error('Presenter not presented');
return model;
});
}
private calculateRatingChange(position: number): number {

View File

@@ -62,4 +62,8 @@ export class RacePenaltiesPresenter {
return this.model;
}
get viewModel(): GetRacePenaltiesResponseModel {
return this.responseModel;
}
}

View File

@@ -63,4 +63,8 @@ export class RaceProtestsPresenter {
return this.model;
}
get viewModel(): GetRaceProtestsResponseModel {
return this.responseModel;
}
}

View File

@@ -1,4 +1,3 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRaceResultsDetailResult,
@@ -16,33 +15,20 @@ export type GetRaceResultsDetailApplicationError = ApplicationErrorCode<
>;
export class RaceResultsDetailPresenter {
private model: GetRaceResultsDetailResponseModel | null = null;
private result: GetRaceResultsDetailResult | null = null;
constructor(private readonly imageService: IImageServicePort) {}
reset(): void {
this.model = null;
present(result: GetRaceResultsDetailResult): void {
this.result = result;
}
async present(
result: Result<GetRaceResultsDetailResult, GetRaceResultsDetailApplicationError>,
): Promise<void> {
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'RACE_NOT_FOUND') {
this.model = {
raceId: '',
track: '',
results: [],
} as RaceResultsDetailDTO;
return;
}
throw new Error(error.details?.message ?? 'Failed to get race results detail');
async getResponseModel(): Promise<GetRaceResultsDetailResponseModel | null> {
if (!this.result) {
return null;
}
const output = result.unwrap();
const output = this.result;
const driverMap = new Map(output.drivers.map(driver => [driver.id, driver]));
@@ -70,22 +56,17 @@ export class RaceResultsDetailPresenter {
}),
);
this.model = {
return {
raceId: output.race.id,
track: output.race.track,
results,
} as RaceResultsDetailDTO;
}
getResponseModel(): GetRaceResultsDetailResponseModel | null {
return this.model;
}
get responseModel(): GetRaceResultsDetailResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.model;
get viewModel(): Promise<GetRaceResultsDetailResponseModel> {
return this.getResponseModel().then(model => {
if (!model) throw new Error('Presenter not presented');
return model;
});
}
}

View File

@@ -55,4 +55,8 @@ export class RaceWithSOFPresenter {
return this.model;
}
get viewModel(): GetRaceWithSOFResponseModel {
return this.responseModel;
}
}

View File

@@ -59,4 +59,8 @@ export class RacesPageDataPresenter {
return this.model;
}
get viewModel(): GetRacesPageDataResponseModel {
return this.responseModel;
}
}