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

@@ -13,6 +13,7 @@ import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeason
import type { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/ISeasonSponsorshipRepository';
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
import type { Logger } from '@core/shared/application/Logger';
import type { MediaResolverPort } from '@core/ports/media/MediaResolverPort';
// Import concrete in-memory implementations
import type { ILeagueWalletRepository } from "@core/racing/domain/repositories/ILeagueWalletRepository";
@@ -20,6 +21,7 @@ import type { ITransactionRepository } from "@core/racing/domain/repositories/IT
import { getLeagueScoringPresetById, listLeagueScoringPresets } from '@adapters/bootstrap/LeagueScoringPresets';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { InMemoryLeagueStandingsRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
import { MediaResolverAdapter } from '@adapters/media/MediaResolverAdapter';
// Import use cases
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
@@ -110,6 +112,8 @@ export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const LEAGUE_WALLET_REPOSITORY_TOKEN = 'ILeagueWalletRepository';
export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
export const LOGGER_TOKEN = 'Logger';
export const MEDIA_RESOLVER_TOKEN = 'MediaResolverPort';
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
export const GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE = 'GetAllLeaguesWithCapacityUseCase';
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE = 'GetAllLeaguesWithCapacityAndScoringUseCase';
@@ -177,9 +181,21 @@ export const LeagueProviders: Provider[] = [
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
{
provide: MEDIA_RESOLVER_TOKEN,
useFactory: () => new MediaResolverAdapter({}),
},
// Presenters
AllLeaguesWithCapacityPresenter,
AllLeaguesWithCapacityAndScoringPresenter,
{
provide: AllLeaguesWithCapacityAndScoringPresenter,
useFactory: (mediaResolver: MediaResolverPort) => {
const presenter = new AllLeaguesWithCapacityAndScoringPresenter();
presenter.setMediaResolver(mediaResolver);
return presenter;
},
inject: [MEDIA_RESOLVER_TOKEN],
},
ApproveLeagueJoinRequestPresenter,
CreateLeaguePresenter,
GetLeagueAdminPermissionsPresenter,

View File

@@ -319,6 +319,9 @@ export class LeagueService {
throw new Error(err.code);
}
// The use case calls presenter.present() internally
// The presenter now handles logo resolution synchronously
// Just get the view model which contains the resolved logo URLs
return this.allLeaguesWithCapacityAndScoringPresenter.getViewModel();
}

View File

@@ -11,6 +11,7 @@ export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const LEAGUE_WALLET_REPOSITORY_TOKEN = 'ILeagueWalletRepository';
export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
export const LOGGER_TOKEN = 'Logger';
export const MEDIA_RESOLVER_TOKEN = 'MediaResolverPort';
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
export const GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE = 'GetAllLeaguesWithCapacityUseCase';

View File

@@ -116,6 +116,11 @@ export class LeagueWithCapacityAndScoringDTO {
@IsOptional()
@IsString()
timingSummary?: string;
@ApiProperty({ required: false, nullable: true })
@IsOptional()
@IsString()
logoUrl?: string | null;
}
export class AllLeaguesWithCapacityAndScoringDTO {

View File

@@ -18,7 +18,7 @@ export class LeagueSummaryDTO {
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
logoUrl?: string;
logoUrl!: string | null;
@ApiProperty({ nullable: true })
@IsOptional()

View File

@@ -4,52 +4,92 @@ import type {
AllLeaguesWithCapacityAndScoringDTO,
LeagueWithCapacityAndScoringDTO,
} from '../dtos/AllLeaguesWithCapacityAndScoringDTO';
import type { MediaResolverPort } from '@core/ports/media/MediaResolverPort';
import { MediaReference } from '@core/domain/media/MediaReference';
export class AllLeaguesWithCapacityAndScoringPresenter
implements UseCaseOutputPort<GetAllLeaguesWithCapacityAndScoringResult>
{
private result: AllLeaguesWithCapacityAndScoringDTO | null = null;
private mediaResolver?: MediaResolverPort;
present(result: GetAllLeaguesWithCapacityAndScoringResult): void {
const leagues: LeagueWithCapacityAndScoringDTO[] = result.leagues.map((summary) => {
const timingSummary = summary.preset
? formatTimingSummary(summary.preset.defaultTimings.mainRaceMinutes)
: undefined;
setMediaResolver(resolver: MediaResolverPort): void {
this.mediaResolver = resolver;
}
return {
id: summary.league.id.toString(),
name: summary.league.name.toString(),
description: summary.league.description?.toString() || '',
ownerId: summary.league.ownerId.toString(),
createdAt: summary.league.createdAt.toDate().toISOString(),
settings: {
maxDrivers: summary.maxDrivers,
...(summary.league.settings.sessionDuration !== undefined
? { sessionDuration: summary.league.settings.sessionDuration }
: {}),
...(summary.league.settings.qualifyingFormat !== undefined
? { qualifyingFormat: summary.league.settings.qualifyingFormat.toString() }
: {}),
},
usedSlots: summary.currentDrivers,
...(summary.league.category ? { category: summary.league.category } : {}),
...mapSocialLinks(summary.league.socialLinks),
...(summary.scoringConfig && summary.game && summary.preset
? {
scoring: {
gameId: summary.game.id.toString(),
gameName: summary.game.name.toString(),
primaryChampionshipType: summary.preset.primaryChampionshipType,
scoringPresetId: summary.scoringConfig.scoringPresetId?.toString() ?? 'custom',
scoringPresetName: summary.preset.name,
dropPolicySummary: summary.preset.dropPolicySummary,
scoringPatternSummary: summary.preset.sessionSummary,
},
async present(result: GetAllLeaguesWithCapacityAndScoringResult): Promise<void> {
const leagues: LeagueWithCapacityAndScoringDTO[] = await Promise.all(
result.leagues.map(async (summary) => {
const timingSummary = summary.preset
? formatTimingSummary(summary.preset.defaultTimings.mainRaceMinutes)
: undefined;
// Resolve logo URL
let logoUrl: string | null | undefined;
if (summary.league.logoRef) {
const ref = summary.league.logoRef instanceof MediaReference
? summary.league.logoRef
: MediaReference.fromJSON(summary.league.logoRef);
if (this.mediaResolver) {
logoUrl = await this.mediaResolver.resolve(ref);
} else {
// Fallback to manual construction
if (ref.type === 'generated' && ref.generationRequestId) {
const requestId = ref.generationRequestId;
const firstHyphenIndex = requestId.indexOf('-');
if (firstHyphenIndex !== -1) {
const type = requestId.substring(0, firstHyphenIndex);
const id = requestId.substring(firstHyphenIndex + 1);
if (type === 'league') {
logoUrl = `/media/leagues/${id}/logo`;
}
}
} else if (ref.type === 'uploaded' && ref.mediaId) {
logoUrl = `/media/uploaded/${ref.mediaId}`;
} else if (ref.type === 'system-default') {
logoUrl = null;
}
: {}),
}
}
return {
id: summary.league.id.toString(),
name: summary.league.name.toString(),
description: summary.league.description?.toString() || '',
ownerId: summary.league.ownerId.toString(),
createdAt: summary.league.createdAt.toDate().toISOString(),
settings: {
maxDrivers: summary.maxDrivers,
...(summary.league.settings.sessionDuration !== undefined
? { sessionDuration: summary.league.settings.sessionDuration }
: {}),
...(summary.league.settings.qualifyingFormat !== undefined
? { qualifyingFormat: summary.league.settings.qualifyingFormat.toString() }
: {}),
},
usedSlots: summary.currentDrivers,
...(summary.league.category ? { category: summary.league.category } : {}),
...mapSocialLinks(summary.league.socialLinks),
...(summary.scoringConfig && summary.game && summary.preset
? {
scoring: {
gameId: summary.game.id.toString(),
gameName: summary.game.name.toString(),
primaryChampionshipType: summary.preset.primaryChampionshipType,
scoringPresetId: summary.scoringConfig.scoringPresetId?.toString() ?? 'custom',
scoringPresetName: summary.preset.name,
dropPolicySummary: summary.preset.dropPolicySummary,
scoringPatternSummary: summary.preset.sessionSummary,
},
}
: {}),
...(timingSummary ? { timingSummary } : {}),
};
});
...(logoUrl !== undefined ? { logoUrl } : {}),
};
})
);
this.result = {
leagues,