Files
gridpilot.gg/adapters/bootstrap/EnsureInitialData.ts
2025-12-26 22:22:39 +01:00

212 lines
6.7 KiB
TypeScript

import { SignupWithEmailUseCase } from '@core/identity/application/use-cases/SignupWithEmailUseCase';
import { CreateAchievementUseCase } from '@core/identity/application/use-cases/achievement/CreateAchievementUseCase';
import type { Logger } from '@core/shared/application';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import { createInMemoryRacingSeed } from './inmemory/InMemoryRacingSeed';
import {
DRIVER_ACHIEVEMENTS,
STEWARD_ACHIEVEMENTS,
ADMIN_ACHIEVEMENTS,
COMMUNITY_ACHIEVEMENTS,
} from '@core/identity/domain/AchievementConstants';
export type InMemorySeedDependencies = {
driverRepository: IDriverRepository;
leagueRepository: ILeagueRepository;
raceRepository: IRaceRepository;
resultRepository: IResultRepository;
standingRepository: IStandingRepository;
leagueMembershipRepository: ILeagueMembershipRepository;
raceRegistrationRepository: IRaceRegistrationRepository;
teamRepository: ITeamRepository;
teamMembershipRepository: ITeamMembershipRepository;
feedRepository: IFeedRepository;
socialGraphRepository: ISocialGraphRepository;
};
export class EnsureInitialData {
constructor(
private readonly signupUseCase: SignupWithEmailUseCase,
private readonly createAchievementUseCase: CreateAchievementUseCase,
private readonly logger: Logger,
private readonly seedDeps?: InMemorySeedDependencies,
) {}
async execute(): Promise<void> {
// Ensure initial admin user exists
try {
await this.signupUseCase.execute({
email: 'admin@gridpilot.local',
password: 'admin123',
displayName: 'Admin',
});
this.logger.info('[Bootstrap] Initial admin user created');
} catch (error) {
if (error instanceof Error && error.message === 'An account with this email already exists') {
// User already exists, nothing to do
this.logger.info('[Bootstrap] Admin user already exists');
} else {
// Re-throw other errors
throw error;
}
}
// Ensure initial achievements exist
const allAchievements = [
...DRIVER_ACHIEVEMENTS,
...STEWARD_ACHIEVEMENTS,
...ADMIN_ACHIEVEMENTS,
...COMMUNITY_ACHIEVEMENTS,
];
let createdCount = 0;
let existingCount = 0;
for (const achievementProps of allAchievements) {
try {
await this.createAchievementUseCase.execute(achievementProps);
createdCount++;
} catch {
// If achievement already exists, that's fine
existingCount++;
}
}
this.logger.info(`[Bootstrap] Achievements: ${createdCount} created, ${existingCount} already exist`);
await this.seedInMemoryRacingDataIfNeeded();
}
private shouldSeedInMemory(): boolean {
const configured = (process.env.GRIDPILOT_API_PERSISTENCE ?? '').toLowerCase();
if (configured) {
return configured === 'inmemory';
}
return process.env.DATABASE_URL === undefined;
}
private async seedInMemoryRacingDataIfNeeded(): Promise<void> {
if (!this.shouldSeedInMemory()) {
return;
}
if (!this.seedDeps) {
this.logger.info('[Bootstrap] In-memory racing seed skipped (missing dependencies)');
return;
}
const existingDrivers = await this.seedDeps.driverRepository.findAll();
if (existingDrivers.length > 0) {
this.logger.info('[Bootstrap] In-memory racing seed skipped (drivers already exist)');
return;
}
const seed = createInMemoryRacingSeed();
for (const driver of seed.drivers) {
try {
await this.seedDeps.driverRepository.create(driver);
} catch {
// ignore duplicates
}
}
for (const league of seed.leagues) {
try {
await this.seedDeps.leagueRepository.create(league);
} catch {
// ignore duplicates
}
}
for (const race of seed.races) {
try {
await this.seedDeps.raceRepository.create(race);
} catch {
// ignore duplicates
}
}
try {
await this.seedDeps.resultRepository.createMany(seed.results);
} catch {
// ignore duplicates
}
for (const membership of seed.leagueMemberships) {
try {
await this.seedDeps.leagueMembershipRepository.saveMembership(membership);
} catch {
// ignore duplicates
}
}
for (const team of seed.teams) {
try {
await this.seedDeps.teamRepository.create(team);
} catch {
// ignore duplicates
}
}
for (const membership of seed.teamMemberships) {
try {
await this.seedDeps.teamMembershipRepository.saveMembership(membership);
} catch {
// ignore duplicates
}
}
for (const registration of seed.raceRegistrations) {
try {
await this.seedDeps.raceRegistrationRepository.register(registration);
} catch {
// ignore duplicates
}
}
try {
await this.seedDeps.standingRepository.saveMany(seed.standings);
} catch {
// ignore duplicates
}
const seedableFeed = this.seedDeps.feedRepository as unknown as { seed?: (input: unknown) => void };
if (typeof seedableFeed.seed === 'function') {
seedableFeed.seed({
drivers: seed.drivers,
friendships: seed.friendships,
feedEvents: seed.feedEvents,
});
}
const seedableSocial = this.seedDeps.socialGraphRepository as unknown as { seed?: (input: unknown) => void };
if (typeof seedableSocial.seed === 'function') {
seedableSocial.seed({
drivers: seed.drivers,
friendships: seed.friendships,
feedEvents: seed.feedEvents,
});
}
this.logger.info(
`[Bootstrap] Seeded in-memory racing data: drivers=${seed.drivers.length}, leagues=${seed.leagues.length}, races=${seed.races.length}`,
);
}
}