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

@@ -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;