racing typeorm
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import 'reflect-metadata';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
type SetupOptions = {
|
||||
persistence: 'postgres' | 'inmemory';
|
||||
nodeEnv: string | undefined;
|
||||
bootstrapEnabled: boolean;
|
||||
leaguesCount: number;
|
||||
};
|
||||
|
||||
describe('BootstrapModule Postgres racing seed gating (unit)', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function setup({
|
||||
persistence,
|
||||
nodeEnv,
|
||||
bootstrapEnabled,
|
||||
leaguesCount,
|
||||
}: SetupOptions): Promise<{
|
||||
seedExecute: ReturnType<typeof vi.fn>;
|
||||
ensureExecute: ReturnType<typeof vi.fn>;
|
||||
leagueCountAll: ReturnType<typeof vi.fn>;
|
||||
}> {
|
||||
process.env.NODE_ENV = nodeEnv;
|
||||
|
||||
vi.doMock('../../env', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../env')>('../../env');
|
||||
return {
|
||||
...actual,
|
||||
getApiPersistence: () => persistence,
|
||||
getEnableBootstrap: () => bootstrapEnabled,
|
||||
};
|
||||
});
|
||||
|
||||
const seedExecute = vi.fn(async () => undefined);
|
||||
vi.doMock('../../../../../adapters/bootstrap/SeedRacingData', () => {
|
||||
class SeedRacingData {
|
||||
execute = seedExecute;
|
||||
}
|
||||
return { SeedRacingData };
|
||||
});
|
||||
|
||||
const { BootstrapModule } = await import('./BootstrapModule');
|
||||
|
||||
const ensureExecute = vi.fn(async () => undefined);
|
||||
|
||||
const leagueCountAll = vi.fn(async () => leaguesCount);
|
||||
|
||||
const bootstrapModule = new BootstrapModule(
|
||||
{ execute: ensureExecute } as any,
|
||||
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
|
||||
{
|
||||
leagueRepository: { countAll: leagueCountAll },
|
||||
} as any,
|
||||
);
|
||||
|
||||
await bootstrapModule.onModuleInit();
|
||||
|
||||
return { seedExecute, ensureExecute, leagueCountAll };
|
||||
}
|
||||
|
||||
it('seeds when inmemory + bootstrap enabled (existing behavior)', async () => {
|
||||
const { seedExecute, ensureExecute, leagueCountAll } = await setup({
|
||||
persistence: 'inmemory',
|
||||
nodeEnv: 'test',
|
||||
bootstrapEnabled: true,
|
||||
leaguesCount: 123,
|
||||
});
|
||||
|
||||
expect(ensureExecute).toHaveBeenCalledTimes(1);
|
||||
expect(leagueCountAll).toHaveBeenCalledTimes(0);
|
||||
expect(seedExecute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('seeds when postgres + non-production + bootstrap enabled + empty', async () => {
|
||||
const { seedExecute, ensureExecute, leagueCountAll } = await setup({
|
||||
persistence: 'postgres',
|
||||
nodeEnv: 'development',
|
||||
bootstrapEnabled: true,
|
||||
leaguesCount: 0,
|
||||
});
|
||||
|
||||
expect(ensureExecute).toHaveBeenCalledTimes(1);
|
||||
expect(leagueCountAll).toHaveBeenCalledTimes(1);
|
||||
expect(seedExecute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not seed when postgres + non-production + bootstrap enabled + not empty', async () => {
|
||||
const { seedExecute, ensureExecute, leagueCountAll } = await setup({
|
||||
persistence: 'postgres',
|
||||
nodeEnv: 'development',
|
||||
bootstrapEnabled: true,
|
||||
leaguesCount: 1,
|
||||
});
|
||||
|
||||
expect(ensureExecute).toHaveBeenCalledTimes(1);
|
||||
expect(leagueCountAll).toHaveBeenCalledTimes(1);
|
||||
expect(seedExecute).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('does not seed when postgres + production (even if empty)', async () => {
|
||||
const { seedExecute, ensureExecute, leagueCountAll } = await setup({
|
||||
persistence: 'postgres',
|
||||
nodeEnv: 'production',
|
||||
bootstrapEnabled: true,
|
||||
leaguesCount: 0,
|
||||
});
|
||||
|
||||
expect(ensureExecute).toHaveBeenCalledTimes(1);
|
||||
expect(leagueCountAll).toHaveBeenCalledTimes(0);
|
||||
expect(seedExecute).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('does not seed when postgres + non-production + bootstrap disabled (even if empty)', async () => {
|
||||
const { seedExecute, ensureExecute, leagueCountAll } = await setup({
|
||||
persistence: 'postgres',
|
||||
nodeEnv: 'development',
|
||||
bootstrapEnabled: false,
|
||||
leaguesCount: 0,
|
||||
});
|
||||
|
||||
expect(ensureExecute).toHaveBeenCalledTimes(0);
|
||||
expect(leagueCountAll).toHaveBeenCalledTimes(0);
|
||||
expect(seedExecute).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -2,13 +2,13 @@ import type { Logger } from '@core/shared/application';
|
||||
import type { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
|
||||
import { SeedRacingData, type RacingSeedDependencies } from '../../../../../adapters/bootstrap/SeedRacingData';
|
||||
import { Inject, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { getApiPersistence } from '../../env';
|
||||
import { InMemoryRacingPersistenceModule } from '../../persistence/inmemory/InMemoryRacingPersistenceModule';
|
||||
import { getApiPersistence, getEnableBootstrap } from '../../env';
|
||||
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
|
||||
import { InMemorySocialPersistenceModule } from '../../persistence/inmemory/InMemorySocialPersistenceModule';
|
||||
import { BootstrapProviders, ENSURE_INITIAL_DATA_TOKEN } from './BootstrapProviders';
|
||||
|
||||
@Module({
|
||||
imports: [InMemoryRacingPersistenceModule, InMemorySocialPersistenceModule],
|
||||
imports: [RacingPersistenceModule, InMemorySocialPersistenceModule],
|
||||
providers: BootstrapProviders,
|
||||
})
|
||||
export class BootstrapModule implements OnModuleInit {
|
||||
@@ -21,9 +21,14 @@ export class BootstrapModule implements OnModuleInit {
|
||||
async onModuleInit() {
|
||||
console.log('[Bootstrap] Initializing application data...');
|
||||
try {
|
||||
if (!getEnableBootstrap()) {
|
||||
this.logger.info('[Bootstrap] Bootstrap disabled via GRIDPILOT_API_BOOTSTRAP; skipping initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.ensureInitialData.execute();
|
||||
|
||||
if (this.shouldSeedRacingData()) {
|
||||
if (await this.shouldSeedRacingData()) {
|
||||
await new SeedRacingData(this.logger, this.seedDeps).execute();
|
||||
}
|
||||
|
||||
@@ -34,7 +39,21 @@ export class BootstrapModule implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
private shouldSeedRacingData(): boolean {
|
||||
return getApiPersistence() === 'inmemory';
|
||||
private async shouldSeedRacingData(): Promise<boolean> {
|
||||
const persistence = getApiPersistence();
|
||||
|
||||
if (persistence === 'inmemory') return true;
|
||||
if (persistence !== 'postgres') return false;
|
||||
if (process.env.NODE_ENV === 'production') return false;
|
||||
|
||||
return this.isRacingDatabaseEmpty();
|
||||
}
|
||||
|
||||
private async isRacingDatabaseEmpty(): Promise<boolean> {
|
||||
const count = await this.seedDeps.leagueRepository.countAll?.();
|
||||
if (typeof count === 'number') return count === 0;
|
||||
|
||||
const leagues = await this.seedDeps.leagueRepository.findAll();
|
||||
return leagues.length === 0;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InMemoryRacingPersistenceModule } from '../../persistence/inmemory/InMemoryRacingPersistenceModule';
|
||||
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
|
||||
import { InMemorySocialPersistenceModule } from '../../persistence/inmemory/InMemorySocialPersistenceModule';
|
||||
import { DashboardService } from './DashboardService';
|
||||
import { DashboardController } from './DashboardController';
|
||||
import { DashboardProviders } from './DashboardProviders';
|
||||
|
||||
@Module({
|
||||
imports: [InMemoryRacingPersistenceModule, InMemorySocialPersistenceModule],
|
||||
imports: [RacingPersistenceModule, InMemorySocialPersistenceModule],
|
||||
controllers: [DashboardController],
|
||||
providers: [DashboardService, ...DashboardProviders],
|
||||
exports: [DashboardService],
|
||||
|
||||
@@ -14,7 +14,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
password: process.env.DATABASE_PASSWORD || 'password',
|
||||
database: process.env.DATABASE_NAME || 'gridpilot',
|
||||
}),
|
||||
// entities: [AnalyticsSnapshotOrmEntity, EngagementOrmEntity],
|
||||
autoLoadEntities: true,
|
||||
synchronize: process.env.NODE_ENV !== 'production',
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InMemoryRacingPersistenceModule } from '../../persistence/inmemory/InMemoryRacingPersistenceModule';
|
||||
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
|
||||
import { InMemorySocialPersistenceModule } from '../../persistence/inmemory/InMemorySocialPersistenceModule';
|
||||
import { DriverService } from './DriverService';
|
||||
import { DriverController } from './DriverController';
|
||||
import { DriverProviders } from './DriverProviders';
|
||||
|
||||
@Module({
|
||||
imports: [InMemoryRacingPersistenceModule, InMemorySocialPersistenceModule],
|
||||
imports: [RacingPersistenceModule, InMemorySocialPersistenceModule],
|
||||
controllers: [DriverController],
|
||||
providers: [DriverService, ...DriverProviders],
|
||||
exports: [DriverService],
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InMemoryRacingPersistenceModule } from '../../persistence/inmemory/InMemoryRacingPersistenceModule';
|
||||
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { LeagueController } from './LeagueController';
|
||||
import { LeagueProviders } from './LeagueProviders';
|
||||
|
||||
@Module({
|
||||
imports: [InMemoryRacingPersistenceModule],
|
||||
imports: [RacingPersistenceModule],
|
||||
controllers: [LeagueController],
|
||||
providers: LeagueProviders,
|
||||
exports: [LeagueService],
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InMemoryRacingPersistenceModule } from '../../persistence/inmemory/InMemoryRacingPersistenceModule';
|
||||
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
|
||||
import { ProtestsController } from './ProtestsController';
|
||||
import { ProtestsService } from './ProtestsService';
|
||||
import { ProtestsProviders } from './ProtestsProviders';
|
||||
|
||||
@Module({
|
||||
imports: [InMemoryRacingPersistenceModule],
|
||||
imports: [RacingPersistenceModule],
|
||||
providers: [ProtestsService, ...ProtestsProviders],
|
||||
controllers: [ProtestsController],
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InMemoryRacingPersistenceModule } from '../../persistence/inmemory/InMemoryRacingPersistenceModule';
|
||||
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
|
||||
import { RaceService } from './RaceService';
|
||||
import { RaceController } from './RaceController';
|
||||
import { RaceProviders } from './RaceProviders';
|
||||
|
||||
@Module({
|
||||
imports: [InMemoryRacingPersistenceModule],
|
||||
imports: [RacingPersistenceModule],
|
||||
controllers: [RaceController],
|
||||
providers: [RaceService, ...RaceProviders],
|
||||
exports: [RaceService],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InMemoryRacingPersistenceModule } from '../../persistence/inmemory/InMemoryRacingPersistenceModule';
|
||||
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
|
||||
import { AuthModule } from '../auth/AuthModule';
|
||||
import { PolicyModule } from '../policy/PolicyModule';
|
||||
import { SponsorService } from './SponsorService';
|
||||
@@ -7,7 +7,7 @@ import { SponsorController } from './SponsorController';
|
||||
import { SponsorProviders } from './SponsorProviders';
|
||||
|
||||
@Module({
|
||||
imports: [InMemoryRacingPersistenceModule, AuthModule, PolicyModule],
|
||||
imports: [RacingPersistenceModule, AuthModule, PolicyModule],
|
||||
controllers: [SponsorController],
|
||||
providers: SponsorProviders,
|
||||
exports: [SponsorService],
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InMemoryRacingPersistenceModule } from '../../persistence/inmemory/InMemoryRacingPersistenceModule';
|
||||
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
|
||||
import { TeamService } from './TeamService';
|
||||
import { TeamController } from './TeamController';
|
||||
import { TeamProviders } from './TeamProviders';
|
||||
|
||||
@Module({
|
||||
imports: [InMemoryRacingPersistenceModule],
|
||||
imports: [RacingPersistenceModule],
|
||||
controllers: [TeamController],
|
||||
providers: [TeamService, ...TeamProviders],
|
||||
exports: [TeamService],
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
|
||||
import type { DataSource } from 'typeorm';
|
||||
|
||||
import { LoggingModule } from '../../domain/logging/LoggingModule';
|
||||
|
||||
import {
|
||||
DRIVER_REPOSITORY_TOKEN,
|
||||
GAME_REPOSITORY_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
LEAGUE_WALLET_REPOSITORY_TOKEN,
|
||||
PENALTY_REPOSITORY_TOKEN,
|
||||
PROTEST_REPOSITORY_TOKEN,
|
||||
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
SEASON_REPOSITORY_TOKEN,
|
||||
SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
|
||||
SPONSOR_REPOSITORY_TOKEN,
|
||||
SPONSORSHIP_PRICING_REPOSITORY_TOKEN,
|
||||
SPONSORSHIP_REQUEST_REPOSITORY_TOKEN,
|
||||
STANDING_REPOSITORY_TOKEN,
|
||||
TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
TEAM_REPOSITORY_TOKEN,
|
||||
TRANSACTION_REPOSITORY_TOKEN,
|
||||
} from '../inmemory/InMemoryRacingPersistenceModule';
|
||||
|
||||
import { LeagueOrmEntity } from '@adapters/racing/persistence/typeorm/entities/LeagueOrmEntity';
|
||||
import { LeagueScoringConfigOrmEntity } from '@adapters/racing/persistence/typeorm/entities/LeagueScoringConfigOrmEntity';
|
||||
import { RaceOrmEntity } from '@adapters/racing/persistence/typeorm/entities/RaceOrmEntity';
|
||||
import { SeasonOrmEntity } from '@adapters/racing/persistence/typeorm/entities/SeasonOrmEntity';
|
||||
|
||||
import { TypeOrmLeagueRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueRepository';
|
||||
import { TypeOrmLeagueScoringConfigRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueScoringConfigRepository';
|
||||
import { TypeOrmRaceRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmRaceRepository';
|
||||
import { TypeOrmSeasonRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmSeasonRepository';
|
||||
import { LeagueOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper';
|
||||
import { RaceOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/RaceOrmMapper';
|
||||
import { SeasonOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper';
|
||||
import { PointsTableJsonMapper } from '@adapters/racing/persistence/typeorm/mappers/PointsTableJsonMapper';
|
||||
import { ChampionshipConfigJsonMapper } from '@adapters/racing/persistence/typeorm/mappers/ChampionshipConfigJsonMapper';
|
||||
import { LeagueScoringConfigOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueScoringConfigOrmMapper';
|
||||
|
||||
function makePlaceholder(token: string): unknown {
|
||||
return Object.freeze({
|
||||
__token: token,
|
||||
__kind: 'postgres-placeholder',
|
||||
__notImplemented(): never {
|
||||
throw new Error(`[PostgresRacingPersistenceModule] Placeholder provider "${token}" is not implemented yet`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const typeOrmFeatureImports = [
|
||||
TypeOrmModule.forFeature([LeagueOrmEntity, SeasonOrmEntity, RaceOrmEntity, LeagueScoringConfigOrmEntity]),
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [LoggingModule, ...typeOrmFeatureImports],
|
||||
providers: [
|
||||
{
|
||||
provide: DRIVER_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(DRIVER_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource) => {
|
||||
const leagueMapper = new LeagueOrmMapper();
|
||||
return new TypeOrmLeagueRepository(dataSource, leagueMapper);
|
||||
},
|
||||
inject: [getDataSourceToken()],
|
||||
},
|
||||
{
|
||||
provide: RACE_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource) => {
|
||||
const raceMapper = new RaceOrmMapper();
|
||||
return new TypeOrmRaceRepository(dataSource, raceMapper);
|
||||
},
|
||||
inject: [getDataSourceToken()],
|
||||
},
|
||||
{
|
||||
provide: RESULT_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(RESULT_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: STANDING_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(STANDING_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(RACE_REGISTRATION_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: TEAM_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(TEAM_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(TEAM_MEMBERSHIP_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: PENALTY_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(PENALTY_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: PROTEST_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(PROTEST_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: SEASON_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource) => {
|
||||
const seasonMapper = new SeasonOrmMapper();
|
||||
return new TypeOrmSeasonRepository(dataSource, seasonMapper);
|
||||
},
|
||||
inject: [getDataSourceToken()],
|
||||
},
|
||||
{
|
||||
provide: SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(SEASON_SPONSORSHIP_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource) => {
|
||||
const pointsTableMapper = new PointsTableJsonMapper();
|
||||
const championshipMapper = new ChampionshipConfigJsonMapper(pointsTableMapper);
|
||||
const scoringConfigMapper = new LeagueScoringConfigOrmMapper(championshipMapper);
|
||||
return new TypeOrmLeagueScoringConfigRepository(dataSource, scoringConfigMapper);
|
||||
},
|
||||
inject: [getDataSourceToken()],
|
||||
},
|
||||
{
|
||||
provide: GAME_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(GAME_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_WALLET_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(LEAGUE_WALLET_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: TRANSACTION_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(TRANSACTION_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: SPONSOR_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(SPONSOR_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: SPONSORSHIP_PRICING_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(SPONSORSHIP_PRICING_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: SPONSORSHIP_REQUEST_REPOSITORY_TOKEN,
|
||||
useFactory: () => makePlaceholder(SPONSORSHIP_REQUEST_REPOSITORY_TOKEN),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
DRIVER_REPOSITORY_TOKEN,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
STANDING_REPOSITORY_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
TEAM_REPOSITORY_TOKEN,
|
||||
TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
PENALTY_REPOSITORY_TOKEN,
|
||||
PROTEST_REPOSITORY_TOKEN,
|
||||
SEASON_REPOSITORY_TOKEN,
|
||||
SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
|
||||
LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
GAME_REPOSITORY_TOKEN,
|
||||
LEAGUE_WALLET_REPOSITORY_TOKEN,
|
||||
TRANSACTION_REPOSITORY_TOKEN,
|
||||
SPONSOR_REPOSITORY_TOKEN,
|
||||
SPONSORSHIP_PRICING_REPOSITORY_TOKEN,
|
||||
SPONSORSHIP_REQUEST_REPOSITORY_TOKEN,
|
||||
],
|
||||
})
|
||||
export class PostgresRacingPersistenceModule {}
|
||||
@@ -0,0 +1,158 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { Season } from '@core/racing/domain/entities/season/Season';
|
||||
import { PointsTable } from '@core/racing/domain/value-objects/PointsTable';
|
||||
|
||||
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
|
||||
import type { SessionType } from '@core/racing/domain/types/SessionType';
|
||||
|
||||
import { LeagueOrmEntity } from '../../../../../../adapters/racing/persistence/typeorm/entities/LeagueOrmEntity';
|
||||
import { LeagueScoringConfigOrmEntity } from '../../../../../../adapters/racing/persistence/typeorm/entities/LeagueScoringConfigOrmEntity';
|
||||
import { RaceOrmEntity } from '../../../../../../adapters/racing/persistence/typeorm/entities/RaceOrmEntity';
|
||||
import { SeasonOrmEntity } from '../../../../../../adapters/racing/persistence/typeorm/entities/SeasonOrmEntity';
|
||||
|
||||
import { TypeOrmLeagueRepository } from '../../../../../../adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueRepository';
|
||||
import { TypeOrmLeagueScoringConfigRepository } from '../../../../../../adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueScoringConfigRepository';
|
||||
import { TypeOrmRaceRepository } from '../../../../../../adapters/racing/persistence/typeorm/repositories/TypeOrmRaceRepository';
|
||||
import { TypeOrmSeasonRepository } from '../../../../../../adapters/racing/persistence/typeorm/repositories/TypeOrmSeasonRepository';
|
||||
import { LeagueOrmMapper } from '../../../../../../adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper';
|
||||
import { RaceOrmMapper } from '../../../../../../adapters/racing/persistence/typeorm/mappers/RaceOrmMapper';
|
||||
import { SeasonOrmMapper } from '../../../../../../adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper';
|
||||
import { PointsTableJsonMapper } from '../../../../../../adapters/racing/persistence/typeorm/mappers/PointsTableJsonMapper';
|
||||
import { ChampionshipConfigJsonMapper } from '../../../../../../adapters/racing/persistence/typeorm/mappers/ChampionshipConfigJsonMapper';
|
||||
import { LeagueScoringConfigOrmMapper } from '../../../../../../adapters/racing/persistence/typeorm/mappers/LeagueScoringConfigOrmMapper';
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const describeIfDatabase = databaseUrl ? describe : describe.skip;
|
||||
|
||||
describeIfDatabase('TypeORM Racing repositories (postgres slice)', () => {
|
||||
let dataSource: DataSource;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (!databaseUrl) {
|
||||
throw new Error('DATABASE_URL is required to run postgres integration tests');
|
||||
}
|
||||
|
||||
dataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
url: databaseUrl,
|
||||
entities: [LeagueOrmEntity, SeasonOrmEntity, RaceOrmEntity, LeagueScoringConfigOrmEntity],
|
||||
synchronize: true,
|
||||
});
|
||||
|
||||
await dataSource.initialize();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (dataSource?.isInitialized) {
|
||||
await dataSource.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('supports: create league + create season + save scoring config + fetch schedule', async () => {
|
||||
const leagueRepo = new TypeOrmLeagueRepository(dataSource, new LeagueOrmMapper());
|
||||
const seasonRepo = new TypeOrmSeasonRepository(dataSource, new SeasonOrmMapper());
|
||||
const raceRepo = new TypeOrmRaceRepository(dataSource, new RaceOrmMapper());
|
||||
|
||||
const pointsTableMapper = new PointsTableJsonMapper();
|
||||
const championshipMapper = new ChampionshipConfigJsonMapper(pointsTableMapper);
|
||||
const scoringConfigMapper = new LeagueScoringConfigOrmMapper(championshipMapper);
|
||||
const scoringRepo = new TypeOrmLeagueScoringConfigRepository(dataSource, scoringConfigMapper);
|
||||
|
||||
const league = League.create({
|
||||
id: 'league-it-1',
|
||||
name: 'Integration League',
|
||||
description: 'For integration testing',
|
||||
ownerId: 'driver-it-1',
|
||||
settings: { pointsSystem: 'custom', visibility: 'unranked', maxDrivers: 32 },
|
||||
participantCount: 0,
|
||||
});
|
||||
|
||||
await leagueRepo.create(league);
|
||||
|
||||
const season = Season.create({
|
||||
id: 'season-it-1',
|
||||
leagueId: league.id.toString(),
|
||||
gameId: 'iracing',
|
||||
name: 'Integration Season',
|
||||
year: 2025,
|
||||
order: 1,
|
||||
status: 'active',
|
||||
startDate: new Date('2025-01-01T00:00:00.000Z'),
|
||||
endDate: new Date('2025-12-31T00:00:00.000Z'),
|
||||
schedulePublished: false,
|
||||
});
|
||||
|
||||
await seasonRepo.create(season);
|
||||
|
||||
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
|
||||
practice: new PointsTable({}),
|
||||
qualifying: new PointsTable({}),
|
||||
q1: new PointsTable({}),
|
||||
q2: new PointsTable({}),
|
||||
q3: new PointsTable({}),
|
||||
sprint: new PointsTable({}),
|
||||
main: new PointsTable({ 1: 25 }),
|
||||
timeTrial: new PointsTable({}),
|
||||
};
|
||||
|
||||
const bonusRulesBySessionType = {
|
||||
practice: [],
|
||||
qualifying: [],
|
||||
q1: [],
|
||||
q2: [],
|
||||
q3: [],
|
||||
sprint: [],
|
||||
main: [],
|
||||
timeTrial: [],
|
||||
};
|
||||
|
||||
const championship: ChampionshipConfig = {
|
||||
id: 'champ-it-1',
|
||||
name: 'Driver Championship',
|
||||
type: 'driver',
|
||||
sessionTypes: ['main' as SessionType],
|
||||
pointsTableBySessionType,
|
||||
bonusRulesBySessionType,
|
||||
dropScorePolicy: { strategy: 'none' },
|
||||
};
|
||||
|
||||
const scoring = LeagueScoringConfig.create({
|
||||
id: 'lsc-it-1',
|
||||
seasonId: season.id,
|
||||
scoringPresetId: 'club-default',
|
||||
championships: [championship],
|
||||
});
|
||||
|
||||
await scoringRepo.save(scoring);
|
||||
|
||||
const race = Race.create({
|
||||
id: 'race-it-1',
|
||||
leagueId: league.id.toString(),
|
||||
scheduledAt: new Date('2025-03-01T12:00:00.000Z'),
|
||||
track: 'Spa',
|
||||
car: 'GT3',
|
||||
status: 'scheduled',
|
||||
});
|
||||
|
||||
await raceRepo.create(race);
|
||||
|
||||
const persistedLeague = await leagueRepo.findById(league.id.toString());
|
||||
expect(persistedLeague?.name.toString()).toBe('Integration League');
|
||||
|
||||
const seasons = await seasonRepo.findByLeagueId(league.id.toString());
|
||||
expect(seasons.map((s: Season) => s.id)).toContain('season-it-1');
|
||||
|
||||
const races = await raceRepo.findByLeagueId(league.id.toString());
|
||||
expect(races.map((r: Race) => r.id)).toContain('race-it-1');
|
||||
|
||||
const persistedScoring = await scoringRepo.findBySeasonId(season.id);
|
||||
expect(persistedScoring?.id.toString()).toBe('lsc-it-1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { Season } from '@core/racing/domain/entities/season/Season';
|
||||
import { PointsTable } from '@core/racing/domain/value-objects/PointsTable';
|
||||
import { SessionType as RaceSessionType } from '@core/racing/domain/value-objects/SessionType';
|
||||
|
||||
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
|
||||
import type { SessionType } from '@core/racing/domain/types/SessionType';
|
||||
|
||||
import { ChampionshipConfigJsonMapper } from '@adapters/racing/persistence/typeorm/mappers/ChampionshipConfigJsonMapper';
|
||||
import { LeagueOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper';
|
||||
import { LeagueScoringConfigOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueScoringConfigOrmMapper';
|
||||
import { PointsTableJsonMapper } from '@adapters/racing/persistence/typeorm/mappers/PointsTableJsonMapper';
|
||||
import { RaceOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/RaceOrmMapper';
|
||||
import { SeasonOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/SeasonOrmMapper';
|
||||
|
||||
describe('RacingOrmMappers', () => {
|
||||
it('maps League domain <-> orm', () => {
|
||||
const league = League.create({
|
||||
id: 'league-1',
|
||||
name: 'My League',
|
||||
description: 'A description',
|
||||
ownerId: 'driver-1',
|
||||
settings: {
|
||||
pointsSystem: 'custom',
|
||||
maxDrivers: 48,
|
||||
visibility: 'unranked',
|
||||
},
|
||||
socialLinks: {
|
||||
discordUrl: 'https://discord.gg/example',
|
||||
},
|
||||
participantCount: 12,
|
||||
createdAt: new Date('2025-01-01T00:00:00.000Z'),
|
||||
});
|
||||
|
||||
const mapper = new LeagueOrmMapper();
|
||||
const orm = mapper.toOrmEntity(league);
|
||||
const rehydrated = mapper.toDomain(orm);
|
||||
|
||||
expect(rehydrated.id.toString()).toBe('league-1');
|
||||
expect(rehydrated.name.toString()).toBe('My League');
|
||||
expect(rehydrated.description.toString()).toBe('A description');
|
||||
expect(rehydrated.ownerId.toString()).toBe('driver-1');
|
||||
expect(rehydrated.settings.maxDrivers).toBe(48);
|
||||
expect(rehydrated.settings.visibility).toBe('unranked');
|
||||
expect(rehydrated.getParticipantCount()).toBe(12);
|
||||
expect(rehydrated.socialLinks?.discordUrl).toBe('https://discord.gg/example');
|
||||
});
|
||||
|
||||
it('maps Season domain <-> orm', () => {
|
||||
const season = Season.create({
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Season 1',
|
||||
year: 2025,
|
||||
order: 1,
|
||||
status: 'active',
|
||||
startDate: new Date('2025-01-01T00:00:00.000Z'),
|
||||
endDate: new Date('2025-02-01T00:00:00.000Z'),
|
||||
schedulePublished: true,
|
||||
participantCount: 7,
|
||||
maxDrivers: 32,
|
||||
});
|
||||
|
||||
const mapper = new SeasonOrmMapper();
|
||||
const orm = mapper.toOrmEntity(season);
|
||||
const rehydrated = mapper.toDomain(orm);
|
||||
|
||||
expect(rehydrated.id).toBe('season-1');
|
||||
expect(rehydrated.leagueId).toBe('league-1');
|
||||
expect(rehydrated.gameId).toBe('iracing');
|
||||
expect(rehydrated.name).toBe('Season 1');
|
||||
expect(rehydrated.status.toString()).toBe('active');
|
||||
expect(rehydrated.schedulePublished).toBe(true);
|
||||
expect(rehydrated.getParticipantCount()).toBe(7);
|
||||
});
|
||||
|
||||
it('maps Race domain <-> orm', () => {
|
||||
const race = Race.create({
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
scheduledAt: new Date('2025-01-10T12:00:00.000Z'),
|
||||
track: 'Spa',
|
||||
car: 'GT3',
|
||||
sessionType: RaceSessionType.main(),
|
||||
status: 'scheduled',
|
||||
registeredCount: 3,
|
||||
maxParticipants: 50,
|
||||
});
|
||||
|
||||
const mapper = new RaceOrmMapper();
|
||||
const orm = mapper.toOrmEntity(race);
|
||||
const rehydrated = mapper.toDomain(orm);
|
||||
|
||||
expect(rehydrated.id).toBe('race-1');
|
||||
expect(rehydrated.leagueId).toBe('league-1');
|
||||
expect(rehydrated.track).toBe('Spa');
|
||||
expect(rehydrated.car).toBe('GT3');
|
||||
expect(rehydrated.sessionType.props).toBe('main');
|
||||
expect(rehydrated.status.toString()).toBe('scheduled');
|
||||
expect(rehydrated.getRegisteredCount()).toBe(3);
|
||||
expect(rehydrated.getMaxParticipants()).toBe(50);
|
||||
});
|
||||
|
||||
it('maps LeagueScoringConfig domain <-> orm (including PointsTable)', () => {
|
||||
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
|
||||
practice: new PointsTable({}),
|
||||
qualifying: new PointsTable({}),
|
||||
q1: new PointsTable({}),
|
||||
q2: new PointsTable({}),
|
||||
q3: new PointsTable({}),
|
||||
sprint: new PointsTable({}),
|
||||
main: new PointsTable({ 1: 25, 2: 18 }),
|
||||
timeTrial: new PointsTable({}),
|
||||
};
|
||||
|
||||
const bonusRulesBySessionType = {
|
||||
practice: [],
|
||||
qualifying: [],
|
||||
q1: [],
|
||||
q2: [],
|
||||
q3: [],
|
||||
sprint: [],
|
||||
main: [],
|
||||
timeTrial: [],
|
||||
};
|
||||
|
||||
const championship: ChampionshipConfig = {
|
||||
id: 'champ-1',
|
||||
name: 'Driver Championship',
|
||||
type: 'driver',
|
||||
sessionTypes: ['main' as SessionType],
|
||||
pointsTableBySessionType,
|
||||
bonusRulesBySessionType,
|
||||
dropScorePolicy: { strategy: 'none' },
|
||||
};
|
||||
|
||||
const config = LeagueScoringConfig.create({
|
||||
id: 'lsc-season-1',
|
||||
seasonId: 'season-1',
|
||||
scoringPresetId: 'club-default',
|
||||
championships: [championship],
|
||||
});
|
||||
|
||||
const pointsTableMapper = new PointsTableJsonMapper();
|
||||
const championshipMapper = new ChampionshipConfigJsonMapper(pointsTableMapper);
|
||||
const mapper = new LeagueScoringConfigOrmMapper(championshipMapper);
|
||||
|
||||
const orm = mapper.toOrmEntity(config);
|
||||
const rehydrated = mapper.toDomain(orm);
|
||||
|
||||
expect(rehydrated.id.toString()).toBe('lsc-season-1');
|
||||
expect(rehydrated.seasonId.toString()).toBe('season-1');
|
||||
expect(rehydrated.scoringPresetId?.toString()).toBe('club-default');
|
||||
expect(rehydrated.championships).toHaveLength(1);
|
||||
|
||||
const mainPointsTable =
|
||||
rehydrated.championships[0]?.pointsTableBySessionType['main' as SessionType];
|
||||
expect(mainPointsTable).toBeDefined();
|
||||
expect(mainPointsTable!.getPointsForPosition(1)).toBe(25);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { MODULE_METADATA } from '@nestjs/common/constants';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LEAGUE_REPOSITORY_TOKEN } from '../inmemory/InMemoryRacingPersistenceModule';
|
||||
|
||||
describe('RacingPersistenceModule', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('uses inmemory providers when GRIDPILOT_API_PERSISTENCE=inmemory', async () => {
|
||||
vi.resetModules();
|
||||
|
||||
process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory';
|
||||
delete process.env.DATABASE_URL;
|
||||
|
||||
const { RacingPersistenceModule } = await import('./RacingPersistenceModule');
|
||||
const { InMemoryLeagueRepository } = await import('@adapters/racing/persistence/inmemory/InMemoryLeagueRepository');
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [RacingPersistenceModule],
|
||||
}).compile();
|
||||
|
||||
const leagueRepo = module.get(LEAGUE_REPOSITORY_TOKEN);
|
||||
expect(leagueRepo).toBeInstanceOf(InMemoryLeagueRepository);
|
||||
|
||||
await module.close();
|
||||
});
|
||||
|
||||
it('uses postgres module when GRIDPILOT_API_PERSISTENCE=postgres', async () => {
|
||||
vi.resetModules();
|
||||
|
||||
process.env.GRIDPILOT_API_PERSISTENCE = 'postgres';
|
||||
delete process.env.DATABASE_URL;
|
||||
|
||||
const { RacingPersistenceModule } = await import('./RacingPersistenceModule');
|
||||
const { PostgresRacingPersistenceModule } = await import('../postgres/PostgresRacingPersistenceModule');
|
||||
|
||||
const imports = Reflect.getMetadata(MODULE_METADATA.IMPORTS, RacingPersistenceModule) as unknown[];
|
||||
expect(imports).toContain(PostgresRacingPersistenceModule);
|
||||
});
|
||||
});
|
||||
14
apps/api/src/persistence/racing/RacingPersistenceModule.ts
Normal file
14
apps/api/src/persistence/racing/RacingPersistenceModule.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { getApiPersistence } from '../../env';
|
||||
import { InMemoryRacingPersistenceModule } from '../inmemory/InMemoryRacingPersistenceModule';
|
||||
import { PostgresRacingPersistenceModule } from '../postgres/PostgresRacingPersistenceModule';
|
||||
|
||||
const selectedPersistenceModule =
|
||||
getApiPersistence() === 'postgres' ? PostgresRacingPersistenceModule : InMemoryRacingPersistenceModule;
|
||||
|
||||
@Module({
|
||||
imports: [selectedPersistenceModule],
|
||||
exports: [selectedPersistenceModule],
|
||||
})
|
||||
export class RacingPersistenceModule {}
|
||||
Reference in New Issue
Block a user