inmemory to postgres
This commit is contained in:
@@ -22,6 +22,8 @@ import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenal
|
||||
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import { createRacingSeed } from './racing/RacingSeed';
|
||||
import { getApiPersistence } from '../../apps/api/src/env';
|
||||
import { seedId } from './racing/SeedIdHelper';
|
||||
|
||||
export type RacingSeedDependencies = {
|
||||
driverRepository: IDriverRepository;
|
||||
@@ -60,7 +62,8 @@ export class SeedRacingData {
|
||||
return;
|
||||
}
|
||||
|
||||
const seed = createRacingSeed();
|
||||
const persistence = getApiPersistence();
|
||||
const seed = createRacingSeed({ persistence });
|
||||
|
||||
let sponsorshipRequestsSeededViaRepo = false;
|
||||
const seedableSponsorshipRequests = this.seedDeps
|
||||
@@ -97,7 +100,7 @@ export class SeedRacingData {
|
||||
|
||||
const activeSeasons = seed.seasons.filter((season) => season.status.isActive());
|
||||
for (const season of activeSeasons) {
|
||||
const presetId = this.selectScoringPresetIdForSeason(season);
|
||||
const presetId = this.selectScoringPresetIdForSeason(season, persistence);
|
||||
const preset = getLeagueScoringPresetById(presetId);
|
||||
|
||||
if (!preset) {
|
||||
@@ -276,7 +279,7 @@ export class SeedRacingData {
|
||||
const existing = await this.seedDeps.leagueScoringConfigRepository.findBySeasonId(season.id);
|
||||
if (existing) continue;
|
||||
|
||||
const presetId = this.selectScoringPresetIdForSeason(season);
|
||||
const presetId = this.selectScoringPresetIdForSeason(season, 'postgres');
|
||||
const preset = getLeagueScoringPresetById(presetId);
|
||||
|
||||
if (!preset) {
|
||||
@@ -297,13 +300,16 @@ export class SeedRacingData {
|
||||
}
|
||||
}
|
||||
|
||||
private selectScoringPresetIdForSeason(season: Season): string {
|
||||
if (season.leagueId === 'league-5' && season.status.isActive()) {
|
||||
private selectScoringPresetIdForSeason(season: Season, persistence: 'postgres' | 'inmemory'): string {
|
||||
const expectedLeagueId = seedId('league-5', persistence);
|
||||
const expectedSeasonId = seedId('season-1-b', persistence);
|
||||
|
||||
if (season.leagueId === expectedLeagueId && season.status.isActive()) {
|
||||
return 'sprint-main-driver';
|
||||
}
|
||||
|
||||
if (season.leagueId === 'league-3') {
|
||||
return season.id.endsWith('-b') ? 'sprint-main-team' : 'club-default-nations';
|
||||
if (season.leagueId === seedId('league-3', persistence)) {
|
||||
return season.id === expectedSeasonId ? 'sprint-main-team' : 'club-default-nations';
|
||||
}
|
||||
|
||||
const match = /^league-(\d+)$/.exec(season.leagueId);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export class RacingDriverFactory {
|
||||
constructor(
|
||||
private readonly driverCount: number,
|
||||
private readonly baseDate: Date,
|
||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||
) {}
|
||||
|
||||
create(): Driver[] {
|
||||
@@ -14,7 +16,7 @@ export class RacingDriverFactory {
|
||||
const i = idx + 1;
|
||||
|
||||
return Driver.create({
|
||||
id: `driver-${i}`,
|
||||
id: seedId(`driver-${i}`, this.persistence),
|
||||
iracingId: String(100000 + i),
|
||||
name: faker.person.fullName(),
|
||||
country: faker.helpers.arrayElement(countries),
|
||||
|
||||
@@ -3,9 +3,13 @@ import { League } from '@core/racing/domain/entities/League';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import type { Friendship } from './RacingSeed';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export class RacingFeedFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
constructor(
|
||||
private readonly baseDate: Date,
|
||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||
) {}
|
||||
|
||||
create(drivers: Driver[], friendships: Friendship[], races: Race[], leagues: League[]): FeedItem[] {
|
||||
const items: FeedItem[] = [];
|
||||
@@ -18,13 +22,13 @@ export class RacingFeedFactory {
|
||||
const now = this.addMinutes(this.baseDate, 10);
|
||||
|
||||
for (let i = 2; i <= 10; i++) {
|
||||
const actor = drivers.find((d) => d.id === `driver-${i}`);
|
||||
const actor = drivers.find((d) => d.id === seedId(`driver-${i}`, this.persistence));
|
||||
if (!actor) continue;
|
||||
|
||||
if (!friendMap.has(`driver-1:${actor.id}`)) continue;
|
||||
|
||||
items.push({
|
||||
id: `feed:${actor.id}:joined:${i}`,
|
||||
id: seedId(`feed:${actor.id}:joined:${i}`, this.persistence),
|
||||
type: 'friend-joined-league',
|
||||
timestamp: this.addMinutes(now, -(i * 7)),
|
||||
actorDriverId: actor.id,
|
||||
@@ -38,7 +42,7 @@ export class RacingFeedFactory {
|
||||
|
||||
if (completedRace) {
|
||||
items.push({
|
||||
id: `feed:${actor.id}:result:${i}`,
|
||||
id: seedId(`feed:${actor.id}:result:${i}`, this.persistence),
|
||||
type: 'friend-finished-race',
|
||||
timestamp: this.addMinutes(now, -(i * 7 + 3)),
|
||||
actorDriverId: actor.id,
|
||||
@@ -56,7 +60,7 @@ export class RacingFeedFactory {
|
||||
|
||||
if (upcomingRace) {
|
||||
items.push({
|
||||
id: `feed:system:scheduled:${upcomingRace.id}`,
|
||||
id: seedId(`feed:system:scheduled:${upcomingRace.id}`, this.persistence),
|
||||
type: 'new-race-scheduled',
|
||||
timestamp: this.addMinutes(now, -3),
|
||||
leagueId: upcomingRace.leagueId,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export class RacingLeagueFactory {
|
||||
constructor(
|
||||
private readonly baseDate: Date,
|
||||
private readonly drivers: Driver[],
|
||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||
) {}
|
||||
|
||||
create(): League[] {
|
||||
@@ -54,7 +56,7 @@ export class RacingLeagueFactory {
|
||||
socialLinks?: { discordUrl?: string; youtubeUrl?: string; websiteUrl?: string };
|
||||
participantCount?: number;
|
||||
} = {
|
||||
id: `league-${i}`,
|
||||
id: seedId(`league-${i}`, this.persistence),
|
||||
name: faker.company.name() + ' Racing League',
|
||||
description: faker.lorem.sentences(2),
|
||||
ownerId: owner.id.toString(),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LeagueWalletId } from '@core/racing/domain/entities/league-wallet/Leagu
|
||||
import { Transaction } from '@core/racing/domain/entities/league-wallet/Transaction';
|
||||
import { TransactionId } from '@core/racing/domain/entities/league-wallet/TransactionId';
|
||||
import { Money } from '@core/racing/domain/value-objects/Money';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
type LeagueWalletSeed = {
|
||||
wallets: LeagueWallet[];
|
||||
@@ -12,7 +13,10 @@ type LeagueWalletSeed = {
|
||||
};
|
||||
|
||||
export class RacingLeagueWalletFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
constructor(
|
||||
private readonly baseDate: Date,
|
||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||
) {}
|
||||
|
||||
create(leagues: League[]): LeagueWalletSeed {
|
||||
const wallets: LeagueWallet[] = [];
|
||||
@@ -20,7 +24,7 @@ export class RacingLeagueWalletFactory {
|
||||
|
||||
for (const league of leagues) {
|
||||
const leagueId = league.id.toString();
|
||||
const walletId = `wallet-${leagueId}`;
|
||||
const walletId = seedId(`wallet-${leagueId}`, this.persistence);
|
||||
const createdAt = faker.date.past({ years: 2, refDate: this.baseDate });
|
||||
|
||||
// Ensure coverage:
|
||||
@@ -55,7 +59,7 @@ export class RacingLeagueWalletFactory {
|
||||
: faker.number.int({ min: 2, max: 18 });
|
||||
|
||||
for (let i = 0; i < transactionCount; i++) {
|
||||
const id = TransactionId.create(`tx-${leagueId}-${i + 1}`);
|
||||
const id = TransactionId.create(seedId(`tx-${leagueId}-${i + 1}`, this.persistence));
|
||||
const type = this.pickTransactionType(i, leagueId);
|
||||
const amount = this.pickAmount(type, currency, leagueId);
|
||||
|
||||
@@ -100,7 +104,7 @@ export class RacingLeagueWalletFactory {
|
||||
// Explicit edge-case: pending prize payout shows up in "pending payouts"
|
||||
if (leagueId === 'league-5') {
|
||||
const pendingPrize = Transaction.create({
|
||||
id: TransactionId.create(`tx-${leagueId}-pending-prize`),
|
||||
id: TransactionId.create(seedId(`tx-${leagueId}-pending-prize`, this.persistence)),
|
||||
walletId: LeagueWalletId.create(walletId),
|
||||
type: 'prize_payout',
|
||||
amount: Money.create(600, currency),
|
||||
@@ -108,7 +112,7 @@ export class RacingLeagueWalletFactory {
|
||||
createdAt: faker.date.recent({ days: 15, refDate: this.baseDate }),
|
||||
completedAt: undefined,
|
||||
description: 'Season prize pool payout (pending)',
|
||||
metadata: { seasonId: 'season-2', placement: 'P1-P3' },
|
||||
metadata: { seasonId: seedId('season-2', this.persistence), placement: 'P1-P3' },
|
||||
});
|
||||
|
||||
transactions.push(pendingPrize);
|
||||
|
||||
@@ -4,9 +4,13 @@ import { JoinRequest } from '@core/racing/domain/entities/JoinRequest';
|
||||
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export class RacingMembershipFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
constructor(
|
||||
private readonly baseDate: Date,
|
||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||
) {}
|
||||
|
||||
createLeagueMemberships(drivers: Driver[], leagues: League[]): LeagueMembership[] {
|
||||
const memberships: LeagueMembership[] = [];
|
||||
@@ -34,19 +38,20 @@ export class RacingMembershipFactory {
|
||||
|
||||
// Empty league: intentionally no memberships.
|
||||
// (Keep `league-2` empty if it exists.)
|
||||
// Widen the type to avoid TS2367 “no overlap” comparisons in some build modes.
|
||||
const emptyLeagueId: string | undefined = leagueById.has('league-2') ? ('league-2' as string) : undefined;
|
||||
// Widen the type to avoid TS2367 "no overlap" comparisons in some build modes.
|
||||
const emptyLeagueId: string | undefined = leagueById.has(seedId('league-2', this.persistence)) ? (seedId('league-2', this.persistence) as string) : undefined;
|
||||
|
||||
// Demo league: "full" + overbooked with pending/inactive members.
|
||||
const demoLeague = leagueById.get('league-5');
|
||||
const demoLeague = leagueById.get(seedId('league-5', this.persistence));
|
||||
if (demoLeague) {
|
||||
const maxDrivers = demoLeague.settings.maxDrivers ?? 32;
|
||||
const activeDrivers = drivers.slice(0, Math.min(maxDrivers, drivers.length));
|
||||
|
||||
activeDrivers.forEach((driver, idx) => {
|
||||
const driverId = driver.id.toString();
|
||||
const expectedDriverId = seedId('driver-1', this.persistence);
|
||||
const role =
|
||||
driverId === 'driver-1'
|
||||
driverId === expectedDriverId
|
||||
? 'owner'
|
||||
: idx === 1 || idx === 2
|
||||
? 'admin'
|
||||
@@ -71,7 +76,7 @@ export class RacingMembershipFactory {
|
||||
}
|
||||
|
||||
// League with mixed statuses and roles (but not full).
|
||||
const league1 = leagueById.get('league-1');
|
||||
const league1 = leagueById.get(seedId('league-1', this.persistence));
|
||||
if (league1) {
|
||||
const pick = drivers.slice(15, 25);
|
||||
pick.forEach((driver, idx) => {
|
||||
@@ -86,7 +91,7 @@ export class RacingMembershipFactory {
|
||||
}
|
||||
|
||||
// League with only pending memberships (tests "pending list" UX).
|
||||
const league4 = leagueById.get('league-4');
|
||||
const league4 = leagueById.get(seedId('league-4', this.persistence));
|
||||
if (league4) {
|
||||
drivers.slice(40, 48).forEach((driver, idx) => {
|
||||
add({
|
||||
@@ -106,10 +111,10 @@ export class RacingMembershipFactory {
|
||||
|
||||
for (const league of leagues) {
|
||||
const leagueId = league.id.toString();
|
||||
if (leagueId === 'league-5') continue;
|
||||
if (leagueId === seedId('league-5', this.persistence)) continue;
|
||||
if (emptyLeagueId && leagueId === emptyLeagueId) continue;
|
||||
|
||||
if (driverNumber % 11 === 0 && leagueId === 'league-3') {
|
||||
if (driverNumber % 11 === 0 && leagueId === seedId('league-3', this.persistence)) {
|
||||
add({
|
||||
leagueId,
|
||||
driverId,
|
||||
@@ -157,7 +162,7 @@ export class RacingMembershipFactory {
|
||||
};
|
||||
|
||||
// League with lots of requests + membership/request conflicts (everyone is a member of league-5 already).
|
||||
const demoLeagueId = 'league-5';
|
||||
const demoLeagueId = seedId('league-5', this.persistence);
|
||||
const demoDrivers = drivers.slice(10, 35);
|
||||
demoDrivers.forEach((driver, idx) => {
|
||||
const message =
|
||||
@@ -178,7 +183,7 @@ export class RacingMembershipFactory {
|
||||
});
|
||||
|
||||
// League with a few "normal" requests (only drivers who are NOT members already).
|
||||
const targetLeagueId = 'league-1';
|
||||
const targetLeagueId = seedId('league-1', this.persistence);
|
||||
const nonMembers = drivers
|
||||
.filter(driver => !membershipIds.has(`${targetLeagueId}:${driver.id.toString()}`))
|
||||
.slice(0, 6);
|
||||
@@ -193,11 +198,11 @@ export class RacingMembershipFactory {
|
||||
});
|
||||
|
||||
// Single request with no message (explicit id).
|
||||
const league3Exists = leagues.some(l => l.id.toString() === 'league-3');
|
||||
const league3Exists = leagues.some(l => l.id.toString() === seedId('league-3', this.persistence));
|
||||
if (league3Exists && drivers[0]) {
|
||||
addRequest({
|
||||
id: 'league-3-join-req-1',
|
||||
leagueId: 'league-3',
|
||||
id: seedId('league-3-join-req-1', this.persistence),
|
||||
leagueId: seedId('league-3', this.persistence),
|
||||
driverId: drivers[0].id.toString(),
|
||||
requestedAt: this.addDays(this.baseDate, -9),
|
||||
});
|
||||
@@ -206,16 +211,16 @@ export class RacingMembershipFactory {
|
||||
// Duplicate id edge case (last write wins in in-memory repo).
|
||||
if (drivers[1]) {
|
||||
addRequest({
|
||||
id: 'dup-league-join-req-1',
|
||||
leagueId: 'league-7',
|
||||
id: seedId('dup-league-join-req-1', this.persistence),
|
||||
leagueId: seedId('league-7', this.persistence),
|
||||
driverId: drivers[1].id.toString(),
|
||||
requestedAt: this.addDays(this.baseDate, -2),
|
||||
message: 'First request message (will be overwritten).',
|
||||
});
|
||||
|
||||
addRequest({
|
||||
id: 'dup-league-join-req-1',
|
||||
leagueId: 'league-7',
|
||||
id: seedId('dup-league-join-req-1', this.persistence),
|
||||
leagueId: seedId('league-7', this.persistence),
|
||||
driverId: drivers[1].id.toString(),
|
||||
requestedAt: this.addDays(this.baseDate, -1),
|
||||
message: 'Updated request message (duplicate id).',
|
||||
@@ -223,10 +228,11 @@ export class RacingMembershipFactory {
|
||||
}
|
||||
|
||||
// Explicit conflict: join request exists even though membership exists.
|
||||
const driver1 = drivers.find(d => d.id.toString() === 'driver-1');
|
||||
const expectedDriverId = seedId('driver-1', this.persistence);
|
||||
const driver1 = drivers.find(d => d.id.toString() === expectedDriverId);
|
||||
if (driver1) {
|
||||
addRequest({
|
||||
id: 'conflict-req-league-5-driver-1',
|
||||
id: seedId('conflict-req-league-5-driver-1', this.persistence),
|
||||
leagueId: demoLeagueId,
|
||||
driverId: driver1.id.toString(),
|
||||
requestedAt: this.addDays(this.baseDate, -15),
|
||||
@@ -311,12 +317,12 @@ export class RacingMembershipFactory {
|
||||
}
|
||||
|
||||
// Keep a tiny curated "happy path" for the demo league as well
|
||||
const upcomingDemoLeague = races.filter((r) => r.status.toString() === 'scheduled' && r.leagueId === 'league-5').slice(0, 3);
|
||||
const upcomingDemoLeague = races.filter((r) => r.status.toString() === 'scheduled' && r.leagueId === seedId('league-5', this.persistence)).slice(0, 3);
|
||||
for (const race of upcomingDemoLeague) {
|
||||
registrations.push(
|
||||
RaceRegistration.create({
|
||||
raceId: race.id,
|
||||
driverId: 'driver-1',
|
||||
driverId: seedId('driver-1', this.persistence),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { Track } from '@core/racing/domain/entities/Track';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export class RacingRaceFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
constructor(
|
||||
private readonly baseDate: Date,
|
||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||
) {}
|
||||
|
||||
create(leagues: League[], tracks: Track[]): Race[] {
|
||||
const cars = ['GT3 – Porsche 911', 'GT3 – BMW M4', 'LMP3 Prototype', 'GT4 – Alpine', 'Touring – Civic'];
|
||||
@@ -51,7 +55,7 @@ export class RacingRaceFactory {
|
||||
}
|
||||
|
||||
const base = {
|
||||
id: `race-${i}`,
|
||||
id: seedId(`race-${i}`, this.persistence),
|
||||
leagueId,
|
||||
scheduledAt,
|
||||
track: track.name.toString(),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { Result as RaceResult } from '@core/racing/domain/entities/result/Result';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export class RacingResultFactory {
|
||||
constructor(private readonly persistence: 'postgres' | 'inmemory' = 'inmemory') {}
|
||||
|
||||
create(drivers: Driver[], races: Race[]): RaceResult[] {
|
||||
const results: RaceResult[] = [];
|
||||
const completed = races.filter((r) => r.status.toString() === 'completed');
|
||||
@@ -50,7 +53,7 @@ export class RacingResultFactory {
|
||||
|
||||
results.push(
|
||||
RaceResult.create({
|
||||
id: `${race.id}:${driver.id}`,
|
||||
id: seedId(`${race.id}:${driver.id}`, this.persistence),
|
||||
raceId: race.id,
|
||||
driverId: driver.id,
|
||||
position,
|
||||
|
||||
@@ -6,9 +6,13 @@ import { SeasonSponsorship } from '@core/racing/domain/entities/season/SeasonSpo
|
||||
import type { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
|
||||
import type { SeasonStatusValue } from '@core/racing/domain/value-objects/SeasonStatus';
|
||||
import { Money } from '@core/racing/domain/value-objects/Money';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export class RacingSeasonSponsorshipFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
constructor(
|
||||
private readonly baseDate: Date,
|
||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||
) {}
|
||||
|
||||
createSeasons(leagues: League[]): Season[] {
|
||||
const seasons: Season[] = [];
|
||||
@@ -16,10 +20,10 @@ export class RacingSeasonSponsorshipFactory {
|
||||
for (const league of leagues) {
|
||||
const leagueId = league.id.toString();
|
||||
|
||||
if (leagueId === 'league-5') {
|
||||
if (leagueId === seedId('league-5', this.persistence)) {
|
||||
seasons.push(
|
||||
Season.create({
|
||||
id: 'season-1',
|
||||
id: seedId('season-1', this.persistence),
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: 'Season 1 (GT Sprint)',
|
||||
@@ -29,7 +33,7 @@ export class RacingSeasonSponsorshipFactory {
|
||||
startDate: this.daysFromBase(-30),
|
||||
}),
|
||||
Season.create({
|
||||
id: 'season-2',
|
||||
id: seedId('season-2', this.persistence),
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: 'Season 2 (Endurance Cup)',
|
||||
@@ -40,7 +44,7 @@ export class RacingSeasonSponsorshipFactory {
|
||||
endDate: this.daysFromBase(-60),
|
||||
}),
|
||||
Season.create({
|
||||
id: 'season-3',
|
||||
id: seedId('season-3', this.persistence),
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: 'Season 3 (Planned)',
|
||||
@@ -53,10 +57,10 @@ export class RacingSeasonSponsorshipFactory {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (leagueId === 'league-3') {
|
||||
if (leagueId === seedId('league-3', this.persistence)) {
|
||||
seasons.push(
|
||||
Season.create({
|
||||
id: 'league-3-season-a',
|
||||
id: seedId('league-3-season-a', this.persistence),
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: 'Split Season A',
|
||||
@@ -66,7 +70,7 @@ export class RacingSeasonSponsorshipFactory {
|
||||
startDate: this.daysFromBase(-10),
|
||||
}),
|
||||
Season.create({
|
||||
id: 'league-3-season-b',
|
||||
id: seedId('league-3-season-b', this.persistence),
|
||||
leagueId,
|
||||
gameId: 'iracing',
|
||||
name: 'Split Season B',
|
||||
@@ -80,16 +84,16 @@ export class RacingSeasonSponsorshipFactory {
|
||||
}
|
||||
|
||||
const baseYear = this.baseDate.getUTCFullYear();
|
||||
const seasonCount = leagueId === 'league-2' ? 1 : faker.number.int({ min: 1, max: 3 });
|
||||
const seasonCount = leagueId === seedId('league-2', this.persistence) ? 1 : faker.number.int({ min: 1, max: 3 });
|
||||
|
||||
for (let i = 0; i < seasonCount; i++) {
|
||||
const id = `${leagueId}-season-${i + 1}`;
|
||||
const id = seedId(`${leagueId}-season-${i + 1}`, this.persistence);
|
||||
const isFirst = i === 0;
|
||||
|
||||
const status: SeasonStatusValue =
|
||||
leagueId === 'league-1' && isFirst
|
||||
leagueId === seedId('league-1', this.persistence) && isFirst
|
||||
? 'active'
|
||||
: leagueId === 'league-2'
|
||||
: leagueId === seedId('league-2', this.persistence)
|
||||
? 'planned'
|
||||
: isFirst
|
||||
? faker.helpers.arrayElement(['active', 'planned'] as const)
|
||||
@@ -131,8 +135,9 @@ export class RacingSeasonSponsorshipFactory {
|
||||
const sponsorIds = sponsors.map((s) => s.id.toString());
|
||||
|
||||
for (const season of seasons) {
|
||||
const expectedSeasonId = seedId('season-1', this.persistence);
|
||||
const sponsorshipCount =
|
||||
season.id === 'season-1'
|
||||
season.id === expectedSeasonId
|
||||
? 2
|
||||
: season.status.isActive()
|
||||
? faker.number.int({ min: 0, max: 2 })
|
||||
@@ -152,7 +157,7 @@ export class RacingSeasonSponsorshipFactory {
|
||||
usedSponsorIds.add(sponsorId);
|
||||
|
||||
const base = SeasonSponsorship.create({
|
||||
id: `season-sponsorship-${season.id}-${i + 1}`,
|
||||
id: seedId(`season-sponsorship-${season.id}-${i + 1}`, this.persistence),
|
||||
seasonId: season.id,
|
||||
leagueId: season.leagueId,
|
||||
sponsorId,
|
||||
@@ -190,7 +195,8 @@ export class RacingSeasonSponsorshipFactory {
|
||||
const sponsorIds = sponsors.map((s) => s.id.toString());
|
||||
|
||||
for (const season of seasons) {
|
||||
const isHighTrafficDemo = season.id === 'season-1';
|
||||
const expectedSeasonId = seedId('season-1', this.persistence);
|
||||
const isHighTrafficDemo = season.id === expectedSeasonId;
|
||||
const maxRequests =
|
||||
isHighTrafficDemo
|
||||
? 8
|
||||
@@ -204,7 +210,7 @@ export class RacingSeasonSponsorshipFactory {
|
||||
|
||||
const sponsorId =
|
||||
isHighTrafficDemo && i === 0
|
||||
? 'demo-sponsor-1'
|
||||
? seedId('demo-sponsor-1', this.persistence)
|
||||
: faker.helpers.arrayElement(sponsorIds);
|
||||
|
||||
const offeredAmount = Money.create(
|
||||
@@ -230,7 +236,7 @@ export class RacingSeasonSponsorshipFactory {
|
||||
|
||||
requests.push(
|
||||
SponsorshipRequest.create({
|
||||
id: `sponsorship-request-${season.id}-${i + 1}`,
|
||||
id: seedId(`sponsorship-request-${season.id}-${i + 1}`, this.persistence),
|
||||
sponsorId,
|
||||
entityType: 'season',
|
||||
entityId: season.id,
|
||||
|
||||
@@ -62,6 +62,7 @@ export type RacingSeed = {
|
||||
export type RacingSeedOptions = {
|
||||
driverCount?: number;
|
||||
baseDate?: Date;
|
||||
persistence?: 'postgres' | 'inmemory';
|
||||
};
|
||||
|
||||
export const racingSeedDefaults: Readonly<
|
||||
@@ -69,33 +70,36 @@ export const racingSeedDefaults: Readonly<
|
||||
> = {
|
||||
driverCount: 100,
|
||||
baseDate: new Date(),
|
||||
persistence: 'inmemory',
|
||||
};
|
||||
|
||||
class RacingSeedFactory {
|
||||
private readonly driverCount: number;
|
||||
private readonly baseDate: Date;
|
||||
private readonly persistence: 'postgres' | 'inmemory';
|
||||
|
||||
constructor(options: RacingSeedOptions) {
|
||||
this.driverCount = options.driverCount ?? racingSeedDefaults.driverCount;
|
||||
this.baseDate = options.baseDate ?? racingSeedDefaults.baseDate;
|
||||
this.persistence = options.persistence ?? racingSeedDefaults.persistence;
|
||||
}
|
||||
|
||||
create(): RacingSeed {
|
||||
const driverFactory = new RacingDriverFactory(this.driverCount, this.baseDate);
|
||||
const driverFactory = new RacingDriverFactory(this.driverCount, this.baseDate, this.persistence);
|
||||
const trackFactory = new RacingTrackFactory();
|
||||
const raceFactory = new RacingRaceFactory(this.baseDate);
|
||||
const resultFactory = new RacingResultFactory();
|
||||
const raceFactory = new RacingRaceFactory(this.baseDate, this.persistence);
|
||||
const resultFactory = new RacingResultFactory(this.persistence);
|
||||
const standingFactory = new RacingStandingFactory();
|
||||
const membershipFactory = new RacingMembershipFactory(this.baseDate);
|
||||
const sponsorFactory = new RacingSponsorFactory(this.baseDate);
|
||||
const seasonSponsorshipFactory = new RacingSeasonSponsorshipFactory(this.baseDate);
|
||||
const leagueWalletFactory = new RacingLeagueWalletFactory(this.baseDate);
|
||||
const membershipFactory = new RacingMembershipFactory(this.baseDate, this.persistence);
|
||||
const sponsorFactory = new RacingSponsorFactory(this.baseDate, this.persistence);
|
||||
const seasonSponsorshipFactory = new RacingSeasonSponsorshipFactory(this.baseDate, this.persistence);
|
||||
const leagueWalletFactory = new RacingLeagueWalletFactory(this.baseDate, this.persistence);
|
||||
const friendshipFactory = new RacingFriendshipFactory();
|
||||
const feedFactory = new RacingFeedFactory(this.baseDate);
|
||||
const feedFactory = new RacingFeedFactory(this.baseDate, this.persistence);
|
||||
|
||||
const drivers = driverFactory.create();
|
||||
const tracks = trackFactory.create();
|
||||
const leagueFactory = new RacingLeagueFactory(this.baseDate, drivers);
|
||||
const leagueFactory = new RacingLeagueFactory(this.baseDate, drivers, this.persistence);
|
||||
const leagues = leagueFactory.create();
|
||||
const sponsors = sponsorFactory.create();
|
||||
const seasons = seasonSponsorshipFactory.createSeasons(leagues);
|
||||
@@ -107,7 +111,7 @@ class RacingSeedFactory {
|
||||
|
||||
const { wallets: leagueWallets, transactions: leagueWalletTransactions } = leagueWalletFactory.create(leagues);
|
||||
|
||||
const teamFactory = new RacingTeamFactory(this.baseDate);
|
||||
const teamFactory = new RacingTeamFactory(this.baseDate, this.persistence);
|
||||
const teams = teamFactory.createTeams(drivers, leagues);
|
||||
const races = raceFactory.create(leagues, tracks);
|
||||
const results = resultFactory.create(drivers, races);
|
||||
@@ -116,7 +120,7 @@ class RacingSeedFactory {
|
||||
const teamMemberships = teamFactory.createTeamMemberships(drivers, teams);
|
||||
const teamJoinRequests = teamFactory.createTeamJoinRequests(drivers, teams, teamMemberships);
|
||||
|
||||
const stewardingFactory = new RacingStewardingFactory(this.baseDate);
|
||||
const stewardingFactory = new RacingStewardingFactory(this.baseDate, this.persistence);
|
||||
const { protests, penalties } = stewardingFactory.create(races, drivers, leagueMemberships);
|
||||
|
||||
const friendships = friendshipFactory.create(drivers);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export class RacingSponsorFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
constructor(
|
||||
private readonly baseDate: Date,
|
||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||
) {}
|
||||
|
||||
create(): Sponsor[] {
|
||||
const demoSponsor = Sponsor.create({
|
||||
id: 'demo-sponsor-1',
|
||||
id: seedId('demo-sponsor-1', this.persistence),
|
||||
name: 'GridPilot Sim Racing Supply',
|
||||
contactEmail: 'partnerships@gridpilot.example',
|
||||
logoUrl: 'http://localhost:3000/images/header.jpeg',
|
||||
@@ -111,7 +115,7 @@ export class RacingSponsorFactory {
|
||||
const websiteUrl = websiteUrls[idx % websiteUrls.length]!;
|
||||
|
||||
return Sponsor.create({
|
||||
id: `sponsor-${i}`,
|
||||
id: seedId(`sponsor-${i}`, this.persistence),
|
||||
name,
|
||||
contactEmail: `partnerships+${safeName}@example.com`,
|
||||
...(logoUrl ? { logoUrl } : {}),
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { LeagueMembership } from '@core/racing/domain/entities/LeagueMember
|
||||
import { Protest } from '@core/racing/domain/entities/Protest';
|
||||
import type { Race } from '@core/racing/domain/entities/Race';
|
||||
import { Penalty } from '@core/racing/domain/entities/penalty/Penalty';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
type StewardingSeed = {
|
||||
protests: Protest[];
|
||||
@@ -11,7 +12,10 @@ type StewardingSeed = {
|
||||
};
|
||||
|
||||
export class RacingStewardingFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
constructor(
|
||||
private readonly baseDate: Date,
|
||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||
) {}
|
||||
|
||||
create(races: Race[], drivers: Driver[], leagueMemberships: LeagueMembership[]): StewardingSeed {
|
||||
const protests: Protest[] = [];
|
||||
@@ -57,7 +61,7 @@ export class RacingStewardingFactory {
|
||||
if (firstRace) {
|
||||
protests.push(
|
||||
Protest.create({
|
||||
id: 'protest-1',
|
||||
id: seedId('protest-1', this.persistence),
|
||||
raceId: firstRace.id.toString(),
|
||||
protestingDriverId: protester,
|
||||
accusedDriverId: accused,
|
||||
@@ -76,7 +80,7 @@ export class RacingStewardingFactory {
|
||||
// No penalty yet (pending), but seed a direct steward warning on same race.
|
||||
penalties.push(
|
||||
Penalty.create({
|
||||
id: 'penalty-1',
|
||||
id: seedId('penalty-1', this.persistence),
|
||||
leagueId: 'league-5',
|
||||
raceId: firstRace.id.toString(),
|
||||
driverId: accused,
|
||||
@@ -94,7 +98,7 @@ export class RacingStewardingFactory {
|
||||
if (secondRace) {
|
||||
protests.push(
|
||||
Protest.create({
|
||||
id: 'protest-2',
|
||||
id: seedId('protest-2', this.persistence),
|
||||
raceId: secondRace.id.toString(),
|
||||
protestingDriverId: spare,
|
||||
accusedDriverId: accused,
|
||||
@@ -113,14 +117,14 @@ export class RacingStewardingFactory {
|
||||
// Under review penalty still pending (linked to protest)
|
||||
penalties.push(
|
||||
Penalty.create({
|
||||
id: 'penalty-2',
|
||||
id: seedId('penalty-2', this.persistence),
|
||||
leagueId: 'league-5',
|
||||
raceId: secondRace.id.toString(),
|
||||
driverId: accused,
|
||||
type: 'time_penalty',
|
||||
value: 10,
|
||||
reason: 'Unsafe rejoin (protest pending review)',
|
||||
protestId: 'protest-2',
|
||||
protestId: seedId('protest-2', this.persistence),
|
||||
issuedBy: steward,
|
||||
status: 'pending',
|
||||
issuedAt: faker.date.recent({ days: 10, refDate: this.baseDate }),
|
||||
@@ -131,7 +135,7 @@ export class RacingStewardingFactory {
|
||||
|
||||
if (thirdRace) {
|
||||
const upheld = Protest.create({
|
||||
id: 'protest-3',
|
||||
id: seedId('protest-3', this.persistence),
|
||||
raceId: thirdRace.id.toString(),
|
||||
protestingDriverId: protester,
|
||||
accusedDriverId: spare,
|
||||
@@ -151,7 +155,7 @@ export class RacingStewardingFactory {
|
||||
|
||||
penalties.push(
|
||||
Penalty.create({
|
||||
id: 'penalty-3',
|
||||
id: seedId('penalty-3', this.persistence),
|
||||
leagueId: 'league-5',
|
||||
raceId: thirdRace.id.toString(),
|
||||
driverId: spare,
|
||||
@@ -168,7 +172,7 @@ export class RacingStewardingFactory {
|
||||
);
|
||||
|
||||
const dismissed = Protest.create({
|
||||
id: 'protest-4',
|
||||
id: seedId('protest-4', this.persistence),
|
||||
raceId: thirdRace.id.toString(),
|
||||
protestingDriverId: accused,
|
||||
accusedDriverId: protester,
|
||||
@@ -210,7 +214,7 @@ export class RacingStewardingFactory {
|
||||
const status = faker.helpers.arrayElement(['awaiting_defense', 'withdrawn'] as const);
|
||||
|
||||
const protest = Protest.create({
|
||||
id: `protest-${leagueId}-${race.id.toString()}`,
|
||||
id: seedId(`protest-${leagueId}-${race.id.toString()}`, this.persistence),
|
||||
raceId: race.id.toString(),
|
||||
protestingDriverId: a,
|
||||
accusedDriverId: b,
|
||||
@@ -239,7 +243,7 @@ export class RacingStewardingFactory {
|
||||
// A non-protest-linked penalty can still exist for the same race.
|
||||
penalties.push(
|
||||
Penalty.create({
|
||||
id: `penalty-${leagueId}-${race.id.toString()}`,
|
||||
id: seedId(`penalty-${leagueId}-${race.id.toString()}`, this.persistence),
|
||||
leagueId,
|
||||
raceId: race.id.toString(),
|
||||
driverId: b,
|
||||
|
||||
@@ -3,9 +3,13 @@ import { League } from '@core/racing/domain/entities/League';
|
||||
import { Team } from '@core/racing/domain/entities/Team';
|
||||
import type { TeamJoinRequest, TeamMembership } from '@core/racing/domain/types/TeamMembership';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { seedId } from './SeedIdHelper';
|
||||
|
||||
export class RacingTeamFactory {
|
||||
constructor(private readonly baseDate: Date) {}
|
||||
constructor(
|
||||
private readonly baseDate: Date,
|
||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||
) {}
|
||||
|
||||
createTeams(drivers: Driver[], leagues: League[]): Team[] {
|
||||
const teamCount = 15;
|
||||
@@ -19,7 +23,7 @@ export class RacingTeamFactory {
|
||||
);
|
||||
|
||||
return Team.create({
|
||||
id: `team-${i}`,
|
||||
id: seedId(`team-${i}`, this.persistence),
|
||||
name: faker.company.name() + ' Racing',
|
||||
tag: faker.string.alpha({ length: 4, casing: 'upper' }),
|
||||
description: faker.lorem.sentences(2),
|
||||
@@ -128,7 +132,7 @@ export class RacingTeamFactory {
|
||||
|
||||
candidateDriverIds.forEach((driverId, idx) => {
|
||||
addRequest({
|
||||
id: `team-join-${team1.id.toString()}-${driverId}`,
|
||||
id: seedId(`team-join-${team1.id.toString()}-${driverId}`, this.persistence),
|
||||
teamId: team1.id.toString(),
|
||||
driverId,
|
||||
requestedAt: this.addDays(this.baseDate, -(5 + idx)),
|
||||
@@ -142,7 +146,7 @@ export class RacingTeamFactory {
|
||||
|
||||
// Conflict edge case: owner submits a join request to own team.
|
||||
addRequest({
|
||||
id: `team-join-${team1.id.toString()}-${team1.ownerId.toString()}-conflict`,
|
||||
id: seedId(`team-join-${team1.id.toString()}-${team1.ownerId.toString()}-conflict`, this.persistence),
|
||||
teamId: team1.id.toString(),
|
||||
driverId: team1.ownerId.toString(),
|
||||
requestedAt: this.addDays(this.baseDate, -1),
|
||||
@@ -154,7 +158,7 @@ export class RacingTeamFactory {
|
||||
if (team3 && drivers[0]) {
|
||||
const driverId = drivers[0].id.toString();
|
||||
addRequest({
|
||||
id: 'dup-team-join-req-1',
|
||||
id: seedId('dup-team-join-req-1', this.persistence),
|
||||
teamId: team3.id.toString(),
|
||||
driverId,
|
||||
requestedAt: this.addDays(this.baseDate, -10),
|
||||
@@ -162,7 +166,7 @@ export class RacingTeamFactory {
|
||||
});
|
||||
|
||||
addRequest({
|
||||
id: 'dup-team-join-req-1',
|
||||
id: seedId('dup-team-join-req-1', this.persistence),
|
||||
teamId: team3.id.toString(),
|
||||
driverId,
|
||||
requestedAt: this.addDays(this.baseDate, -9),
|
||||
|
||||
128
adapters/bootstrap/racing/SeedIdHelper.test.ts
Normal file
128
adapters/bootstrap/racing/SeedIdHelper.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { stableUuidFromSeedKey, seedId, seedUuid } from './SeedIdHelper';
|
||||
|
||||
describe('SeedIdHelper', () => {
|
||||
describe('stableUuidFromSeedKey', () => {
|
||||
it('should return a valid UUID v4 format', () => {
|
||||
const uuid = stableUuidFromSeedKey('team-3');
|
||||
|
||||
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||
// where y is one of [8, 9, a, b]
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
expect(uuid).toMatch(uuidRegex);
|
||||
});
|
||||
|
||||
it('should be deterministic - same input produces same output', () => {
|
||||
const input = 'driver-1';
|
||||
const uuid1 = stableUuidFromSeedKey(input);
|
||||
const uuid2 = stableUuidFromSeedKey(input);
|
||||
|
||||
expect(uuid1).toBe(uuid2);
|
||||
});
|
||||
|
||||
it('should produce different UUIDs for different inputs', () => {
|
||||
const uuid1 = stableUuidFromSeedKey('team-1');
|
||||
const uuid2 = stableUuidFromSeedKey('team-2');
|
||||
|
||||
expect(uuid1).not.toBe(uuid2);
|
||||
});
|
||||
|
||||
it('should handle various seed patterns', () => {
|
||||
const testCases = [
|
||||
'team-3',
|
||||
'driver-1',
|
||||
'league-5',
|
||||
'race-10',
|
||||
'sponsor-1',
|
||||
'season-1',
|
||||
'demo-sponsor-1',
|
||||
'league-3-season-a',
|
||||
'team-join-team-1-driver-5',
|
||||
'dup-team-join-req-1',
|
||||
];
|
||||
|
||||
testCases.forEach((seed) => {
|
||||
const uuid = stableUuidFromSeedKey(seed);
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
expect(uuid).toMatch(uuidRegex);
|
||||
expect(uuid.length).toBe(36);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const uuid = stableUuidFromSeedKey('');
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
expect(uuid).toMatch(uuidRegex);
|
||||
});
|
||||
|
||||
it('should handle long seed strings', () => {
|
||||
const longSeed = 'a'.repeat(1000);
|
||||
const uuid = stableUuidFromSeedKey(longSeed);
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
expect(uuid).toMatch(uuidRegex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('seedId', () => {
|
||||
it('should return UUID for postgres mode', () => {
|
||||
const result = seedId('team-3', 'postgres');
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
expect(result).toMatch(uuidRegex);
|
||||
});
|
||||
|
||||
it('should return original string for inmemory mode', () => {
|
||||
const result = seedId('team-3', 'inmemory');
|
||||
expect(result).toBe('team-3');
|
||||
});
|
||||
|
||||
it('should be deterministic for postgres mode', () => {
|
||||
const result1 = seedId('driver-5', 'postgres');
|
||||
const result2 = seedId('driver-5', 'postgres');
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
|
||||
it('should preserve original string for inmemory mode', () => {
|
||||
const testCases = ['team-3', 'driver-1', 'league-5', 'race-10'];
|
||||
|
||||
testCases.forEach((seed) => {
|
||||
const result = seedId(seed, 'inmemory');
|
||||
expect(result).toBe(seed);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('seedUuid', () => {
|
||||
it('should return a valid UUID', () => {
|
||||
const uuid = seedUuid('team-3');
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
expect(uuid).toMatch(uuidRegex);
|
||||
});
|
||||
|
||||
it('should be deterministic', () => {
|
||||
const uuid1 = seedUuid('driver-1');
|
||||
const uuid2 = seedUuid('driver-1');
|
||||
expect(uuid1).toBe(uuid2);
|
||||
});
|
||||
|
||||
it('should produce different UUIDs for different inputs', () => {
|
||||
const uuid1 = seedUuid('team-1');
|
||||
const uuid2 = seedUuid('team-2');
|
||||
expect(uuid1).not.toBe(uuid2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UUID v4 compliance', () => {
|
||||
it('should have version 4 bits set correctly', () => {
|
||||
const uuid = stableUuidFromSeedKey('test');
|
||||
// Version is in the 13th character (0-indexed), should be '4'
|
||||
expect(uuid.charAt(14)).toBe('4');
|
||||
});
|
||||
|
||||
it('should have variant bits set correctly', () => {
|
||||
const uuid = stableUuidFromSeedKey('test');
|
||||
// Variant is in the 19th character (0-indexed), should be 8, 9, a, or b
|
||||
const variantChar = uuid.charAt(19);
|
||||
expect(['8', '9', 'a', 'b']).toContain(variantChar.toLowerCase());
|
||||
});
|
||||
});
|
||||
});
|
||||
76
adapters/bootstrap/racing/SeedIdHelper.ts
Normal file
76
adapters/bootstrap/racing/SeedIdHelper.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
/**
|
||||
* Generates a deterministic UUID v4 from a seed string.
|
||||
*
|
||||
* This is used for postgres seeding to ensure all IDs are valid UUIDs
|
||||
* while maintaining determinism across runs (important for tests and reproducible seeds).
|
||||
*
|
||||
* The function uses SHA-256 hash of the seed, then formats it as a UUID v4
|
||||
* with proper version (0100) and variant (10xx) bits.
|
||||
*
|
||||
* @param seedKey - The deterministic seed string (e.g., "team-3", "driver-1")
|
||||
* @returns A valid UUID v4 string
|
||||
*
|
||||
* @example
|
||||
* stableUuidFromSeedKey("team-3")
|
||||
* // Returns: "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" (deterministic)
|
||||
*/
|
||||
export function stableUuidFromSeedKey(seedKey: string): string {
|
||||
// Create a deterministic hash from the seed
|
||||
const hash = createHash('sha256').update(seedKey).digest('hex');
|
||||
|
||||
// Take first 32 characters for UUID
|
||||
const uuidHex = hash.substring(0, 32);
|
||||
|
||||
// Format as UUID v4 with proper version and variant bits
|
||||
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||
// where x is any hex digit, y is one of [8, 9, a, b]
|
||||
|
||||
// Build the UUID step by step
|
||||
const part1 = uuidHex.substring(0, 8);
|
||||
const part2 = uuidHex.substring(8, 12);
|
||||
const part3 = '4' + uuidHex.substring(13, 16); // Version 4
|
||||
const variantChar = ['8', '9', 'a', 'b'][parseInt(uuidHex.charAt(16), 16) % 4];
|
||||
const part4 = variantChar + uuidHex.substring(17, 20);
|
||||
const part5 = uuidHex.substring(20, 32);
|
||||
|
||||
return `${part1}-${part2}-${part3}-${part4}-${part5}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ID appropriate for the current persistence mode.
|
||||
*
|
||||
* For postgres mode: returns a deterministic UUID
|
||||
* For inmemory mode: returns the original deterministic string ID
|
||||
*
|
||||
* @param seedKey - The deterministic seed string (e.g., "team-3")
|
||||
* @param mode - The persistence mode
|
||||
* @returns Either a UUID (postgres) or the original string (inmemory)
|
||||
*
|
||||
* @example
|
||||
* seedId("team-3", "postgres") // Returns UUID
|
||||
* seedId("team-3", "inmemory") // Returns "team-3"
|
||||
*/
|
||||
export function seedId(seedKey: string, mode: 'postgres' | 'inmemory'): string {
|
||||
if (mode === 'postgres') {
|
||||
return stableUuidFromSeedKey(seedKey);
|
||||
}
|
||||
return seedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a UUID for postgres seeding.
|
||||
*
|
||||
* Convenience wrapper around seedId with mode already set to 'postgres'.
|
||||
* Use this when you know you're in postgres mode.
|
||||
*
|
||||
* @param seedKey - The deterministic seed string
|
||||
* @returns A valid UUID v4 string
|
||||
*
|
||||
* @example
|
||||
* seedUuid("driver-1") // Returns UUID
|
||||
*/
|
||||
export function seedUuid(seedKey: string): string {
|
||||
return stableUuidFromSeedKey(seedKey);
|
||||
}
|
||||
Reference in New Issue
Block a user