114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
|
|
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
|
|
import { DriverService } from '@/lib/services/drivers/DriverService';
|
|
import type { DriverProfileViewModelData } from '@/lib/view-models/DriverProfileViewModel';
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
export type PageQueryResult =
|
|
| { status: 'ok'; dto: DriverProfileViewModelData }
|
|
| { status: 'notFound' }
|
|
| { status: 'redirect'; to: string }
|
|
| { status: 'error'; errorId: string };
|
|
|
|
// ============================================================================
|
|
// SERVER QUERY CLASS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* ProfilePageQuery
|
|
*
|
|
* Server-side data fetcher for the profile page.
|
|
* Returns a discriminated union with all possible page states.
|
|
* Ensures JSON-serializable DTO with no null leakage.
|
|
*/
|
|
export class ProfilePageQuery {
|
|
/**
|
|
* Execute the profile page query
|
|
*
|
|
* @param driverId - The driver ID to fetch profile for
|
|
* @returns PageQueryResult with discriminated union of states
|
|
*/
|
|
static async execute(driverId: string | null): Promise<PageQueryResult> {
|
|
// Handle missing driver ID
|
|
if (!driverId) {
|
|
return { status: 'notFound' };
|
|
}
|
|
|
|
try {
|
|
// Fetch using PageDataFetcher to avoid direct DI in page
|
|
const driverService = await PageDataFetcher.fetchManual(async () => {
|
|
const container = (await import('@/lib/di/container')).ContainerManager.getInstance().getContainer();
|
|
return container.get<DriverService>(DRIVER_SERVICE_TOKEN);
|
|
});
|
|
|
|
if (!driverService) {
|
|
return { status: 'error', errorId: 'SERVICE_UNAVAILABLE' };
|
|
}
|
|
|
|
const viewModel = await driverService.getDriverProfile(driverId);
|
|
|
|
// Convert to DTO and ensure JSON-serializable
|
|
const dto = this.toSerializableDTO(viewModel.toDTO());
|
|
|
|
if (!dto.currentDriver) {
|
|
return { status: 'notFound' };
|
|
}
|
|
|
|
return { status: 'ok', dto };
|
|
|
|
} catch (error) {
|
|
console.error('ProfilePageQuery failed:', error);
|
|
return { status: 'error', errorId: 'FETCH_FAILED' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert DTO to ensure JSON-serializability
|
|
* - Dates become ISO strings
|
|
* - Undefined becomes null
|
|
* - No Date objects remain
|
|
*/
|
|
private static toSerializableDTO(dto: DriverProfileViewModelData): DriverProfileViewModelData {
|
|
return {
|
|
currentDriver: dto.currentDriver ? {
|
|
...dto.currentDriver,
|
|
joinedAt: dto.currentDriver.joinedAt, // Already ISO string
|
|
} : null,
|
|
stats: dto.stats ? {
|
|
...dto.stats,
|
|
// Ensure all nullable numbers are properly handled
|
|
avgFinish: dto.stats.avgFinish ?? null,
|
|
bestFinish: dto.stats.bestFinish ?? null,
|
|
worstFinish: dto.stats.worstFinish ?? null,
|
|
finishRate: dto.stats.finishRate ?? null,
|
|
winRate: dto.stats.winRate ?? null,
|
|
podiumRate: dto.stats.podiumRate ?? null,
|
|
percentile: dto.stats.percentile ?? null,
|
|
rating: dto.stats.rating ?? null,
|
|
consistency: dto.stats.consistency ?? null,
|
|
overallRank: dto.stats.overallRank ?? null,
|
|
} : null,
|
|
finishDistribution: dto.finishDistribution ? { ...dto.finishDistribution } : null,
|
|
teamMemberships: dto.teamMemberships.map(m => ({
|
|
...m,
|
|
joinedAt: m.joinedAt, // Already ISO string
|
|
})),
|
|
socialSummary: {
|
|
friendsCount: dto.socialSummary.friendsCount,
|
|
friends: dto.socialSummary.friends.map(f => ({
|
|
...f,
|
|
})),
|
|
},
|
|
extendedProfile: dto.extendedProfile ? {
|
|
...dto.extendedProfile,
|
|
achievements: dto.extendedProfile.achievements.map(a => ({
|
|
...a,
|
|
earnedAt: a.earnedAt, // Already ISO string
|
|
})),
|
|
} : null,
|
|
};
|
|
}
|
|
} |