seed data

This commit is contained in:
2025-12-30 18:33:15 +01:00
parent 83371ea839
commit 92226800df
306 changed files with 1753 additions and 501 deletions

View File

@@ -78,6 +78,10 @@ export class SeedRacingData {
return process.env.DATABASE_URL ? 'postgres' : 'inmemory';
}
private getMediaBaseUrl(): string {
return process.env.NODE_ENV === 'development' ? 'http://localhost:3001' : 'https://api.gridpilot.io';
}
async execute(): Promise<void> {
const existingDrivers = await this.seedDeps.driverRepository.findAll();
const persistence = this.getApiPersistence();
@@ -428,7 +432,7 @@ export class SeedRacingData {
private calculateTeamStats(team: Team, results: Result[], drivers: Driver[]): TeamStats {
const wins = results.filter(r => r.position.toNumber() === 1).length;
const totalRaces = results.length;
// Calculate rating
const baseRating = 1000;
const winBonus = wins * 50;
@@ -462,7 +466,7 @@ export class SeedRacingData {
})));
return {
logoUrl: `https://api.gridpilot.io/media/team/${team.id}/logo.png`,
logoUrl: `${this.getMediaBaseUrl()}/api/media/teams/${team.id}/logo`,
performanceLevel,
specialization,
region,
@@ -482,21 +486,22 @@ export class SeedRacingData {
}
private async seedMediaAssets(seed: any): Promise<void> {
// Seed driver avatars
const baseUrl = this.getMediaBaseUrl();
// Seed driver avatars using static files
for (const driver of seed.drivers) {
const avatarUrl = `https://api.gridpilot.io/media/driver/${driver.id}/avatar.png`;
// Type assertion to access the helper method
const avatarUrl = this.getDriverAvatarUrl(driver.id);
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setDriverAvatar) {
mediaRepo.setDriverAvatar(driver.id, avatarUrl);
}
}
// Seed team logos
// Seed team logos using API routes
for (const team of seed.teams) {
const logoUrl = `https://api.gridpilot.io/media/team/${team.id}/logo.png`;
const logoUrl = `${baseUrl}/api/media/teams/${team.id}/logo`;
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setTeamLogo) {
mediaRepo.setTeamLogo(team.id, logoUrl);
@@ -505,8 +510,8 @@ export class SeedRacingData {
// Seed track images
for (const track of seed.tracks || []) {
const trackImageUrl = `https://api.gridpilot.io/media/track/${track.id}/image.png`;
const trackImageUrl = `${baseUrl}/api/media/tracks/${track.id}/image`;
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setTrackImage) {
mediaRepo.setTrackImage(track.id, trackImageUrl);
@@ -516,8 +521,8 @@ export class SeedRacingData {
// Seed category icons (if categories exist)
const categories = ['beginner', 'intermediate', 'advanced', 'pro', 'endurance', 'sprint'];
for (const category of categories) {
const iconUrl = `https://api.gridpilot.io/media/category/${category}/icon.png`;
const iconUrl = `${baseUrl}/api/media/categories/${category}/icon`;
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setCategoryIcon) {
mediaRepo.setCategoryIcon(category, iconUrl);
@@ -526,8 +531,8 @@ export class SeedRacingData {
// Seed sponsor logos
for (const sponsor of seed.sponsors || []) {
const logoUrl = `https://api.gridpilot.io/media/sponsor/${sponsor.id}/logo.png`;
const logoUrl = `${baseUrl}/api/media/sponsors/${sponsor.id}/logo`;
const mediaRepo = this.seedDeps.mediaRepository as any;
if (mediaRepo.setSponsorLogo) {
mediaRepo.setSponsorLogo(sponsor.id, logoUrl);
@@ -537,6 +542,48 @@ export class SeedRacingData {
this.logger.info(`[Bootstrap] Seeded media assets for ${seed.drivers.length} drivers, ${seed.teams.length} teams`);
}
/**
* Get deterministic avatar URL for a driver based on their ID
* Uses static files from the website public directory
*/
private getDriverAvatarUrl(driverId: string): string {
// Deterministic selection based on driver ID
const numericSuffixMatch = driverId.match(/(\d+)$/);
let useFemale = false;
let useNeutral = false;
if (numericSuffixMatch && numericSuffixMatch[1]) {
const numericSuffix = parseInt(numericSuffixMatch[1], 10);
// 40% female, 40% male, 20% neutral
if (numericSuffix % 5 === 0) {
useNeutral = true;
} else if (numericSuffix % 2 === 0) {
useFemale = true;
}
} else {
// Fallback hash
let hash = 0;
for (let i = 0; i < driverId.length; i++) {
hash = (hash * 31 + driverId.charCodeAt(i)) | 0;
}
const hashValue = Math.abs(hash);
if (hashValue % 5 === 0) {
useNeutral = true;
} else if (hashValue % 2 === 0) {
useFemale = true;
}
}
// Return static file paths that Next.js can serve
if (useNeutral) {
return '/images/avatars/neutral-default-avatar.jpeg';
} else if (useFemale) {
return '/images/avatars/female-default-avatar.jpeg';
} else {
return '/images/avatars/male-default-avatar.jpg';
}
}
private async clearExistingRacingData(): Promise<void> {
// Get all existing drivers
const drivers = await this.seedDeps.driverRepository.findAll();

View File

@@ -25,8 +25,51 @@ export class RacingDriverFactory {
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
) {}
/**
* Get deterministic avatar URL for a driver based on their ID
* Uses static files from the website public directory
*/
getDriverAvatarUrl(driverId: string): string {
// Deterministic selection based on driver ID
const numericSuffixMatch = driverId.match(/(\d+)$/);
let useFemale = false;
let useNeutral = false;
if (numericSuffixMatch && numericSuffixMatch[1]) {
const numericSuffix = parseInt(numericSuffixMatch[1], 10);
// 40% female, 40% male, 20% neutral
if (numericSuffix % 5 === 0) {
useNeutral = true;
} else if (numericSuffix % 2 === 0) {
useFemale = true;
}
} else {
// Fallback hash
let hash = 0;
for (let i = 0; i < driverId.length; i++) {
hash = (hash * 31 + driverId.charCodeAt(i)) | 0;
}
const hashValue = Math.abs(hash);
if (hashValue % 5 === 0) {
useNeutral = true;
} else if (hashValue % 2 === 0) {
useFemale = true;
}
}
// Return static file paths that Next.js can serve
if (useNeutral) {
return '/images/avatars/neutral-default-avatar.jpeg';
} else if (useFemale) {
return '/images/avatars/female-default-avatar.jpeg';
} else {
return '/images/avatars/male-default-avatar.jpg';
}
}
create(): Driver[] {
const countries = ['DE', 'NL', 'FR', 'GB', 'US', 'CA', 'SE', 'NO', 'IT', 'ES', 'AU', 'BR', 'JP', 'KR', 'RU', 'PL', 'CZ', 'HU', 'AT', 'CH'] as const;
const categories = ['beginner', 'intermediate', 'advanced', 'pro', 'endurance', 'sprint'];
return Array.from({ length: this.driverCount }, (_, idx) => {
const i = idx + 1;
@@ -53,6 +96,9 @@ export class RacingDriverFactory {
joinedAt = faker.date.past({ years: 2, refDate: this.baseDate });
}
// Assign category - use all available categories
const category = faker.helpers.arrayElement(categories);
const driverData: {
id: string;
iracingId: string;
@@ -60,12 +106,14 @@ export class RacingDriverFactory {
country: string;
bio?: string;
joinedAt?: Date;
category?: string;
} = {
id: seedId(`driver-${i}`, this.persistence),
iracingId: String(100000 + i),
name: faker.person.fullName(),
country: faker.helpers.arrayElement(countries),
joinedAt,
category,
};
if (hasBio) {

View File

@@ -1,4 +1,4 @@
import { League } from '@core/racing/domain/entities/League';
import { League, LeagueSettings } from '@core/racing/domain/entities/League';
import { Driver } from '@core/racing/domain/entities/Driver';
import { faker } from '@faker-js/faker';
import { seedId } from './SeedIdHelper';
@@ -11,7 +11,7 @@ export class RacingLeagueFactory {
) {}
create(): League[] {
const leagueCount = 30;
const leagueCount = 120; // Expanded to 100+ leagues
// Create diverse league configurations covering ALL enum combinations
// Points systems: f1-2024, indycar, custom (3)
@@ -20,6 +20,8 @@ export class RacingLeagueFactory {
// Decision modes: admin_only, steward_decides, steward_vote, member_vote, steward_veto, member_veto (6)
// Total combinations: 3 * 2 * 2 * 6 = 72, but we'll sample 30 covering extremes
// Category types for leagues
const leagueConfigs = [
// 1-5: Ranked, F1-2024, various stewarding
{
@@ -277,7 +279,8 @@ export class RacingLeagueFactory {
return Array.from({ length: leagueCount }, (_, idx) => {
const i = idx + 1;
const owner = faker.helpers.arrayElement(this.drivers);
const config = leagueConfigs[idx]!;
// Cycle through the 30 configs for variety
const config = leagueConfigs[idx % 30]!;
const createdAt =
// Ensure some "New" leagues (created within 7 days) so `/leagues` featured categories are populated.
@@ -310,33 +313,29 @@ export class RacingLeagueFactory {
}
}
const leagueData: {
id: string;
name: string;
description: string;
ownerId: string;
settings: {
pointsSystem: 'f1-2024' | 'indycar' | 'custom';
maxDrivers: number;
sessionDuration: number;
qualifyingFormat: 'open' | 'single-lap';
visibility?: 'ranked' | 'unranked';
stewarding?: {
decisionMode: 'admin_only' | 'steward_decides' | 'steward_vote' | 'member_vote' | 'steward_veto' | 'member_veto';
requiredVotes?: number;
requireDefense?: boolean;
defenseTimeLimit?: number;
voteTimeLimit?: number;
protestDeadlineHours?: number;
stewardingClosesHours?: number;
notifyAccusedOnProtest?: boolean;
notifyOnVoteRequired?: boolean;
};
};
createdAt: Date;
socialLinks?: { discordUrl?: string; youtubeUrl?: string; websiteUrl?: string };
participantCount?: number;
} = {
// Determine category based on scoring configuration
let category: string | undefined;
if (config.pointsSystem === 'f1-2024') {
if (config.qualifyingFormat === 'single-lap') {
category = 'driver';
} else {
category = 'team';
}
} else if (config.pointsSystem === 'indycar') {
category = 'nations';
} else if (config.pointsSystem === 'custom') {
category = 'trophy';
}
// Override some leagues to have endurance or sprint categories
if (idx % 8 === 0) {
category = 'endurance';
} else if (idx % 7 === 0) {
category = 'sprint';
}
// Build the league data object
const leagueData = {
id: seedId(`league-${i}`, this.persistence),
name: faker.company.name() + ' Racing League',
description: faker.lorem.sentences(2),
@@ -356,6 +355,7 @@ export class RacingLeagueFactory {
notifyOnVoteRequired: true,
},
},
category,
createdAt,
participantCount,
};
@@ -374,11 +374,37 @@ export class RacingLeagueFactory {
if (type === 'website') socialLinks.websiteUrl = faker.internet.url();
});
// Create the final league data with optional fields
const finalLeagueData: {
id: string;
name: string;
description: string;
ownerId: string;
settings?: Partial<LeagueSettings>;
category?: string | undefined;
createdAt?: Date;
socialLinks?: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
participantCount?: number;
} = {
id: leagueData.id,
name: leagueData.name,
description: leagueData.description,
ownerId: leagueData.ownerId,
settings: leagueData.settings,
category: leagueData.category,
createdAt: leagueData.createdAt,
participantCount: leagueData.participantCount,
};
if (Object.keys(socialLinks).length > 0) {
leagueData.socialLinks = socialLinks;
finalLeagueData.socialLinks = socialLinks;
}
return League.create(leagueData);
return League.create(finalLeagueData);
});
}
}

View File

@@ -14,6 +14,7 @@ export interface TeamStats {
totalWins: number;
totalRaces: number;
rating: number;
category?: string;
}
export class RacingTeamFactory {
@@ -33,6 +34,9 @@ export class RacingTeamFactory {
{ min: 0, max: 3 },
);
// 30-50% of teams are recruiting
const isRecruiting = faker.datatype.boolean({ probability: 0.4 });
return Team.create({
id: seedId(`team-${i}`, this.persistence),
name: faker.company.name() + ' Racing',
@@ -40,6 +44,7 @@ export class RacingTeamFactory {
description: faker.lorem.sentences(2),
ownerId: owner.id,
leagues: teamLeagues,
isRecruiting,
createdAt: faker.date.past({ years: 2, refDate: this.baseDate }),
});
});
@@ -256,6 +261,10 @@ export class RacingTeamFactory {
specialization = 'mixed';
}
// Determine category - use all available categories
const categories = ['beginner', 'intermediate', 'advanced', 'pro', 'endurance', 'sprint'];
const category = faker.helpers.arrayElement(categories);
// Generate region and languages
const region = faker.helpers.arrayElement(regions);
const languageCount = faker.number.int({ min: 1, max: 3 });
@@ -273,6 +282,7 @@ export class RacingTeamFactory {
totalWins,
totalRaces,
rating,
category,
});
});