harden media

This commit is contained in:
2025-12-31 15:39:28 +01:00
parent 92226800df
commit 8260bf7baf
413 changed files with 8361 additions and 1544 deletions

View File

@@ -111,7 +111,7 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase {
});
}
this.output.present({ leagues: enrichedLeagues });
await this.output.present({ leagues: enrichedLeagues });
return Result.ok(undefined);
} catch (error: unknown) {

View File

@@ -3,7 +3,6 @@ import { GetAllTeamsUseCase, type GetAllTeamsInput, type GetAllTeamsResult } fro
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import type { ITeamStatsRepository } from '../../domain/repositories/ITeamStatsRepository';
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
@@ -55,15 +54,6 @@ describe('GetAllTeamsUseCase', () => {
existsByRaceId: vi.fn(),
};
const mockMediaRepo: IMediaRepository = {
getDriverAvatar: vi.fn(),
getTeamLogo: vi.fn(),
getTrackImage: vi.fn(),
getCategoryIcon: vi.fn(),
getSponsorLogo: vi.fn(),
clear: vi.fn(),
};
const mockLogger: Logger = {
debug: vi.fn(),
info: vi.fn(),
@@ -85,7 +75,6 @@ describe('GetAllTeamsUseCase', () => {
mockTeamRepo,
mockTeamMembershipRepo,
mockTeamStatsRepo,
mockMediaRepo,
mockResultRepo,
mockLogger,
output,
@@ -99,6 +88,9 @@ describe('GetAllTeamsUseCase', () => {
ownerId: { toString: () => 'owner1' },
leagues: [{ toString: () => 'league1' }],
createdAt: { toDate: () => new Date('2023-01-01T00:00:00Z') },
logoRef: { toJSON: () => ({ type: 'generated', generationRequestId: 'team-team1' }) },
category: undefined,
isRecruiting: false,
};
const team2 = {
id: 'team2',
@@ -108,11 +100,39 @@ describe('GetAllTeamsUseCase', () => {
ownerId: { toString: () => 'owner2' },
leagues: [{ toString: () => 'league2' }],
createdAt: { toDate: () => new Date('2023-01-02T00:00:00Z') },
logoRef: { toJSON: () => ({ type: 'generated', generationRequestId: 'team-team2' }) },
category: undefined,
isRecruiting: true,
};
mockTeamFindAll.mockResolvedValue([team1, team2]);
mockTeamMembershipCountByTeamId.mockImplementation((id: string) => Promise.resolve(id === 'team1' ? 5 : 3));
// Provide precomputed stats so the use case doesn't compute from results.
(mockTeamStatsRepo.getTeamStats as unknown as Mock).mockImplementation((teamId: string) =>
Promise.resolve(
teamId === 'team1'
? {
performanceLevel: 'intermediate',
specialization: 'mixed',
region: 'EU',
languages: ['en'],
totalWins: 2,
totalRaces: 10,
rating: 1200,
}
: {
performanceLevel: 'advanced',
specialization: 'mixed',
region: 'US',
languages: ['en', 'de'],
totalWins: 5,
totalRaces: 20,
rating: 1400,
},
),
);
const result = await useCase.execute({} as GetAllTeamsInput);
expect(result.isOk()).toBe(true);
@@ -132,6 +152,17 @@ describe('GetAllTeamsUseCase', () => {
leagues: ['league1'],
createdAt: new Date('2023-01-01T00:00:00Z'),
memberCount: 5,
totalWins: 2,
totalRaces: 10,
performanceLevel: 'intermediate',
specialization: 'mixed',
region: 'EU',
languages: ['en'],
logoRef: team1.logoRef,
logoUrl: null,
rating: 1200,
category: undefined,
isRecruiting: false,
},
{
id: 'team2',
@@ -142,6 +173,17 @@ describe('GetAllTeamsUseCase', () => {
leagues: ['league2'],
createdAt: new Date('2023-01-02T00:00:00Z'),
memberCount: 3,
totalWins: 5,
totalRaces: 20,
performanceLevel: 'advanced',
specialization: 'mixed',
region: 'US',
languages: ['en', 'de'],
logoRef: team2.logoRef,
logoUrl: null,
rating: 1400,
category: undefined,
isRecruiting: true,
},
],
totalCount: 2,
@@ -153,7 +195,6 @@ describe('GetAllTeamsUseCase', () => {
mockTeamRepo,
mockTeamMembershipRepo,
mockTeamStatsRepo,
mockMediaRepo,
mockResultRepo,
mockLogger,
output,
@@ -180,7 +221,6 @@ describe('GetAllTeamsUseCase', () => {
mockTeamRepo,
mockTeamMembershipRepo,
mockTeamStatsRepo,
mockMediaRepo,
mockResultRepo,
mockLogger,
output,

View File

@@ -1,12 +1,12 @@
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import type { ITeamStatsRepository } from '../../domain/repositories/ITeamStatsRepository';
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { MediaReference } from '@core/domain/media/MediaReference';
export type GetAllTeamsInput = {};
@@ -27,7 +27,8 @@ export interface TeamSummary {
specialization?: string;
region?: string;
languages?: string[];
logoUrl?: string;
logoRef?: MediaReference;
logoUrl?: string | null;
rating?: number;
category?: string | undefined;
isRecruiting: boolean;
@@ -46,7 +47,6 @@ export class GetAllTeamsUseCase {
private readonly teamRepository: ITeamRepository,
private readonly teamMembershipRepository: ITeamMembershipRepository,
private readonly teamStatsRepository: ITeamStatsRepository,
private readonly mediaRepository: IMediaRepository,
private readonly resultRepository: IResultRepository,
private readonly logger: Logger,
private readonly output: UseCaseOutputPort<GetAllTeamsResult>,
@@ -64,7 +64,9 @@ export class GetAllTeamsUseCase {
const enrichedTeams: TeamSummary[] = await Promise.all(
teams.map(async (team) => {
const memberCount = await this.teamMembershipRepository.countByTeamId(team.id);
const logoUrl = await this.mediaRepository.getTeamLogo(team.id);
// Get logo reference from team entity
const logoRef = team.logoRef;
// Try to get pre-computed stats first
let stats = await this.teamStatsRepository.getTeamStats(team.id);
@@ -95,7 +97,6 @@ export class GetAllTeamsUseCase {
else performanceLevel = 'beginner';
stats = {
logoUrl: await this.mediaRepository.getTeamLogo(team.id) || '',
performanceLevel,
specialization: 'mixed',
region: 'International',
@@ -121,7 +122,8 @@ export class GetAllTeamsUseCase {
specialization: stats!.specialization,
region: stats!.region,
languages: stats!.languages,
logoUrl: logoUrl || stats!.logoUrl,
logoRef: logoRef,
logoUrl: null, // Will be resolved by presenter
rating: stats!.rating,
category: team.category,
isRecruiting: team.isRecruiting,

View File

@@ -33,7 +33,6 @@ describe('GetDriversLeaderboardUseCase', () => {
getDriverStats: mockDriverStatsGetDriverStats,
};
const mockGetDriverAvatar = vi.fn();
const mockLogger: Logger = {
debug: vi.fn(),
info: vi.fn(),
@@ -50,13 +49,22 @@ describe('GetDriversLeaderboardUseCase', () => {
mockDriverRepo,
mockRankingUseCase,
mockDriverStatsUseCase,
mockGetDriverAvatar,
mockLogger,
mockOutput,
);
const driver1 = { id: 'driver1', name: { value: 'Driver One' }, country: { value: 'US' } };
const driver2 = { id: 'driver2', name: { value: 'Driver Two' }, country: { value: 'US' } };
const driver1 = {
id: 'driver1',
name: { value: 'Driver One' },
country: { value: 'US' },
avatarRef: { type: 'system-default', variant: 'avatar' }
};
const driver2 = {
id: 'driver2',
name: { value: 'Driver Two' },
country: { value: 'US' },
avatarRef: { type: 'system-default', variant: 'avatar' }
};
const rankings = [
{ driverId: 'driver1', rating: 2500, overallRank: 1 },
{ driverId: 'driver2', rating: 2400, overallRank: 2 },
@@ -71,11 +79,6 @@ describe('GetDriversLeaderboardUseCase', () => {
if (id === 'driver2') return stats2;
return null;
});
mockGetDriverAvatar.mockImplementation((driverId: string) => {
if (driverId === 'driver1') return Promise.resolve('avatar-driver1');
if (driverId === 'driver2') return Promise.resolve('avatar-driver2');
return Promise.resolve('avatar-default');
});
const input: GetDriversLeaderboardInput = { leagueId: 'league-1' };
@@ -94,7 +97,7 @@ describe('GetDriversLeaderboardUseCase', () => {
podiums: 7,
isActive: true,
rank: 1,
avatarUrl: 'avatar-driver1',
avatarRef: driver1.avatarRef,
}),
expect.objectContaining({
driver: driver2,
@@ -105,7 +108,7 @@ describe('GetDriversLeaderboardUseCase', () => {
podiums: 4,
isActive: true,
rank: 2,
avatarUrl: 'avatar-driver2',
avatarRef: driver2.avatarRef,
}),
],
totalRaces: 18,
@@ -119,7 +122,6 @@ describe('GetDriversLeaderboardUseCase', () => {
mockDriverRepo,
mockRankingUseCase,
mockDriverStatsUseCase,
mockGetDriverAvatar,
mockLogger,
mockOutput,
);
@@ -146,18 +148,21 @@ describe('GetDriversLeaderboardUseCase', () => {
mockDriverRepo,
mockRankingUseCase,
mockDriverStatsUseCase,
mockGetDriverAvatar,
mockLogger,
mockOutput,
);
const driver1 = { id: 'driver1', name: { value: 'Driver One' }, country: { value: 'US' } };
const driver1 = {
id: 'driver1',
name: { value: 'Driver One' },
country: { value: 'US' },
avatarRef: { type: 'system-default', variant: 'avatar' }
};
const rankings = [{ driverId: 'driver1', rating: 2500, overallRank: 1 }];
mockDriverFindAll.mockResolvedValue([driver1]);
mockRankingGetAllDriverRankings.mockReturnValue(rankings);
mockDriverStatsGetDriverStats.mockReturnValue(null);
mockGetDriverAvatar.mockResolvedValue('avatar-driver1');
const input: GetDriversLeaderboardInput = { leagueId: 'league-1' };
@@ -176,7 +181,7 @@ describe('GetDriversLeaderboardUseCase', () => {
podiums: 0,
isActive: false,
rank: 1,
avatarUrl: 'avatar-driver1',
avatarRef: driver1.avatarRef,
}),
],
totalRaces: 0,
@@ -190,7 +195,6 @@ describe('GetDriversLeaderboardUseCase', () => {
mockDriverRepo,
mockRankingUseCase,
mockDriverStatsUseCase,
mockGetDriverAvatar,
mockLogger,
mockOutput,
);

View File

@@ -7,6 +7,7 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { IDriverStatsUseCase } from './IDriverStatsUseCase';
import type { IRankingUseCase } from './IRankingUseCase';
import { SkillLevelService, type SkillLevel } from '../../domain/services/SkillLevelService';
import { MediaReference } from '@core/domain/media/MediaReference';
export type GetDriversLeaderboardInput = {
leagueId?: string;
@@ -23,7 +24,7 @@ export interface DriverLeaderboardItem {
podiums: number;
isActive: boolean;
rank: number;
avatarUrl?: string;
avatarRef?: MediaReference;
}
export interface GetDriversLeaderboardResult {
@@ -47,7 +48,6 @@ export class GetDriversLeaderboardUseCase implements UseCase<GetDriversLeaderboa
private readonly driverRepository: IDriverRepository,
private readonly rankingUseCase: IRankingUseCase,
private readonly driverStatsUseCase: IDriverStatsUseCase,
private readonly getDriverAvatar: (driverId: string) => Promise<string | undefined>,
private readonly logger: Logger,
private readonly output: UseCaseOutputPort<GetDriversLeaderboardResult>,
) {}
@@ -66,12 +66,6 @@ export class GetDriversLeaderboardUseCase implements UseCase<GetDriversLeaderboa
const drivers = await this.driverRepository.findAll();
const rankings = await this.rankingUseCase.getAllDriverRankings();
const avatarUrls: Record<string, string | undefined> = {};
for (const driver of drivers) {
avatarUrls[driver.id] = await this.getDriverAvatar(driver.id);
}
// Get stats for all drivers
const statsPromises = drivers.map(driver =>
this.driverStatsUseCase.getDriverStats(driver.id)
@@ -90,7 +84,6 @@ export class GetDriversLeaderboardUseCase implements UseCase<GetDriversLeaderboa
const rating = ranking?.rating ?? 0;
const racesCompleted = stats?.totalRaces ?? 0;
const skillLevel: SkillLevel = SkillLevelService.getSkillLevel(rating);
const avatarUrl = avatarUrls[driver.id];
return {
driver,
@@ -101,7 +94,7 @@ export class GetDriversLeaderboardUseCase implements UseCase<GetDriversLeaderboa
podiums: stats?.podiums ?? 0,
isActive: racesCompleted > 0,
rank: ranking?.overallRank ?? 0,
...(avatarUrl !== undefined ? { avatarUrl } : {}),
avatarRef: driver.avatarRef,
};
});
@@ -132,4 +125,4 @@ export class GetDriversLeaderboardUseCase implements UseCase<GetDriversLeaderboa
});
}
}
}
}

View File

@@ -12,6 +12,7 @@ import { DriverName } from '../value-objects/driver/DriverName';
import { CountryCode } from '../value-objects/CountryCode';
import { DriverBio } from '../value-objects/driver/DriverBio';
import { JoinedAt } from '../value-objects/JoinedAt';
import { MediaReference } from '@core/domain/media/MediaReference';
export class Driver implements IEntity<string> {
readonly id: string;
@@ -21,6 +22,7 @@ export class Driver implements IEntity<string> {
readonly bio: DriverBio | undefined;
readonly joinedAt: JoinedAt;
readonly category: string | undefined;
readonly avatarRef: MediaReference;
private constructor(props: {
id: string;
@@ -30,6 +32,7 @@ export class Driver implements IEntity<string> {
bio?: DriverBio;
joinedAt: JoinedAt;
category?: string;
avatarRef: MediaReference;
}) {
this.id = props.id;
this.iracingId = props.iracingId;
@@ -38,6 +41,7 @@ export class Driver implements IEntity<string> {
this.bio = props.bio;
this.joinedAt = props.joinedAt;
this.category = props.category;
this.avatarRef = props.avatarRef;
}
/**
@@ -51,6 +55,7 @@ export class Driver implements IEntity<string> {
bio?: string;
joinedAt?: Date;
category?: string;
avatarRef?: MediaReference;
}): Driver {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('Driver ID is required');
@@ -64,12 +69,14 @@ export class Driver implements IEntity<string> {
bio?: DriverBio;
joinedAt: JoinedAt;
category?: string;
avatarRef: MediaReference;
} = {
id: props.id,
iracingId: IRacingId.create(props.iracingId),
name: DriverName.create(props.name),
country: CountryCode.create(props.country),
joinedAt: JoinedAt.create(props.joinedAt ?? new Date()),
avatarRef: props.avatarRef ?? MediaReference.createSystemDefault('avatar'),
};
if (props.bio !== undefined) {
@@ -90,6 +97,7 @@ export class Driver implements IEntity<string> {
bio?: string;
joinedAt: Date;
category?: string;
avatarRef?: MediaReference;
}): Driver {
const driverProps: {
id: string;
@@ -99,12 +107,14 @@ export class Driver implements IEntity<string> {
bio?: DriverBio;
joinedAt: JoinedAt;
category?: string;
avatarRef: MediaReference;
} = {
id: props.id,
iracingId: IRacingId.create(props.iracingId),
name: DriverName.create(props.name),
country: CountryCode.create(props.country),
joinedAt: JoinedAt.create(props.joinedAt),
avatarRef: props.avatarRef ?? MediaReference.createSystemDefault('avatar'),
};
if (props.bio !== undefined) {
@@ -125,11 +135,13 @@ export class Driver implements IEntity<string> {
country: string;
bio: string | undefined;
category: string | undefined;
avatarRef: MediaReference;
}>): Driver {
const nextName = 'name' in props ? DriverName.create(props.name!) : this.name;
const nextCountry = 'country' in props ? CountryCode.create(props.country!) : this.country;
const nextBio = 'bio' in props ? (props.bio ? DriverBio.create(props.bio) : undefined) : this.bio;
const nextCategory = 'category' in props ? props.category : this.category;
const nextAvatarRef = 'avatarRef' in props ? props.avatarRef! : this.avatarRef;
const driverProps: {
id: string;
@@ -139,12 +151,14 @@ export class Driver implements IEntity<string> {
bio?: DriverBio;
joinedAt: JoinedAt;
category?: string;
avatarRef: MediaReference;
} = {
id: this.id,
iracingId: this.iracingId,
name: nextName,
country: nextCountry,
joinedAt: this.joinedAt,
avatarRef: nextAvatarRef,
};
if (nextBio !== undefined) {

View File

@@ -17,6 +17,7 @@ import { ParticipantCount } from '../value-objects/ParticipantCount';
import { MaxParticipants } from '../value-objects/MaxParticipants';
import { SessionDuration } from '../value-objects/SessionDuration';
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
import { MediaReference } from '@core/domain/media/MediaReference';
/**
* Stewarding decision mode for protests
@@ -99,6 +100,7 @@ export class League implements IEntity<LeagueId> {
readonly category?: string | undefined;
readonly createdAt: LeagueCreatedAt;
readonly socialLinks: LeagueSocialLinks | undefined;
readonly logoRef: MediaReference;
// Domain state for business rule enforcement
private readonly _participantCount: ParticipantCount;
@@ -115,6 +117,7 @@ export class League implements IEntity<LeagueId> {
socialLinks?: LeagueSocialLinks;
participantCount: ParticipantCount;
visibility: LeagueVisibility;
logoRef: MediaReference;
}) {
this.id = props.id;
this.name = props.name;
@@ -126,6 +129,7 @@ export class League implements IEntity<LeagueId> {
this.socialLinks = props.socialLinks;
this._participantCount = props.participantCount;
this._visibility = props.visibility;
this.logoRef = props.logoRef;
}
/**
@@ -146,6 +150,7 @@ export class League implements IEntity<LeagueId> {
websiteUrl?: string;
};
participantCount?: number;
logoRef?: MediaReference;
}): League {
// Validate required fields
if (!props.id || props.id.trim().length === 0) {
@@ -254,6 +259,7 @@ export class League implements IEntity<LeagueId> {
...(socialLinks !== undefined ? { socialLinks } : {}),
participantCount,
visibility,
logoRef: props.logoRef ?? MediaReference.createSystemDefault('logo'),
});
}
@@ -271,6 +277,7 @@ export class League implements IEntity<LeagueId> {
youtubeUrl?: string;
websiteUrl?: string;
};
logoRef?: MediaReference;
}): League {
const id = LeagueId.create(props.id);
const name = LeagueName.create(props.name);
@@ -297,6 +304,7 @@ export class League implements IEntity<LeagueId> {
...(socialLinks !== undefined ? { socialLinks } : {}),
participantCount,
visibility,
logoRef: props.logoRef ?? MediaReference.createSystemDefault('logo'),
});
}
@@ -356,11 +364,13 @@ export class League implements IEntity<LeagueId> {
youtubeUrl?: string;
websiteUrl?: string;
};
logoRef: MediaReference;
}>): League {
const name = props.name ? LeagueName.create(props.name) : this.name;
const description = props.description ? LeagueDescription.create(props.description) : this.description;
const ownerId = props.ownerId ? LeagueOwnerId.create(props.ownerId) : this.ownerId;
const socialLinks = props.socialLinks ? LeagueSocialLinks.create(props.socialLinks) : this.socialLinks;
const logoRef = 'logoRef' in props ? props.logoRef! : this.logoRef;
// If settings are being updated, validate them
let newSettings = props.settings ?? this.settings;
@@ -427,6 +437,7 @@ export class League implements IEntity<LeagueId> {
...(socialLinks !== undefined ? { socialLinks } : {}),
participantCount: this._participantCount,
visibility: this._visibility,
logoRef: logoRef,
});
}
@@ -461,6 +472,7 @@ export class League implements IEntity<LeagueId> {
...(this.socialLinks !== undefined ? { socialLinks: this.socialLinks } : {}),
participantCount: newCount,
visibility: this._visibility,
logoRef: this.logoRef,
});
}
@@ -485,6 +497,7 @@ export class League implements IEntity<LeagueId> {
...(this.socialLinks !== undefined ? { socialLinks: this.socialLinks } : {}),
participantCount: newCount,
visibility: this._visibility,
logoRef: this.logoRef,
});
}

View File

@@ -14,6 +14,7 @@ import { TeamDescription } from '../value-objects/TeamDescription';
import { DriverId } from './DriverId';
import { LeagueId } from './LeagueId';
import { TeamCreatedAt } from '../value-objects/TeamCreatedAt';
import { MediaReference } from '@core/domain/media/MediaReference';
export class Team implements IEntity<string> {
readonly id: string;
@@ -25,6 +26,7 @@ export class Team implements IEntity<string> {
readonly category: string | undefined;
readonly isRecruiting: boolean;
readonly createdAt: TeamCreatedAt;
readonly logoRef: MediaReference;
private constructor(props: {
id: string;
@@ -36,6 +38,7 @@ export class Team implements IEntity<string> {
category: string | undefined;
isRecruiting: boolean;
createdAt: TeamCreatedAt;
logoRef: MediaReference;
}) {
this.id = props.id;
this.name = props.name;
@@ -46,6 +49,7 @@ export class Team implements IEntity<string> {
this.category = props.category;
this.isRecruiting = props.isRecruiting;
this.createdAt = props.createdAt;
this.logoRef = props.logoRef;
}
/**
@@ -61,6 +65,7 @@ export class Team implements IEntity<string> {
category?: string;
isRecruiting?: boolean;
createdAt?: Date;
logoRef?: MediaReference;
}): Team {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('Team ID is required');
@@ -80,6 +85,7 @@ export class Team implements IEntity<string> {
category: props.category,
isRecruiting: props.isRecruiting ?? false,
createdAt: TeamCreatedAt.create(props.createdAt ?? new Date()),
logoRef: props.logoRef ?? MediaReference.createSystemDefault('logo'),
});
}
@@ -93,6 +99,7 @@ export class Team implements IEntity<string> {
category?: string;
isRecruiting: boolean;
createdAt: Date;
logoRef?: MediaReference;
}): Team {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('Team ID is required');
@@ -112,6 +119,7 @@ export class Team implements IEntity<string> {
category: props.category,
isRecruiting: props.isRecruiting,
createdAt: TeamCreatedAt.create(props.createdAt),
logoRef: props.logoRef ?? MediaReference.createSystemDefault('logo'),
});
}
@@ -126,6 +134,7 @@ export class Team implements IEntity<string> {
leagues: string[];
category: string | undefined;
isRecruiting: boolean;
logoRef: MediaReference;
}>): Team {
const nextName = 'name' in props ? TeamName.create(props.name!) : this.name;
const nextTag = 'tag' in props ? TeamTag.create(props.tag!) : this.tag;
@@ -134,6 +143,7 @@ export class Team implements IEntity<string> {
const nextLeagues = 'leagues' in props ? props.leagues!.map(leagueId => LeagueId.create(leagueId)) : this.leagues;
const nextCategory = 'category' in props ? props.category : this.category;
const nextIsRecruiting = 'isRecruiting' in props ? props.isRecruiting! : this.isRecruiting;
const nextLogoRef = 'logoRef' in props ? props.logoRef! : this.logoRef;
return new Team({
id: this.id,
@@ -145,6 +155,7 @@ export class Team implements IEntity<string> {
category: nextCategory,
isRecruiting: nextIsRecruiting,
createdAt: this.createdAt,
logoRef: nextLogoRef,
});
}

View File

@@ -1,8 +1,8 @@
/**
* Application Port: IMediaRepository
*
* Repository interface for static media assets (logos, images, icons).
* Handles frontend assets like team logos, driver avatars, etc.
* Repository interface for media assets (logos, avatars).
* Handles frontend assets like team logos and driver avatars.
*/
export interface IMediaRepository {
@@ -17,22 +17,17 @@ export interface IMediaRepository {
getTeamLogo(teamId: string): Promise<string | null>;
/**
* Get track image URL
* Get league logo URL
*/
getTrackImage(trackId: string): Promise<string | null>;
getLeagueLogo(leagueId: string): Promise<string | null>;
/**
* Get category icon URL
* Get league cover URL
*/
getCategoryIcon(categoryId: string): Promise<string | null>;
/**
* Get sponsor logo URL
*/
getSponsorLogo(sponsorId: string): Promise<string | null>;
getLeagueCover(leagueId: string): Promise<string | null>;
/**
* Clear all media data (for reseeding)
*/
clear(): Promise<void>;
}
}

View File

@@ -6,7 +6,6 @@
*/
export interface TeamStats {
logoUrl: string;
performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
specialization: 'endurance' | 'sprint' | 'mixed';
region: string;