refactor driver module (wip)
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,4 +47,8 @@ export class AllRacesPageDataPresenter {
|
||||
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get viewModel(): AllRacesPageDataResponseModel {
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -53,4 +53,8 @@ export class GetAllRacesPresenter implements UseCaseOutputPort<GetAllRacesResult
|
||||
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get viewModel(): GetAllRacesResponseModel {
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,8 @@ export class GetTotalRacesPresenter {
|
||||
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get viewModel(): GetTotalRacesResponseModel {
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
@@ -50,4 +50,8 @@ export class ImportRaceResultsApiPresenter {
|
||||
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get viewModel(): ImportRaceResultsApiResponseModel {
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -62,4 +62,8 @@ export class RacePenaltiesPresenter {
|
||||
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get viewModel(): GetRacePenaltiesResponseModel {
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,4 +63,8 @@ export class RaceProtestsPresenter {
|
||||
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get viewModel(): GetRaceProtestsResponseModel {
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,4 +55,8 @@ export class RaceWithSOFPresenter {
|
||||
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get viewModel(): GetRaceWithSOFResponseModel {
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,8 @@ export class RacesPageDataPresenter {
|
||||
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get viewModel(): GetRacesPageDataResponseModel {
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user