refactor api modules

This commit is contained in:
2025-12-22 19:17:33 +01:00
parent c90b2166c1
commit 1333f5e907
100 changed files with 2226 additions and 1936 deletions

View File

@@ -3,7 +3,8 @@ import { vi } from 'vitest';
import { TeamController } from './TeamController';
import { TeamService } from './TeamService';
import type { Request } from 'express';
import { CreateTeamInputDTO, UpdateTeamInputDTO } from './dtos/CreateTeamInputDTO';
import { CreateTeamInputDTO } from './dtos/CreateTeamInputDTO';
import { UpdateTeamInput } from './dtos/TeamDto';
describe('TeamController', () => {
let controller: TeamController;
@@ -35,7 +36,7 @@ describe('TeamController', () => {
describe('getAll', () => {
it('should return all teams', async () => {
const result = { teams: [] };
const result = { teams: [], totalCount: 0 };
service.getAll.mockResolvedValue(result);
const response = await controller.getAll();
@@ -49,12 +50,12 @@ describe('TeamController', () => {
it('should return team details', async () => {
const teamId = 'team-123';
const userId = 'user-456';
const result = { id: teamId, name: 'Team' };
const result = { team: { id: teamId, name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [] }, membership: null, canManage: false };
service.getDetails.mockResolvedValue(result);
const mockReq: Partial<Request> = { ['user']: { userId } };
const mockReq = { user: { userId } } as any;
const response = await controller.getDetails(teamId, mockReq as Request);
const response = await controller.getDetails(teamId, mockReq);
expect(service.getDetails).toHaveBeenCalledWith(teamId, userId);
expect(response).toEqual(result);
@@ -64,7 +65,7 @@ describe('TeamController', () => {
describe('getMembers', () => {
it('should return team members', async () => {
const teamId = 'team-123';
const result = { members: [] };
const result = { members: [], totalCount: 0, ownerCount: 0, managerCount: 0, memberCount: 0 };
service.getMembers.mockResolvedValue(result);
const response = await controller.getMembers(teamId);
@@ -77,7 +78,7 @@ describe('TeamController', () => {
describe('getJoinRequests', () => {
it('should return join requests', async () => {
const teamId = 'team-123';
const result = { requests: [] };
const result = { requests: [], pendingCount: 0, totalCount: 0 };
service.getJoinRequests.mockResolvedValue(result);
const response = await controller.getJoinRequests(teamId);
@@ -89,14 +90,14 @@ describe('TeamController', () => {
describe('create', () => {
it('should create team', async () => {
const input: CreateTeamInputDTO = { name: 'New Team' };
const input: CreateTeamInputDTO = { name: 'New Team', tag: 'TAG' };
const userId = 'user-123';
const result = { teamId: 'team-456' };
const result = { id: 'team-456', success: true };
service.create.mockResolvedValue(result);
const mockReq: Partial<Request> = { ['user']: { userId } };
const mockReq = { user: { userId } } as any;
const response = await controller.create(input, mockReq as Request);
const response = await controller.create(input, mockReq);
expect(service.create).toHaveBeenCalledWith(input, userId);
expect(response).toEqual(result);
@@ -106,14 +107,14 @@ describe('TeamController', () => {
describe('update', () => {
it('should update team', async () => {
const teamId = 'team-123';
const input: UpdateTeamInputDTO = { name: 'Updated Team' };
const userId = 'user-456';
const input: UpdateTeamInput = { name: 'Updated Team', updatedBy: userId };
const result = { success: true };
service.update.mockResolvedValue(result);
const mockReq: Partial<Request> = { ['user']: { userId } };
const mockReq = { user: { userId } } as any;
const response = await controller.update(teamId, input, mockReq as Request);
const response = await controller.update(teamId, input, mockReq);
expect(service.update).toHaveBeenCalledWith(teamId, input, userId);
expect(response).toEqual(result);
@@ -123,7 +124,7 @@ describe('TeamController', () => {
describe('getDriverTeam', () => {
it('should return driver team', async () => {
const driverId = 'driver-123';
const result = { teamId: 'team-456' };
const result = { team: { id: 'team-456', name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [] }, membership: { role: 'member' as const, joinedAt: '2023-01-01', isActive: true }, isOwner: false, canManage: false };
service.getDriverTeam.mockResolvedValue(result);
const response = await controller.getDriverTeam(driverId);
@@ -137,7 +138,7 @@ describe('TeamController', () => {
it('should return team membership', async () => {
const teamId = 'team-123';
const driverId = 'driver-456';
const result = { role: 'member' };
const result = { role: 'member' as const, joinedAt: '2023-01-01', isActive: true };
service.getMembership.mockResolvedValue(result);
const response = await controller.getMembership(teamId, driverId);

View File

@@ -8,7 +8,7 @@ import { GetTeamMembersOutputDTO } from './dtos/GetTeamMembersOutputDTO';
import { GetTeamJoinRequestsOutputDTO } from './dtos/GetTeamJoinRequestsOutputDTO';
import { CreateTeamInputDTO } from './dtos/CreateTeamInputDTO';
import { CreateTeamOutputDTO } from './dtos/CreateTeamOutputDTO';
import { UpdateTeamInputDTO } from './dtos/UpdateTeamInputDTO';
import { UpdateTeamInput } from './dtos/TeamDto';
import { UpdateTeamOutputDTO } from './dtos/UpdateTeamOutputDTO';
import { GetDriverTeamOutputDTO } from './dtos/GetDriverTeamOutputDTO';
import { GetTeamMembershipOutputDTO } from './dtos/GetTeamMembershipOutputDTO';
@@ -22,8 +22,7 @@ export class TeamController {
@ApiOperation({ summary: 'Get all teams' })
@ApiResponse({ status: 200, description: 'List of all teams', type: GetAllTeamsOutputDTO })
async getAll(): Promise<GetAllTeamsOutputDTO> {
const presenter = await this.teamService.getAll();
return presenter.responseModel;
return await this.teamService.getAll();
}
@Get(':teamId')
@@ -31,43 +30,38 @@ export class TeamController {
@ApiResponse({ status: 200, description: 'Team details', type: GetTeamDetailsOutputDTO })
@ApiResponse({ status: 404, description: 'Team not found' })
async getDetails(@Param('teamId') teamId: string, @Req() req: Request): Promise<GetTeamDetailsOutputDTO | null> {
const userId = req['user']?.userId;
const presenter = await this.teamService.getDetails(teamId, userId);
return presenter.getResponseModel();
const userId = (req as any)['user']?.userId;
return await this.teamService.getDetails(teamId, userId);
}
@Get(':teamId/members')
@ApiOperation({ summary: 'Get team members' })
@ApiResponse({ status: 200, description: 'Team members', type: GetTeamMembersOutputDTO })
async getMembers(@Param('teamId') teamId: string): Promise<GetTeamMembersOutputDTO> {
const presenter = await this.teamService.getMembers(teamId);
return presenter.getResponseModel()!;
return await this.teamService.getMembers(teamId);
}
@Get(':teamId/join-requests')
@ApiOperation({ summary: 'Get team join requests' })
@ApiResponse({ status: 200, description: 'Team join requests', type: GetTeamJoinRequestsOutputDTO })
async getJoinRequests(@Param('teamId') teamId: string): Promise<GetTeamJoinRequestsOutputDTO> {
const presenter = await this.teamService.getJoinRequests(teamId);
return presenter.getResponseModel()!;
return await this.teamService.getJoinRequests(teamId);
}
@Post()
@ApiOperation({ summary: 'Create a new team' })
@ApiResponse({ status: 201, description: 'Team created', type: CreateTeamOutputDTO })
async create(@Body() input: CreateTeamInputDTO, @Req() req: Request): Promise<CreateTeamOutputDTO> {
const userId = req['user']?.userId;
const presenter = await this.teamService.create(input, userId);
return presenter.responseModel;
const userId = (req as any)['user']?.userId;
return await this.teamService.create(input, userId);
}
@Patch(':teamId')
@ApiOperation({ summary: 'Update team' })
@ApiResponse({ status: 200, description: 'Team updated', type: UpdateTeamOutputDTO })
async update(@Param('teamId') teamId: string, @Body() input: UpdateTeamInputDTO, @Req() req: Request): Promise<UpdateTeamOutputDTO> {
const userId = req['user']?.userId;
const presenter = await this.teamService.update(teamId, input, userId);
return presenter.responseModel;
async update(@Param('teamId') teamId: string, @Body() input: UpdateTeamInput, @Req() req: Request): Promise<UpdateTeamOutputDTO> {
const userId = (req as any)['user']?.userId;
return await this.teamService.update(teamId, input, userId);
}
@Get('driver/:driverId')
@@ -75,8 +69,7 @@ export class TeamController {
@ApiResponse({ status: 200, description: 'Driver\'s team', type: GetDriverTeamOutputDTO })
@ApiResponse({ status: 404, description: 'Team not found' })
async getDriverTeam(@Param('driverId') driverId: string): Promise<GetDriverTeamOutputDTO | null> {
const presenter = await this.teamService.getDriverTeam(driverId);
return presenter.getResponseModel();
return await this.teamService.getDriverTeam(driverId);
}
@Get(':teamId/members/:driverId')
@@ -84,7 +77,6 @@ export class TeamController {
@ApiResponse({ status: 200, description: 'Team membership', type: GetTeamMembershipOutputDTO })
@ApiResponse({ status: 404, description: 'Membership not found' })
async getMembership(@Param('teamId') teamId: string, @Param('driverId') driverId: string): Promise<GetTeamMembershipOutputDTO | null> {
const presenter = await this.teamService.getMembership(teamId, driverId);
return presenter.responseModel;
return await this.teamService.getMembership(teamId, driverId);
}
}

View File

@@ -3,10 +3,6 @@ import { TeamService } from './TeamService';
// Import core interfaces
import type { Logger } from '@core/shared/application/Logger';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Import concrete in-memory implementations
import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository';
@@ -15,15 +11,7 @@ import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase';
import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase';
import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase';
import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase';
// Use cases are imported and used directly in the service
// Define injection tokens
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
@@ -58,53 +46,5 @@ export const TeamProviders: Provider[] = [
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Use cases
{
provide: GetAllTeamsUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetAllTeamsUseCase(teamRepo, membershipRepo, logger),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: GetTeamDetailsUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new GetTeamDetailsUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: GetTeamMembersUseCase,
useFactory: (membershipRepo: ITeamMembershipRepository, driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) =>
new GetTeamMembersUseCase(membershipRepo, driverRepo, imageService, logger),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN],
},
{
provide: GetTeamJoinRequestsUseCase,
useFactory: (membershipRepo: ITeamMembershipRepository, driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) =>
new GetTeamJoinRequestsUseCase(membershipRepo, driverRepo, imageService, logger),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN],
},
{
provide: CreateTeamUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new CreateTeamUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: UpdateTeamUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new UpdateTeamUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: GetDriverTeamUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetDriverTeamUseCase(teamRepo, membershipRepo, logger),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: GetTeamMembershipUseCase,
useFactory: (membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetTeamMembershipUseCase(membershipRepo, logger),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
// Use cases are created directly in the service
];

View File

@@ -1,4 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { TeamService } from './TeamService';
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
@@ -11,20 +12,20 @@ import { DriverTeamViewModel } from './dtos/TeamDto';
describe('TeamService', () => {
let service: TeamService;
let getAllTeamsUseCase: jest.Mocked<GetAllTeamsUseCase>;
let getDriverTeamUseCase: jest.Mocked<GetDriverTeamUseCase>;
let getAllTeamsUseCase: ReturnType<typeof vi.mocked<GetAllTeamsUseCase>>;
let getDriverTeamUseCase: ReturnType<typeof vi.mocked<GetDriverTeamUseCase>>;
beforeEach(async () => {
const mockGetAllTeamsUseCase = {
execute: jest.fn(),
execute: vi.fn(),
};
const mockGetDriverTeamUseCase = {
execute: jest.fn(),
execute: vi.fn(),
};
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
debug: vi.fn(),
info: vi.fn(),
error: vi.fn(),
};
const module: TestingModule = await Test.createTestingModule({
@@ -61,11 +62,11 @@ describe('TeamService', () => {
getAllTeamsUseCase.execute.mockResolvedValue(mockResult as any);
const mockPresenter = {
present: jest.fn(),
getViewModel: jest.fn().mockReturnValue({ teams: [], totalCount: 0 }),
present: vi.fn(),
getViewModel: vi.fn().mockReturnValue({ teams: [], totalCount: 0 }),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(AllTeamsPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
(AllTeamsPresenter as any) = vi.fn().mockImplementation(() => mockPresenter);
const result = await service.getAll();
@@ -81,11 +82,11 @@ describe('TeamService', () => {
getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any);
const mockPresenter = {
present: jest.fn(),
getViewModel: jest.fn().mockReturnValue({} as DriverTeamViewModel),
present: vi.fn(),
getViewModel: vi.fn().mockReturnValue({} as DriverTeamViewModel),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(DriverTeamPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
(DriverTeamPresenter as any) = vi.fn().mockImplementation(() => mockPresenter);
const result = await service.getDriverTeam('driver1');

View File

@@ -15,7 +15,6 @@ import type { Logger } from '@core/shared/application/Logger';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Use cases
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
@@ -38,7 +37,7 @@ import { CreateTeamPresenter } from './presenters/CreateTeamPresenter';
import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter';
// Tokens
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN } from './TeamProviders';
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN } from './TeamProviders';
@Injectable()
export class TeamService {
@@ -46,7 +45,6 @@ export class TeamService {
@Inject(TEAM_REPOSITORY_TOKEN) private readonly teamRepository: ITeamRepository,
@Inject(TEAM_MEMBERSHIP_REPOSITORY_TOKEN) private readonly membershipRepository: ITeamMembershipRepository,
@Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository,
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
@@ -57,11 +55,11 @@ export class TeamService {
const useCase = new GetAllTeamsUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
const result = await useCase.execute();
if (result.isErr()) {
this.logger.error('Error fetching all teams', result.error?.details?.message || 'Unknown error');
this.logger.error('Error fetching all teams', new Error(result.error?.details?.message || 'Unknown error'));
return { teams: [], totalCount: 0 };
}
return presenter.responseModel;
return presenter.getResponseModel()!;
}
async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> {
@@ -75,14 +73,14 @@ export class TeamService {
return null;
}
return presenter.getResponseModel();
return presenter.getResponseModel()!;
}
async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> {
this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`);
const presenter = new TeamMembersPresenter();
const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.imageService, this.logger, presenter);
const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, this.logger, presenter);
const result = await useCase.execute({ teamId });
if (result.isErr()) {
this.logger.error(`Error fetching team members for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
@@ -105,7 +103,7 @@ export class TeamService {
const useCase = new GetTeamJoinRequestsUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, presenter);
const result = await useCase.execute({ teamId });
if (result.isErr()) {
this.logger.error(new Error(`Error fetching team join requests for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`));
this.logger.error(`Error fetching team join requests for teamId: ${teamId}`, new Error(result.error?.details?.message || 'Unknown error'));
return {
requests: [],
pendingCount: 0,

View File

@@ -1,16 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateTeamInputDTO {
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
name!: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
tag: string;
tag!: string;
@ApiProperty({ required: false })
@IsOptional()

View File

@@ -2,8 +2,8 @@ import { ApiProperty } from '@nestjs/swagger';
export class CreateTeamOutputDTO {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
success: boolean;
success!: boolean;
}

View File

@@ -2,22 +2,22 @@ import { ApiProperty } from '@nestjs/swagger';
class TeamListItemDTO {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
tag: string;
tag!: string;
@ApiProperty()
description: string;
description!: string;
@ApiProperty()
memberCount: number;
memberCount!: number;
@ApiProperty({ type: [String] })
leagues: string[];
leagues!: string[];
@ApiProperty({ required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@@ -31,8 +31,8 @@ class TeamListItemDTO {
export class GetAllTeamsOutputDTO {
@ApiProperty({ type: [TeamListItemDTO] })
teams: TeamListItemDTO[];
teams!: TeamListItemDTO[];
@ApiProperty()
totalCount: number;
totalCount!: number;
}

View File

@@ -2,22 +2,22 @@ import { ApiProperty } from '@nestjs/swagger';
class TeamDTO {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
tag: string;
tag!: string;
@ApiProperty()
description: string;
description!: string;
@ApiProperty()
ownerId: string;
ownerId!: string;
@ApiProperty({ type: [String] })
leagues: string[];
leagues!: string[];
@ApiProperty({ required: false })
createdAt?: string;
@@ -34,25 +34,25 @@ class TeamDTO {
class MembershipDTO {
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
}
export class GetDriverTeamOutputDTO {
@ApiProperty({ type: TeamDTO })
team: TeamDTO;
team!: TeamDTO;
@ApiProperty({ type: MembershipDTO })
membership: MembershipDTO;
membership!: MembershipDTO;
@ApiProperty()
isOwner: boolean;
isOwner!: boolean;
@ApiProperty()
canManage: boolean;
canManage!: boolean;
}

View File

@@ -2,22 +2,22 @@ import { ApiProperty } from '@nestjs/swagger';
class TeamDTO {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
tag: string;
tag!: string;
@ApiProperty()
description: string;
description!: string;
@ApiProperty()
ownerId: string;
ownerId!: string;
@ApiProperty({ type: [String] })
leagues: string[];
leagues!: string[];
@ApiProperty({ required: false })
createdAt?: string;
@@ -34,22 +34,22 @@ class TeamDTO {
class MembershipDTO {
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
}
export class GetTeamDetailsOutputDTO {
@ApiProperty({ type: TeamDTO })
team: TeamDTO;
team!: TeamDTO;
@ApiProperty({ type: MembershipDTO, nullable: true })
membership: MembershipDTO | null;
membership!: MembershipDTO | null;
@ApiProperty()
canManage: boolean;
canManage!: boolean;
}

View File

@@ -2,34 +2,34 @@ import { ApiProperty } from '@nestjs/swagger';
class TeamJoinRequestDTO {
@ApiProperty()
requestId: string;
requestId!: string;
@ApiProperty()
driverId: string;
driverId!: string;
@ApiProperty()
driverName: string;
driverName!: string;
@ApiProperty()
teamId: string;
teamId!: string;
@ApiProperty()
status: 'pending' | 'approved' | 'rejected';
status!: 'pending' | 'approved' | 'rejected';
@ApiProperty()
requestedAt: string;
requestedAt!: string;
@ApiProperty()
avatarUrl: string;
avatarUrl!: string;
}
export class GetTeamJoinRequestsOutputDTO {
@ApiProperty({ type: [TeamJoinRequestDTO] })
requests: TeamJoinRequestDTO[];
requests!: TeamJoinRequestDTO[];
@ApiProperty()
pendingCount: number;
pendingCount!: number;
@ApiProperty()
totalCount: number;
totalCount!: number;
}

View File

@@ -2,37 +2,37 @@ import { ApiProperty } from '@nestjs/swagger';
class TeamMemberDTO {
@ApiProperty()
driverId: string;
driverId!: string;
@ApiProperty()
driverName: string;
driverName!: string;
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
@ApiProperty()
avatarUrl: string;
avatarUrl!: string;
}
export class GetTeamMembersOutputDTO {
@ApiProperty({ type: [TeamMemberDTO] })
members: TeamMemberDTO[];
members!: TeamMemberDTO[];
@ApiProperty()
totalCount: number;
totalCount!: number;
@ApiProperty()
ownerCount: number;
ownerCount!: number;
@ApiProperty()
managerCount: number;
managerCount!: number;
@ApiProperty()
memberCount: number;
memberCount!: number;
}

View File

@@ -2,11 +2,11 @@ import { ApiProperty } from '@nestjs/swagger';
export class GetTeamMembershipOutputDTO {
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
}

View File

@@ -4,31 +4,31 @@ export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
class TeamLeaderboardItemDTO {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
memberCount: number;
memberCount!: number;
@ApiProperty({ nullable: true })
rating: number | null;
rating!: number | null;
@ApiProperty()
totalWins: number;
totalWins!: number;
@ApiProperty()
totalRaces: number;
totalRaces!: number;
@ApiProperty({ enum: ['beginner', 'intermediate', 'advanced', 'pro'] })
performanceLevel: SkillLevel;
performanceLevel!: SkillLevel;
@ApiProperty()
isRecruiting: boolean;
isRecruiting!: boolean;
@ApiProperty()
createdAt: string;
createdAt!: string;
@ApiProperty({ required: false })
description?: string;
@@ -45,14 +45,14 @@ class TeamLeaderboardItemDTO {
export class GetTeamsLeaderboardOutputDTO {
@ApiProperty({ type: [TeamLeaderboardItemDTO] })
teams: TeamLeaderboardItemDTO[];
teams!: TeamLeaderboardItemDTO[];
@ApiProperty()
recruitingCount: number;
recruitingCount!: number;
@ApiProperty({ type: 'object', additionalProperties: { type: 'array', items: { $ref: '#/components/schemas/TeamLeaderboardItemDTO' } } })
groupsBySkillLevel: Record<SkillLevel, TeamLeaderboardItemDTO[]>;
groupsBySkillLevel!: Record<SkillLevel, TeamLeaderboardItemDTO[]>;
@ApiProperty({ type: [TeamLeaderboardItemDTO] })
topTeams: TeamLeaderboardItemDTO[];
topTeams!: TeamLeaderboardItemDTO[];
}

View File

@@ -3,22 +3,22 @@ import { IsString, IsNotEmpty, IsBoolean, IsOptional } from 'class-validator';
export class TeamListItemViewModel {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
tag: string;
tag!: string;
@ApiProperty()
description: string;
description!: string;
@ApiProperty()
memberCount: number;
memberCount!: number;
@ApiProperty({ type: [String] })
leagues: string[];
leagues!: string[];
@ApiProperty({ required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@@ -32,30 +32,30 @@ export class TeamListItemViewModel {
export class AllTeamsViewModel {
@ApiProperty({ type: [TeamListItemViewModel] })
teams: TeamListItemViewModel[];
teams!: TeamListItemViewModel[];
@ApiProperty()
totalCount: number;
totalCount!: number;
}
export class TeamViewModel {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
tag: string;
tag!: string;
@ApiProperty()
description: string;
description!: string;
@ApiProperty()
ownerId: string;
ownerId!: string;
@ApiProperty({ type: [String] })
leagues: string[];
leagues!: string[];
@ApiProperty({ required: false })
createdAt?: string;
@@ -85,131 +85,131 @@ export enum MembershipStatus {
export class MembershipViewModel {
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
}
export class DriverTeamViewModel {
@ApiProperty({ type: TeamViewModel })
team: TeamViewModel;
team!: TeamViewModel;
@ApiProperty({ type: MembershipViewModel })
membership: MembershipViewModel;
membership!: MembershipViewModel;
@ApiProperty()
isOwner: boolean;
isOwner!: boolean;
@ApiProperty()
canManage: boolean;
canManage!: boolean;
}
export class GetDriverTeamQuery {
@ApiProperty()
@IsString()
teamId: string;
teamId!: string;
@ApiProperty()
@IsString()
driverId: string;
driverId!: string;
}
export class TeamDetailsViewModel {
@ApiProperty({ type: TeamViewModel })
team: TeamViewModel;
team!: TeamViewModel;
@ApiProperty({ type: MembershipViewModel, nullable: true })
membership: MembershipViewModel | null;
membership!: MembershipViewModel | null;
@ApiProperty()
canManage: boolean;
canManage!: boolean;
}
export class TeamMemberViewModel {
@ApiProperty()
driverId: string;
driverId!: string;
@ApiProperty()
driverName: string;
driverName!: string;
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
@ApiProperty()
avatarUrl: string;
avatarUrl!: string;
}
export class TeamMembersViewModel {
@ApiProperty({ type: [TeamMemberViewModel] })
members: TeamMemberViewModel[];
members!: TeamMemberViewModel[];
@ApiProperty()
totalCount: number;
totalCount!: number;
@ApiProperty()
ownerCount: number;
ownerCount!: number;
@ApiProperty()
managerCount: number;
managerCount!: number;
@ApiProperty()
memberCount: number;
memberCount!: number;
}
export class TeamJoinRequestViewModel {
@ApiProperty()
requestId: string;
requestId!: string;
@ApiProperty()
driverId: string;
driverId!: string;
@ApiProperty()
driverName: string;
driverName!: string;
@ApiProperty()
teamId: string;
teamId!: string;
@ApiProperty()
status: 'pending' | 'approved' | 'rejected';
status!: 'pending' | 'approved' | 'rejected';
@ApiProperty()
requestedAt: string;
requestedAt!: string;
@ApiProperty()
avatarUrl: string;
avatarUrl!: string;
}
export class TeamJoinRequestsViewModel {
@ApiProperty({ type: [TeamJoinRequestViewModel] })
requests: TeamJoinRequestViewModel[];
requests!: TeamJoinRequestViewModel[];
@ApiProperty()
pendingCount: number;
pendingCount!: number;
@ApiProperty()
totalCount: number;
totalCount!: number;
}
export class CreateTeamInput {
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
name!: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
tag: string;
tag!: string;
@ApiProperty({ required: false })
@IsOptional()
@@ -218,17 +218,17 @@ export class CreateTeamInput {
@ApiProperty()
@IsString()
ownerId: string;
ownerId!: string;
}
export class CreateTeamOutput {
@ApiProperty()
@IsString()
teamId: string;
teamId!: string;
@ApiProperty()
@IsBoolean()
success: boolean;
success!: boolean;
}
export class UpdateTeamInput {
@@ -254,19 +254,19 @@ export class UpdateTeamInput {
@ApiProperty()
@IsString()
updatedBy: string;
updatedBy!: string;
}
export class UpdateTeamOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
success!: boolean;
}
export class ApproveTeamJoinRequestInput {
@ApiProperty()
@IsString()
requestId: string;
requestId!: string;
@ApiProperty({ required: false })
@IsOptional()
@@ -277,13 +277,13 @@ export class ApproveTeamJoinRequestInput {
export class ApproveTeamJoinRequestOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
success!: boolean;
}
export class RejectTeamJoinRequestInput {
@ApiProperty()
@IsString()
requestId: string;
requestId!: string;
@ApiProperty({ required: false })
@IsOptional()
@@ -294,5 +294,5 @@ export class RejectTeamJoinRequestInput {
export class RejectTeamJoinRequestOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
success!: boolean;
}

View File

@@ -2,5 +2,5 @@ import { ApiProperty } from '@nestjs/swagger';
export class UpdateTeamOutputDTO {
@ApiProperty()
success: boolean;
success!: boolean;
}

View File

@@ -16,4 +16,11 @@ export class TeamMembershipPresenter implements UseCaseOutputPort<GetTeamMembers
getResponseModel(): GetTeamMembershipOutputDTO | null {
return this.result;
}
get responseModel(): GetTeamMembershipOutputDTO {
if (!this.result) {
throw new Error('Presenter not presented');
}
return this.result;
}
}

View File

@@ -1,107 +1,90 @@
import type { TeamsLeaderboardOutputPort } from '@core/racing/application/ports/output/TeamsLeaderboardOutputPort';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { GetTeamsLeaderboardResult } from '@core/racing/application/use-cases/GetTeamsLeaderboardUseCase';
import type { GetTeamsLeaderboardOutputDTO } from '../dtos/GetTeamsLeaderboardOutputDTO';
export class TeamsLeaderboardPresenter {
export class TeamsLeaderboardPresenter implements UseCaseOutputPort<GetTeamsLeaderboardResult> {
private result: GetTeamsLeaderboardOutputDTO | null = null;
reset() {
this.result = null;
}
async present(outputPort: TeamsLeaderboardOutputPort): Promise<void> {
present(result: GetTeamsLeaderboardResult): void {
this.result = {
teams: outputPort.teams.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
teams: result.items.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
recruitingCount: outputPort.recruitingCount,
recruitingCount: result.recruitingCount,
groupsBySkillLevel: {
beginner: outputPort.groupsBySkillLevel.beginner.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
beginner: result.groupsBySkillLevel.beginner.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
intermediate: outputPort.groupsBySkillLevel.intermediate.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
intermediate: result.groupsBySkillLevel.intermediate.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
advanced: outputPort.groupsBySkillLevel.advanced.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
advanced: result.groupsBySkillLevel.advanced.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
pro: outputPort.groupsBySkillLevel.pro.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
pro: result.groupsBySkillLevel.pro.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
},
topTeams: outputPort.topTeams.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
topTeams: result.topItems.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
};
}