racing typeorm

This commit is contained in:
2025-12-29 00:24:56 +01:00
parent 2f6657f56d
commit 9e17d0752a
55 changed files with 3528 additions and 22 deletions

View File

@@ -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);
});
});

View File

@@ -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;
}
}