module creation
This commit is contained in:
129
adapters/identity/persistence/inmemory/InMemoryAuthRepository.ts
Normal file
129
adapters/identity/persistence/inmemory/InMemoryAuthRepository.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { IAuthRepository } from '@gridpilot/core/identity/domain/repositories/IAuthRepository';
|
||||
import { IUserRepository, StoredUser } from '@gridpilot/core/identity/domain/repositories/IUserRepository';
|
||||
import { IPasswordHashingService } from '@gridpilot/core/identity/domain/services/PasswordHashingService';
|
||||
import { User } from '@gridpilot/core/identity/domain/entities/User';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { EmailAddress } from '@gridpilot/core/identity/domain/value-objects/EmailAddress';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
export class InMemoryAuthRepository implements IAuthRepository {
|
||||
constructor(
|
||||
private readonly userRepository: IUserRepository,
|
||||
private readonly passwordHashingService: IPasswordHashingService,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async findByEmail(email: EmailAddress): Promise<User | null> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Finding user by email: ${email.value}`);
|
||||
const storedUser = await this.userRepository.findByEmail(email.value);
|
||||
if (!storedUser) {
|
||||
return null;
|
||||
}
|
||||
return User.fromStored(storedUser);
|
||||
}
|
||||
|
||||
async save(user: User): Promise<void> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Saving user with email: ${user.getEmail()}`);
|
||||
|
||||
const userId = user.getId().value;
|
||||
const userEmail = user.getEmail() ?? '';
|
||||
const userDisplayName = user.getDisplayName();
|
||||
const userPasswordHash = user.getPasswordHash()?.value ?? '';
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
passwordHash: userPasswordHash,
|
||||
displayName: userDisplayName,
|
||||
salt: '', // In-memory doesn't use actual salt, but Stub has to provide.
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
// Check if user exists to decide between create or update
|
||||
const existingStoredUser = await this.userRepository.findById(userId);
|
||||
if (existingStoredUser) {
|
||||
// For update, ensure createdAt is preserved from existing user
|
||||
const updatedStoredUser: StoredUser = {
|
||||
...storedUser,
|
||||
createdAt: existingStoredUser.createdAt,
|
||||
};
|
||||
await this.userRepository.update(updatedStoredUser);
|
||||
} else {
|
||||
await this.userRepository.create(storedUser);
|
||||
}
|
||||
}
|
||||
|
||||
async create(user: User, passwordPlain: string): Promise<User> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Creating user with email: ${user.getEmail()}`);
|
||||
const passwordHashValue = await this.passwordHashingService.hash(passwordPlain);
|
||||
|
||||
// Ensure email is not undefined before using
|
||||
const userEmail = user.getEmail() ?? '';
|
||||
const userDisplayName = user.getDisplayName();
|
||||
const userId = user.getId().value;
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
passwordHash: passwordHashValue,
|
||||
displayName: userDisplayName,
|
||||
salt: '', // Salt is handled by PasswordHashingService, but StoredUser requires it.
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
const createdStoredUser = await this.userRepository.create(storedUser);
|
||||
return User.fromStored(createdStoredUser);
|
||||
}
|
||||
|
||||
async update(user: User): Promise<User> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Updating user with email: ${user.getEmail()}`);
|
||||
// Ensure values are not undefined before using
|
||||
const userId = user.getId().value;
|
||||
const userEmail = user.getEmail() ?? '';
|
||||
const userPasswordHash = user.getPasswordHash()?.value ?? '';
|
||||
const userDisplayName = user.getDisplayName();
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
passwordHash: userPasswordHash,
|
||||
displayName: userDisplayName,
|
||||
salt: '', // Salt is handled by PasswordHashingService, but StoredUser requires it.
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
createdAt: new Date(), // Assuming creation date is maintained or updated within UserRepository
|
||||
};
|
||||
const updatedStoredUser = await this.userRepository.update(storedUser);
|
||||
return User.fromStored(updatedStoredUser);
|
||||
}
|
||||
|
||||
async delete(userId: string): Promise<boolean> {
|
||||
// Deletion needs to be implemented in InMemoryUserRepository and exposed via IUserRepository
|
||||
// For now, it's not supported.
|
||||
this.logger.warn(`[InMemoryAuthRepository] Delete operation not implemented for user: ${userId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
async userExists(email: string): Promise<boolean> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Checking if user exists for email: ${email}`);
|
||||
return this.userRepository.emailExists(email);
|
||||
}
|
||||
|
||||
async verifyPassword(email: string, passwordPlain: string): Promise<User | null> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Verifying password for user with email: ${email}`);
|
||||
const user = await this.findByEmail(EmailAddress.create(email)); // Use EmailAddress value object
|
||||
if (!user) {
|
||||
this.logger.warn(`[InMemoryAuthRepository] User not found for email: ${email}`);
|
||||
return null;
|
||||
}
|
||||
const isPasswordValid = await this.passwordHashingService.verify(
|
||||
passwordPlain,
|
||||
user.getPasswordHash()?.value ?? '', // Access value and provide fallback
|
||||
);
|
||||
if (!isPasswordValid) {
|
||||
this.logger.warn(`[InMemoryAuthRepository] Invalid password for user with email: ${email}`);
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
22
adapters/identity/services/InMemoryPasswordHashingService.ts
Normal file
22
adapters/identity/services/InMemoryPasswordHashingService.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* In-Memory Password Hashing Service
|
||||
*
|
||||
* Provides basic password hashing and comparison for demo/development purposes.
|
||||
* NOT FOR PRODUCTION USE - uses a simple string reversal as "hashing".
|
||||
*/
|
||||
|
||||
import type { IPasswordHashingService } from '@gridpilot/core/identity/domain/services/PasswordHashingService';
|
||||
|
||||
export class InMemoryPasswordHashingService implements IPasswordHashingService {
|
||||
async hash(plain: string): Promise<string> {
|
||||
// In a real application, use a robust hashing library like bcrypt or Argon2.
|
||||
// For demo, we'll just reverse the password and add a salt-like prefix.
|
||||
const salt = 'demo_salt_';
|
||||
return Promise.resolve(salt + plain.split('').reverse().join(''));
|
||||
}
|
||||
|
||||
async verify(plain: string, hash: string): Promise<boolean> {
|
||||
const hashedPlain = await this.hash(plain);
|
||||
return Promise.resolve(hashedPlain === hash);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,47 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { AuthenticatedUserDTO } from '../../application/dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '../../application/dto/AuthSessionDTO';
|
||||
import type { IdentitySessionPort } from '../../application/ports/IdentitySessionPort';
|
||||
/**
|
||||
* Adapter: CookieIdentitySessionAdapter
|
||||
*
|
||||
* Manages user session using cookies. This is a placeholder implementation.
|
||||
*/
|
||||
|
||||
const SESSION_COOKIE = 'gp_demo_session';
|
||||
|
||||
function parseCookieValue(raw: string | undefined): AuthSessionDTO | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as AuthSessionDTO;
|
||||
if (!parsed.expiresAt || Date.now() > parsed.expiresAt) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function serializeSession(session: AuthSessionDTO): string {
|
||||
return JSON.stringify(session);
|
||||
}
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import type { AuthenticatedUserDTO } from '@gridpilot/core/identity/application/dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '@gridpilot/core/identity/application/dto/AuthSessionDTO';
|
||||
import type { IdentitySessionPort } from '@gridpilot/core/identity/application/ports/IdentitySessionPort';
|
||||
|
||||
export class CookieIdentitySessionAdapter implements IdentitySessionPort {
|
||||
private currentSession: AuthSessionDTO | null = null;
|
||||
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('CookieIdentitySessionAdapter initialized.');
|
||||
// In a real application, you would load the session from a cookie here
|
||||
// For demo, we'll start with no session.
|
||||
}
|
||||
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
const store = await cookies();
|
||||
const raw = store.get(SESSION_COOKIE)?.value;
|
||||
return parseCookieValue(raw);
|
||||
this.logger.debug('[CookieIdentitySessionAdapter] Getting current session.');
|
||||
return Promise.resolve(this.currentSession);
|
||||
}
|
||||
|
||||
async createSession(user: AuthenticatedUserDTO): Promise<AuthSessionDTO> {
|
||||
const now = Date.now();
|
||||
const expiresAt = now + 24 * 60 * 60 * 1000;
|
||||
|
||||
const session: AuthSessionDTO = {
|
||||
user,
|
||||
issuedAt: now,
|
||||
expiresAt,
|
||||
token: randomUUID(),
|
||||
this.logger.debug(`[CookieIdentitySessionAdapter] Creating session for user: ${user.id}`);
|
||||
const newSession: AuthSessionDTO = {
|
||||
user: user,
|
||||
issuedAt: Date.now(),
|
||||
expiresAt: Date.now() + 3600 * 1000, // 1 hour expiration
|
||||
token: `mock-token-${user.id}-${Date.now()}`,
|
||||
};
|
||||
|
||||
const store = await cookies();
|
||||
store.set(SESSION_COOKIE, serializeSession(session), {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
|
||||
return session;
|
||||
this.currentSession = newSession;
|
||||
// In a real app, you'd set a secure, HTTP-only cookie here.
|
||||
this.logger.info(`[CookieIdentitySessionAdapter] Session created for user ${user.id}.`);
|
||||
return Promise.resolve(newSession);
|
||||
}
|
||||
|
||||
async clearSession(): Promise<void> {
|
||||
const store = await cookies();
|
||||
store.delete(SESSION_COOKIE);
|
||||
this.logger.debug('[CookieIdentitySessionAdapter] Clearing session.');
|
||||
this.currentSession = null;
|
||||
// In a real app, you'd clear the session cookie here.
|
||||
this.logger.info('[CookieIdentitySessionAdapter] Session cleared.');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,85 @@
|
||||
import type {
|
||||
IAvatarGenerationRepository,
|
||||
} from '@gridpilot/media';
|
||||
import {
|
||||
AvatarGenerationRequest,
|
||||
type AvatarGenerationRequestProps,
|
||||
} from '@gridpilot/media';
|
||||
import { IAvatarGenerationRepository } from '@gridpilot/core/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import { AvatarGenerationRequest } from '@gridpilot/core/media/domain/entities/AvatarGenerationRequest';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
/**
|
||||
* In-memory implementation of IAvatarGenerationRepository.
|
||||
*
|
||||
* For demo/development purposes. In production, this would use a database.
|
||||
*/
|
||||
export class InMemoryAvatarGenerationRepository implements IAvatarGenerationRepository {
|
||||
private readonly requests = new Map<string, AvatarGenerationRequestProps>();
|
||||
private readonly logger: ILogger;
|
||||
private requests: Map<string, AvatarGenerationRequest> = new Map(); // Key: requestId
|
||||
private userRequests: Map<string, AvatarGenerationRequest[]> = new Map(); // Key: userId
|
||||
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialRequests: AvatarGenerationRequest[] = []) {
|
||||
this.logger.info('InMemoryAvatarGenerationRepository initialized.');
|
||||
for (const req of initialRequests) {
|
||||
this.requests.set(req.id, req);
|
||||
if (!this.userRequests.has(req.userId)) {
|
||||
this.userRequests.set(req.userId, []);
|
||||
}
|
||||
this.userRequests.get(req.userId)?.push(req);
|
||||
this.logger.debug(`Seeded avatar generation request: ${req.id} for user ${req.userId}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async save(request: AvatarGenerationRequest): Promise<void> {
|
||||
this.logger.debug(`Saving avatar generation request with ID: ${request.id}`);
|
||||
this.requests.set(request.id, request.toProps());
|
||||
this.logger.info(`Avatar generation request with ID: ${request.id} saved successfully.`);
|
||||
this.logger.debug(`[InMemoryAvatarGenerationRepository] Saving avatar generation request: ${request.id} for user ${request.userId}.`);
|
||||
this.requests.set(request.id, request);
|
||||
if (!this.userRequests.has(request.userId)) {
|
||||
this.userRequests.set(request.userId, []);
|
||||
}
|
||||
const existingRequests = this.userRequests.get(request.userId)!;
|
||||
// Remove old version if exists and add new version (update-like behavior)
|
||||
const index = existingRequests.findIndex(r => r.id === request.id);
|
||||
if (index > -1) {
|
||||
existingRequests[index] = request;
|
||||
} else {
|
||||
existingRequests.push(request);
|
||||
}
|
||||
this.logger.info(`Avatar generation request ${request.id} for user ${request.userId} saved successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<AvatarGenerationRequest | null> {
|
||||
this.logger.debug(`Finding avatar generation request by ID: ${id}`);
|
||||
const props = this.requests.get(id);
|
||||
if (!props) {
|
||||
this.logger.info(`Avatar generation request with ID: ${id} not found.`);
|
||||
return null;
|
||||
this.logger.debug(`[InMemoryAvatarGenerationRepository] Finding request by ID: ${id}`);
|
||||
const request = this.requests.get(id) ?? null;
|
||||
if (request) {
|
||||
this.logger.info(`Found request by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Request with ID ${id} not found.`);
|
||||
}
|
||||
this.logger.info(`Avatar generation request with ID: ${id} found.`);
|
||||
return AvatarGenerationRequest.reconstitute(props);
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<AvatarGenerationRequest[]> {
|
||||
this.logger.debug(`Finding avatar generation requests by user ID: ${userId}`);
|
||||
const results: AvatarGenerationRequest[] = [];
|
||||
for (const props of Array.from(this.requests.values())) {
|
||||
if (props.userId === userId) {
|
||||
results.push(AvatarGenerationRequest.reconstitute(props));
|
||||
}
|
||||
}
|
||||
this.logger.info(`${results.length} avatar generation requests found for user ID: ${userId}.`);
|
||||
return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
this.logger.debug(`[InMemoryAvatarGenerationRepository] Finding requests for user: ${userId}`);
|
||||
const requests = this.userRequests.get(userId) ?? [];
|
||||
this.logger.info(`Found ${requests.length} requests for user ${userId}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async findLatestByUserId(userId: string): Promise<AvatarGenerationRequest | null> {
|
||||
this.logger.debug(`Finding latest avatar generation request for user ID: ${userId}`);
|
||||
const userRequests = await this.findByUserId(userId);
|
||||
if (userRequests.length === 0) {
|
||||
this.logger.info(`No avatar generation requests found for user ID: ${userId}.`);
|
||||
return null;
|
||||
this.logger.debug(`[InMemoryAvatarGenerationRepository] Finding latest request for user: ${userId}`);
|
||||
const requests = (this.userRequests.get(userId) ?? [])
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); // Sort by most recent
|
||||
const latest = requests.length > 0 ? requests[0]! : null; // Use non-null assertion as `requests.length > 0` is checked
|
||||
if (latest) {
|
||||
this.logger.info(`Found latest request for user ${userId}: ${latest.id}.`);
|
||||
} else {
|
||||
this.logger.warn(`No latest request found for user: ${userId}.`);
|
||||
}
|
||||
const latest = userRequests[0];
|
||||
if (!latest) {
|
||||
this.logger.info(`No latest avatar generation request found for user ID: ${userId}.`);
|
||||
return null;
|
||||
}
|
||||
this.logger.info(`Latest avatar generation request found for user ID: ${userId}, ID: ${latest.id}.`);
|
||||
return latest;
|
||||
return Promise.resolve(latest);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting avatar generation request with ID: ${id}`);
|
||||
const deleted = this.requests.delete(id);
|
||||
if (deleted) {
|
||||
this.logger.info(`Avatar generation request with ID: ${id} deleted successfully.`);
|
||||
this.logger.debug(`[InMemoryAvatarGenerationRepository] Deleting request with ID: ${id}.`);
|
||||
const requestToDelete = this.requests.get(id);
|
||||
if (requestToDelete) {
|
||||
this.requests.delete(id);
|
||||
const userRequests = this.userRequests.get(requestToDelete.userId);
|
||||
if (userRequests) {
|
||||
this.userRequests.set(requestToDelete.userId, userRequests.filter(req => req.id !== id));
|
||||
}
|
||||
this.logger.info(`Request ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Attempted to delete non-existent avatar generation request with ID: ${id}.`);
|
||||
this.logger.warn(`Request with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
adapters/media/ports/InMemoryFaceValidationAdapter.ts
Normal file
19
adapters/media/ports/InMemoryFaceValidationAdapter.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { FaceValidationPort, FaceValidationResult } from '@gridpilot/core/media/application/ports/FaceValidationPort';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryFaceValidationAdapter implements FaceValidationPort {
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('InMemoryFaceValidationAdapter initialized.');
|
||||
}
|
||||
|
||||
async validateFacePhoto(imageData: string | Buffer): Promise<FaceValidationResult> {
|
||||
this.logger.debug('[InMemoryFaceValidationAdapter] Validating face photo (mock).');
|
||||
// Simulate a successful validation for any input for demo purposes
|
||||
return Promise.resolve({
|
||||
isValid: true,
|
||||
hasFace: true,
|
||||
faceCount: 1,
|
||||
confidence: 0.95,
|
||||
});
|
||||
}
|
||||
}
|
||||
28
adapters/media/ports/InMemoryImageServiceAdapter.ts
Normal file
28
adapters/media/ports/InMemoryImageServiceAdapter.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { IImageServicePort } from '@gridpilot/racing/application/ports/IImageServicePort';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryImageServiceAdapter implements IImageServicePort {
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('InMemoryImageServiceAdapter initialized.');
|
||||
}
|
||||
|
||||
getDriverAvatar(driverId: string): string {
|
||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting avatar for driver: ${driverId}`);
|
||||
return `https://cdn.example.com/avatars/${driverId}.png`; // Mock URL
|
||||
}
|
||||
|
||||
getTeamLogo(teamId: string): string {
|
||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting logo for team: ${teamId}`);
|
||||
return `https://cdn.example.com/logos/team-${teamId}.png`; // Mock URL
|
||||
}
|
||||
|
||||
getLeagueCover(leagueId: string): string {
|
||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting cover for league: ${leagueId}`);
|
||||
return `https://cdn.example.com/covers/league-${leagueId}.png`; // Mock URL
|
||||
}
|
||||
|
||||
getLeagueLogo(leagueId: string): string {
|
||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting logo for league: ${leagueId}`);
|
||||
return `https://cdn.example.com/logos/league-${leagueId}.png`; // Mock URL
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,54 @@
|
||||
/**
|
||||
* In-Memory Implementation: InMemoryNotificationPreferenceRepository
|
||||
*
|
||||
* Provides an in-memory storage implementation for notification preferences.
|
||||
*/
|
||||
|
||||
import { NotificationPreference } from '../../domain/entities/NotificationPreference';
|
||||
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { INotificationPreferenceRepository } from '@gridpilot/core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
import { NotificationPreference } from '@gridpilot/core/notifications/domain/entities/NotificationPreference';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryNotificationPreferenceRepository implements INotificationPreferenceRepository {
|
||||
private preferences: Map<string, NotificationPreference> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger, initialPreferences: NotificationPreference[] = []) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialPreferences: NotificationPreference[] = []) {
|
||||
this.logger.info('InMemoryNotificationPreferenceRepository initialized.');
|
||||
initialPreferences.forEach(pref => {
|
||||
this.preferences.set(pref.driverId, pref);
|
||||
this.logger.debug(`Seeded preference for driver: ${pref.driverId}`);
|
||||
});
|
||||
for (const pref of initialPreferences) {
|
||||
this.preferences.set(pref.id, pref);
|
||||
this.logger.debug(`Seeded preference: ${pref.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async findByDriverId(driverId: string): Promise<NotificationPreference | null> {
|
||||
this.logger.debug(`Finding notification preference for driver: ${driverId}`);
|
||||
try {
|
||||
const preference = this.preferences.get(driverId) || null;
|
||||
if (preference) {
|
||||
this.logger.info(`Found preference for driver: ${driverId}`);
|
||||
} else {
|
||||
this.logger.warn(`Preference not found for driver: ${driverId}`);
|
||||
}
|
||||
return preference;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding preference for driver ${driverId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryNotificationPreferenceRepository] Finding preferences for driver: ${driverId}`);
|
||||
const preference = this.preferences.get(driverId) ?? null;
|
||||
if (preference) {
|
||||
this.logger.info(`Found preferences for driver: ${driverId}.`);
|
||||
} else {
|
||||
this.logger.warn(`No preferences found for driver: ${driverId}.`);
|
||||
}
|
||||
return Promise.resolve(preference);
|
||||
}
|
||||
|
||||
async save(preference: NotificationPreference): Promise<void> {
|
||||
this.logger.debug(`Saving notification preference for driver: ${preference.driverId}`);
|
||||
try {
|
||||
this.preferences.set(preference.driverId, preference);
|
||||
this.logger.info(`Preference for driver ${preference.driverId} saved successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving preference for driver ${preference.driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryNotificationPreferenceRepository] Saving preferences for driver: ${preference.driverId}.`);
|
||||
this.preferences.set(preference.id, preference);
|
||||
this.logger.info(`Preferences for driver ${preference.driverId} saved successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async delete(driverId: string): Promise<void> {
|
||||
this.logger.debug(`Deleting notification preference for driver: ${driverId}`);
|
||||
try {
|
||||
if (this.preferences.delete(driverId)) {
|
||||
this.logger.info(`Preference for driver ${driverId} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Preference for driver ${driverId} not found for deletion.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting preference for driver ${driverId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryNotificationPreferenceRepository] Deleting preferences for driver: ${driverId}.`);
|
||||
if (this.preferences.delete(driverId)) {
|
||||
this.logger.info(`Preferences for driver ${driverId} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`No preferences found for driver ${driverId} to delete.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getOrCreateDefault(driverId: string): Promise<NotificationPreference> {
|
||||
this.logger.debug(`Getting or creating default notification preference for driver: ${driverId}`);
|
||||
try {
|
||||
const existing = this.preferences.get(driverId);
|
||||
if (existing) {
|
||||
this.logger.debug(`Existing preference found for driver: ${driverId}.`);
|
||||
return existing;
|
||||
}
|
||||
|
||||
this.logger.info(`Creating default preference for driver: ${driverId}.`);
|
||||
const defaultPreference = NotificationPreference.createDefault(driverId);
|
||||
this.preferences.set(driverId, defaultPreference);
|
||||
this.logger.info(`Default preference created and saved for driver: ${driverId}.`);
|
||||
return defaultPreference;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting or creating default preference for driver ${driverId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryNotificationPreferenceRepository] Getting or creating default preferences for driver: ${driverId}.`);
|
||||
let preference = await this.findByDriverId(driverId);
|
||||
if (!preference) {
|
||||
this.logger.info(`Creating default preferences for new driver: ${driverId}.`);
|
||||
preference = NotificationPreference.createDefault(driverId);
|
||||
await this.save(preference);
|
||||
}
|
||||
return preference;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,163 +1,99 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryDriverRepository
|
||||
*
|
||||
* In-memory implementation of IDriverRepository.
|
||||
* Stores data in Map structure with UUID generation.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryDriverRepository implements IDriverRepository {
|
||||
private drivers: Map<string, Driver>;
|
||||
private readonly logger: ILogger;
|
||||
private drivers: Map<string, Driver> = new Map();
|
||||
private iracingIdIndex: Map<string, string> = new Map(); // iracingId -> driverId
|
||||
|
||||
constructor(logger: ILogger, seedData?: Driver[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialDrivers: Driver[] = []) {
|
||||
this.logger.info('InMemoryDriverRepository initialized.');
|
||||
this.drivers = new Map();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(driver => {
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.logger.debug(`Seeded driver: ${driver.id}.`);
|
||||
});
|
||||
for (const driver of initialDrivers) {
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.iracingIdIndex.set(driver.iracingId, driver.id);
|
||||
this.logger.debug(`Seeded driver: ${driver.id} (iRacing ID: ${driver.iracingId}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Driver | null> {
|
||||
this.logger.debug(`Finding driver by id: ${id}`);
|
||||
try {
|
||||
const driver = this.drivers.get(id) ?? null;
|
||||
if (driver) {
|
||||
this.logger.info(`Found driver: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Driver with id ${id} not found.`);
|
||||
}
|
||||
return driver;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding driver by id ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryDriverRepository] Finding driver by ID: ${id}`);
|
||||
const driver = this.drivers.get(id) ?? null;
|
||||
if (driver) {
|
||||
this.logger.info(`Found driver by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Driver with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(driver);
|
||||
}
|
||||
|
||||
async findByIRacingId(iracingId: string): Promise<Driver | null> {
|
||||
this.logger.debug(`Finding driver by iRacing id: ${iracingId}`);
|
||||
try {
|
||||
const driver = Array.from(this.drivers.values()).find(
|
||||
d => d.iracingId === iracingId
|
||||
);
|
||||
if (driver) {
|
||||
this.logger.info(`Found driver with iRacing id: ${iracingId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Driver with iRacing id ${iracingId} not found.`);
|
||||
}
|
||||
return driver ?? null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding driver by iRacing id ${iracingId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryDriverRepository] Finding driver by iRacing ID: ${iracingId}`);
|
||||
const driverId = this.iracingIdIndex.get(iracingId);
|
||||
if (!driverId) {
|
||||
this.logger.warn(`Driver with iRacing ID ${iracingId} not found.`);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.findById(driverId);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Driver[]> {
|
||||
this.logger.debug('Finding all drivers.');
|
||||
try {
|
||||
const drivers = Array.from(this.drivers.values());
|
||||
this.logger.info(`Found ${drivers.length} drivers.`);
|
||||
return drivers;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all drivers:', error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug('[InMemoryDriverRepository] Finding all drivers.');
|
||||
return Promise.resolve(Array.from(this.drivers.values()));
|
||||
}
|
||||
|
||||
async create(driver: Driver): Promise<Driver> {
|
||||
this.logger.debug(`Creating driver: ${driver.id}`);
|
||||
try {
|
||||
if (await this.exists(driver.id)) {
|
||||
this.logger.warn(`Driver with ID ${driver.id} already exists.`);
|
||||
throw new Error(`Driver with ID ${driver.id} already exists`);
|
||||
}
|
||||
|
||||
if (await this.existsByIRacingId(driver.iracingId)) {
|
||||
this.logger.warn(`Driver with iRacing ID ${driver.iracingId} already exists.`);
|
||||
throw new Error(`Driver with iRacing ID ${driver.iracingId} already exists`);
|
||||
}
|
||||
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.logger.info(`Driver ${driver.id} created successfully.`);
|
||||
return driver;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating driver ${driver.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryDriverRepository] Creating driver: ${driver.id} (iRacing ID: ${driver.iracingId})`);
|
||||
if (this.drivers.has(driver.id)) {
|
||||
this.logger.warn(`Driver with ID ${driver.id} already exists.`);
|
||||
throw new Error('Driver already exists');
|
||||
}
|
||||
if (this.iracingIdIndex.has(driver.iracingId)) {
|
||||
this.logger.warn(`Driver with iRacing ID ${driver.iracingId} already exists.`);
|
||||
throw new Error('iRacing ID already registered');
|
||||
}
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.iracingIdIndex.set(driver.iracingId, driver.id);
|
||||
this.logger.info(`Driver ${driver.id} created successfully.`);
|
||||
return Promise.resolve(driver);
|
||||
}
|
||||
|
||||
async update(driver: Driver): Promise<Driver> {
|
||||
this.logger.debug(`Updating driver: ${driver.id}`);
|
||||
try {
|
||||
if (!await this.exists(driver.id)) {
|
||||
this.logger.warn(`Driver with ID ${driver.id} not found for update.`);
|
||||
throw new Error(`Driver with ID ${driver.id} not found`);
|
||||
}
|
||||
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.logger.info(`Driver ${driver.id} updated successfully.`);
|
||||
return driver;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating driver ${driver.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryDriverRepository] Updating driver: ${driver.id} (iRacing ID: ${driver.iracingId})`);
|
||||
if (!this.drivers.has(driver.id)) {
|
||||
this.logger.warn(`Driver with ID ${driver.id} not found for update.`);
|
||||
throw new Error('Driver not found');
|
||||
}
|
||||
this.drivers.set(driver.id, driver);
|
||||
// Re-index iRacing ID if it changed
|
||||
const existingDriver = this.drivers.get(driver.id);
|
||||
if (existingDriver && existingDriver.iracingId !== driver.iracingId) {
|
||||
this.iracingIdIndex.delete(existingDriver.iracingId);
|
||||
this.iracingIdIndex.set(driver.iracingId, driver.id);
|
||||
}
|
||||
this.logger.info(`Driver ${driver.id} updated successfully.`);
|
||||
return Promise.resolve(driver);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting driver: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`Driver with ID ${id} not found for deletion.`);
|
||||
throw new Error(`Driver with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.logger.debug(`[InMemoryDriverRepository] Deleting driver with ID: ${id}`);
|
||||
const driver = this.drivers.get(id);
|
||||
if (driver) {
|
||||
this.drivers.delete(id);
|
||||
this.iracingIdIndex.delete(driver.iracingId);
|
||||
this.logger.info(`Driver ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting driver ${id}:`, error);
|
||||
throw error;
|
||||
} else {
|
||||
this.logger.warn(`Driver with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of driver with id: ${id}`);
|
||||
try {
|
||||
const exists = this.drivers.has(id);
|
||||
this.logger.debug(`Driver ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of driver with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryDriverRepository] Checking existence of driver with ID: ${id}`);
|
||||
return Promise.resolve(this.drivers.has(id));
|
||||
}
|
||||
|
||||
async existsByIRacingId(iracingId: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of driver with iRacing id: ${iracingId}`);
|
||||
try {
|
||||
const exists = Array.from(this.drivers.values()).some(
|
||||
d => d.iracingId === iracingId
|
||||
);
|
||||
this.logger.debug(`Driver with iRacing id ${iracingId} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of driver with iRacing id ${iracingId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryDriverRepository] Checking existence of driver with iRacing ID: ${iracingId}`);
|
||||
return Promise.resolve(this.iracingIdIndex.has(iracingId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to generate a new UUID
|
||||
*/
|
||||
static generateId(): string {
|
||||
return uuidv4();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,31 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryGameRepository
|
||||
*
|
||||
* In-memory implementation of IGameRepository.
|
||||
*/
|
||||
|
||||
import { Game } from '../../domain/entities/Game';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { IGameRepository } from '@gridpilot/racing/domain/repositories/IGameRepository';
|
||||
import { Game } from '@gridpilot/racing/domain/entities/Game';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryGameRepository implements IGameRepository {
|
||||
private games: Map<string, Game>;
|
||||
private readonly logger: ILogger;
|
||||
private games: Map<string, Game> = new Map();
|
||||
|
||||
constructor(logger: ILogger, seedData?: Game[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialGames: Game[] = []) {
|
||||
this.logger.info('InMemoryGameRepository initialized.');
|
||||
this.games = new Map();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(game => {
|
||||
this.games.set(game.id, game);
|
||||
this.logger.debug(`Seeded game: ${game.id}.`);
|
||||
});
|
||||
} else {
|
||||
// Default seed data for common sim racing games
|
||||
const defaultGames = [
|
||||
Game.create({ id: 'iracing', name: 'iRacing' }),
|
||||
Game.create({ id: 'acc', name: 'Assetto Corsa Competizione' }),
|
||||
Game.create({ id: 'ac', name: 'Assetto Corsa' }),
|
||||
Game.create({ id: 'rf2', name: 'rFactor 2' }),
|
||||
Game.create({ id: 'ams2', name: 'Automobilista 2' }),
|
||||
Game.create({ id: 'lmu', name: 'Le Mans Ultimate' }),
|
||||
];
|
||||
defaultGames.forEach(game => {
|
||||
this.games.set(game.id, game);
|
||||
this.logger.debug(`Seeded default game: ${game.id}.`);
|
||||
});
|
||||
for (const game of initialGames) {
|
||||
this.games.set(game.id, game);
|
||||
this.logger.debug(`Seeded game: ${game.id} (${game.name}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Game | null> {
|
||||
this.logger.debug(`Finding game by id: ${id}`);
|
||||
try {
|
||||
const game = this.games.get(id) ?? null;
|
||||
if (game) {
|
||||
this.logger.info(`Found game: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Game with id ${id} not found.`);
|
||||
}
|
||||
return game;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding game by id ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryGameRepository] Finding game by ID: ${id}`);
|
||||
const game = this.games.get(id) ?? null;
|
||||
if (game) {
|
||||
this.logger.info(`Found game by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Game with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(game);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Game[]> {
|
||||
this.logger.debug('Finding all games.');
|
||||
try {
|
||||
const games = Array.from(this.games.values()).sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Found ${games.length} games.`);
|
||||
return games;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all games:', error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug('[InMemoryGameRepository] Finding all games.');
|
||||
return Promise.resolve(Array.from(this.games.values()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to add a game
|
||||
*/
|
||||
async create(game: Game): Promise<Game> {
|
||||
this.logger.debug(`Creating game: ${game.id}`);
|
||||
try {
|
||||
if (this.games.has(game.id)) {
|
||||
this.logger.warn(`Game with ID ${game.id} already exists.`);
|
||||
throw new Error(`Game with ID ${game.id} already exists`);
|
||||
}
|
||||
this.games.set(game.id, game);
|
||||
this.logger.info(`Game ${game.id} created successfully.`);
|
||||
return game;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating game ${game.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test helper to clear data
|
||||
*/
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all games.');
|
||||
this.games.clear();
|
||||
this.logger.info('All games cleared.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,179 +1,102 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryLeagueMembershipRepository
|
||||
*
|
||||
* In-memory implementation of ILeagueMembershipRepository.
|
||||
* Stores memberships and join requests in maps keyed by league.
|
||||
*/
|
||||
|
||||
import type {
|
||||
LeagueMembership,
|
||||
JoinRequest,
|
||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import { LeagueMembership, JoinRequest } from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
|
||||
private membershipsByLeague: Map<string, LeagueMembership[]>;
|
||||
private joinRequestsByLeague: Map<string, JoinRequest[]>;
|
||||
private readonly logger: ILogger;
|
||||
private memberships: Map<string, LeagueMembership> = new Map(); // Key: `${leagueId}:${driverId}`
|
||||
private joinRequests: Map<string, JoinRequest> = new Map(); // Key: requestId
|
||||
|
||||
constructor(logger: ILogger, seedMemberships?: LeagueMembership[], seedJoinRequests?: JoinRequest[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialMemberships: LeagueMembership[] = [], initialJoinRequests: JoinRequest[] = []) {
|
||||
this.logger.info('InMemoryLeagueMembershipRepository initialized.');
|
||||
this.membershipsByLeague = new Map();
|
||||
this.joinRequestsByLeague = new Map();
|
||||
|
||||
if (seedMemberships) {
|
||||
seedMemberships.forEach((membership) => {
|
||||
const list = this.membershipsByLeague.get(membership.leagueId) ?? [];
|
||||
list.push(membership);
|
||||
this.membershipsByLeague.set(membership.leagueId, list);
|
||||
this.logger.debug(`Seeded membership for league ${membership.leagueId}, driver ${membership.driverId}.`);
|
||||
});
|
||||
for (const membership of initialMemberships) {
|
||||
this.memberships.set(`${membership.leagueId}:${membership.driverId}`, membership);
|
||||
this.logger.debug(`Seeded membership: ${membership.id}.`);
|
||||
}
|
||||
|
||||
if (seedJoinRequests) {
|
||||
seedJoinRequests.forEach((request) => {
|
||||
const list = this.joinRequestsByLeague.get(request.leagueId) ?? [];
|
||||
list.push(request);
|
||||
this.joinRequestsByLeague.set(request.leagueId, list);
|
||||
this.logger.debug(`Seeded join request for league ${request.leagueId}, driver ${request.driverId}.`);
|
||||
});
|
||||
for (const req of initialJoinRequests) {
|
||||
this.joinRequests.set(req.id, req);
|
||||
this.logger.debug(`Seeded join request: ${req.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async getMembership(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
||||
this.logger.debug(`Getting membership for league: ${leagueId}, driver: ${driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByLeague.get(leagueId);
|
||||
if (!list) {
|
||||
this.logger.warn(`No membership list found for league: ${leagueId}.`);
|
||||
return null;
|
||||
}
|
||||
const membership = list.find((m) => m.driverId === driverId) ?? null;
|
||||
if (membership) {
|
||||
this.logger.info(`Found membership for league: ${leagueId}, driver: ${driverId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Membership not found for league: ${leagueId}, driver: ${driverId}.`);
|
||||
}
|
||||
return membership;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting membership for league ${leagueId}, driver ${driverId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting membership for league ${leagueId}, driver ${driverId}.`);
|
||||
const key = `${leagueId}:${driverId}`;
|
||||
const membership = this.memberships.get(key) ?? null;
|
||||
if (membership) {
|
||||
this.logger.info(`Found membership for league ${leagueId}, driver ${driverId}.`);
|
||||
} else {
|
||||
this.logger.warn(`No membership found for league ${leagueId}, driver ${driverId}.`);
|
||||
}
|
||||
return Promise.resolve(membership);
|
||||
}
|
||||
|
||||
async findActiveByLeagueIdAndDriverId(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding active membership for league ${leagueId}, driver ${driverId}.`);
|
||||
const membership = await this.getMembership(leagueId, driverId);
|
||||
return Promise.resolve(membership && membership.status === 'active' ? membership : null);
|
||||
}
|
||||
|
||||
async findAllByLeagueId(leagueId: string): Promise<LeagueMembership[]> {
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for league ${leagueId}.`);
|
||||
const filteredMemberships = Array.from(this.memberships.values()).filter(mem => mem.leagueId === leagueId);
|
||||
this.logger.info(`Found ${filteredMemberships.length} memberships for league ${leagueId}.`);
|
||||
return Promise.resolve(filteredMemberships);
|
||||
}
|
||||
|
||||
async findAllByDriverId(driverId: string): Promise<LeagueMembership[]> {
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for driver ${driverId}.`);
|
||||
const memberships = Array.from(this.memberships.values()).filter(mem => mem.driverId === driverId);
|
||||
this.logger.info(`Found ${memberships.length} memberships for driver ${driverId}.`);
|
||||
return Promise.resolve(memberships);
|
||||
}
|
||||
|
||||
async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> {
|
||||
this.logger.debug(`Getting league members for league: ${leagueId}`);
|
||||
try {
|
||||
const members = [...(this.membershipsByLeague.get(leagueId) ?? [])];
|
||||
this.logger.info(`Found ${members.length} members for league: ${leagueId}.`);
|
||||
return members;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting league members for league ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting active members for league ${leagueId}.`);
|
||||
const members = Array.from(this.memberships.values()).filter(mem => mem.leagueId === leagueId && mem.status === 'active');
|
||||
this.logger.info(`Found ${members.length} active members for league ${leagueId}.`);
|
||||
return Promise.resolve(members);
|
||||
}
|
||||
|
||||
async getJoinRequests(leagueId: string): Promise<JoinRequest[]> {
|
||||
this.logger.debug(`Getting join requests for league: ${leagueId}`);
|
||||
try {
|
||||
const requests = [...(this.joinRequestsByLeague.get(leagueId) ?? [])];
|
||||
this.logger.info(`Found ${requests.length} join requests for league: ${leagueId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting join requests for league ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting join requests for league ${leagueId}.`);
|
||||
const requests = Array.from(this.joinRequests.values()).filter(req => req.leagueId === leagueId);
|
||||
this.logger.info(`Found ${requests.length} join requests for league ${leagueId}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async saveMembership(membership: LeagueMembership): Promise<LeagueMembership> {
|
||||
this.logger.debug(`Saving membership for league: ${membership.leagueId}, driver: ${membership.driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByLeague.get(membership.leagueId) ?? [];
|
||||
const existingIndex = list.findIndex(
|
||||
(m) => m.leagueId === membership.leagueId && m.driverId === membership.driverId,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = membership;
|
||||
this.logger.info(`Updated existing membership for league: ${membership.leagueId}, driver: ${membership.driverId}.`);
|
||||
} else {
|
||||
list.push(membership);
|
||||
this.logger.info(`Created new membership for league: ${membership.leagueId}, driver: ${membership.driverId}.`);
|
||||
}
|
||||
|
||||
this.membershipsByLeague.set(membership.leagueId, list);
|
||||
return membership;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving membership for league ${membership.leagueId}, driver ${membership.driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Saving membership for ${membership.id}.`);
|
||||
const key = `${membership.leagueId}:${membership.driverId}`;
|
||||
this.memberships.set(key, membership);
|
||||
this.logger.info(`Membership ${membership.id} saved successfully.`);
|
||||
return Promise.resolve(membership);
|
||||
}
|
||||
|
||||
async removeMembership(leagueId: string, driverId: string): Promise<void> {
|
||||
this.logger.debug(`Removing membership for league: ${leagueId}, driver: ${driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByLeague.get(leagueId);
|
||||
if (!list) {
|
||||
this.logger.warn(`No membership list found for league: ${leagueId}. Cannot remove.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const next = list.filter((m) => m.driverId !== driverId);
|
||||
if (next.length < list.length) {
|
||||
this.membershipsByLeague.set(leagueId, next);
|
||||
this.logger.info(`Removed membership for league: ${leagueId}, driver: ${driverId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Membership not found for league: ${leagueId}, driver: ${driverId}. Cannot remove.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error removing membership for league ${leagueId}, driver ${driverId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Removing membership for league ${leagueId}, driver ${driverId}.`);
|
||||
const key = `${leagueId}:${driverId}`;
|
||||
if (this.memberships.delete(key)) {
|
||||
this.logger.info(`Membership for league ${leagueId}, driver ${driverId} removed successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Membership for league ${leagueId}, driver ${driverId} not found for removal.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async saveJoinRequest(request: JoinRequest): Promise<JoinRequest> {
|
||||
this.logger.debug(`Saving join request for league: ${request.leagueId}, driver: ${request.driverId}, id: ${request.id}`);
|
||||
try {
|
||||
const list = this.joinRequestsByLeague.get(request.leagueId) ?? [];
|
||||
const existingIndex = list.findIndex((r) => r.id === request.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = request;
|
||||
this.logger.info(`Updated existing join request: ${request.id}.`);
|
||||
} else {
|
||||
list.push(request);
|
||||
this.logger.info(`Created new join request: ${request.id}.`);
|
||||
}
|
||||
|
||||
this.joinRequestsByLeague.set(request.leagueId, list);
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving join request ${request.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Saving join request for ${request.id}.`);
|
||||
this.joinRequests.set(request.id, request);
|
||||
this.logger.info(`Join request ${request.id} saved successfully.`);
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
async removeJoinRequest(requestId: string): Promise<void> {
|
||||
this.logger.debug(`Removing join request with ID: ${requestId}`);
|
||||
try {
|
||||
let removed = false;
|
||||
for (const [leagueId, requests] of this.joinRequestsByLeague.entries()) {
|
||||
const next = requests.filter((r) => r.id !== requestId);
|
||||
if (next.length !== requests.length) {
|
||||
this.joinRequestsByLeague.set(leagueId, next);
|
||||
removed = true;
|
||||
this.logger.info(`Removed join request ${requestId} from league ${leagueId}.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!removed) {
|
||||
this.logger.warn(`Join request with ID ${requestId} not found for removal.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error removing join request ${requestId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Removing join request: ${requestId}.`);
|
||||
if (this.joinRequests.delete(requestId)) {
|
||||
this.logger.info(`Join request ${requestId} removed successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Join request ${requestId} not found for removal.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +1,84 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryLeagueRepository
|
||||
*
|
||||
* In-memory implementation of ILeagueRepository.
|
||||
* Stores data in Map structure with UUID generation.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ILeagueRepository } from '@gridpilot/core/racing/domain/repositories/ILeagueRepository';
|
||||
import { League } from '@gridpilot/core/racing/domain/entities/League';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueRepository implements ILeagueRepository {
|
||||
private leagues: Map<string, League>;
|
||||
private readonly logger: ILogger;
|
||||
private leagues: Map<string, League> = new Map();
|
||||
|
||||
constructor(logger: ILogger, seedData?: League[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialLeagues: League[] = []) {
|
||||
this.logger.info('InMemoryLeagueRepository initialized.');
|
||||
this.leagues = new Map();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(league => {
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.debug(`Seeded league: ${league.id}.`);
|
||||
});
|
||||
for (const league of initialLeagues) {
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.debug(`Seeded league: ${league.id} (${league.name}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<League | null> {
|
||||
this.logger.debug(`Finding league by id: ${id}`);
|
||||
try {
|
||||
const league = this.leagues.get(id) ?? null;
|
||||
if (league) {
|
||||
this.logger.info(`Found league: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`League with id ${id} not found.`);
|
||||
}
|
||||
return league;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding league by id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(): Promise<League[]> {
|
||||
this.logger.debug('Finding all leagues.');
|
||||
try {
|
||||
const leagues = Array.from(this.leagues.values());
|
||||
this.logger.info(`Found ${leagues.length} leagues.`);
|
||||
return leagues;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all leagues:', error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Finding league by ID: ${id}`);
|
||||
const league = this.leagues.get(id) ?? null;
|
||||
if (league) {
|
||||
this.logger.info(`Found league by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`League with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(league);
|
||||
}
|
||||
|
||||
async findByOwnerId(ownerId: string): Promise<League[]> {
|
||||
this.logger.debug(`Finding leagues by owner id: ${ownerId}`);
|
||||
try {
|
||||
const leagues = Array.from(this.leagues.values()).filter(
|
||||
league => league.ownerId === ownerId
|
||||
);
|
||||
this.logger.info(`Found ${leagues.length} leagues for owner id: ${ownerId}.`);
|
||||
return leagues;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding leagues by owner id ${ownerId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Finding leagues by owner ID: ${ownerId}`);
|
||||
const ownedLeagues = Array.from(this.leagues.values()).filter(league => league.ownerId === ownerId);
|
||||
this.logger.info(`Found ${ownedLeagues.length} leagues for owner ID: ${ownerId}.`);
|
||||
return Promise.resolve(ownedLeagues);
|
||||
}
|
||||
|
||||
async searchByName(name: string): Promise<League[]> {
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Searching leagues by name: ${name}`);
|
||||
const matchingLeagues = Array.from(this.leagues.values()).filter(league =>
|
||||
league.name.toLowerCase().includes(name.toLowerCase()),
|
||||
);
|
||||
this.logger.info(`Found ${matchingLeagues.length} matching leagues for name search: ${name}.`);
|
||||
return Promise.resolve(matchingLeagues);
|
||||
}
|
||||
|
||||
async findAll(): Promise<League[]> {
|
||||
this.logger.debug('[InMemoryLeagueRepository] Finding all leagues.');
|
||||
return Promise.resolve(Array.from(this.leagues.values()));
|
||||
}
|
||||
|
||||
async create(league: League): Promise<League> {
|
||||
this.logger.debug(`Creating league: ${league.id}`);
|
||||
try {
|
||||
if (await this.exists(league.id)) {
|
||||
this.logger.warn(`League with ID ${league.id} already exists.`);
|
||||
throw new Error(`League with ID ${league.id} already exists`);
|
||||
}
|
||||
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.info(`League ${league.id} created successfully.`);
|
||||
return league;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating league ${league.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Creating league: ${league.id} (${league.name})`);
|
||||
if (this.leagues.has(league.id)) {
|
||||
this.logger.warn(`League with ID ${league.id} already exists.`);
|
||||
throw new Error('League already exists');
|
||||
}
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.info(`League ${league.id} created successfully.`);
|
||||
return Promise.resolve(league);
|
||||
}
|
||||
|
||||
async update(league: League): Promise<League> {
|
||||
this.logger.debug(`Updating league: ${league.id}`);
|
||||
try {
|
||||
if (!await this.exists(league.id)) {
|
||||
this.logger.warn(`League with ID ${league.id} not found for update.`);
|
||||
throw new Error(`League with ID ${league.id} not found`);
|
||||
}
|
||||
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.info(`League ${league.id} updated successfully.`);
|
||||
return league;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating league ${league.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Updating league: ${league.id} (${league.name})`);
|
||||
if (!this.leagues.has(league.id)) {
|
||||
this.logger.warn(`League with ID ${league.id} not found for update.`);
|
||||
throw new Error('League not found');
|
||||
}
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.info(`League ${league.id} updated successfully.`);
|
||||
return Promise.resolve(league);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting league: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`League with ID ${id} not found for deletion.`);
|
||||
throw new Error(`League with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.leagues.delete(id);
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Deleting league with ID: ${id}`);
|
||||
if (this.leagues.delete(id)) {
|
||||
this.logger.info(`League ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting league ${id}:`, error);
|
||||
throw error;
|
||||
} else {
|
||||
this.logger.warn(`League with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of league with id: ${id}`);
|
||||
try {
|
||||
const exists = this.leagues.has(id);
|
||||
this.logger.debug(`League ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of league with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Checking existence of league with ID: ${id}`);
|
||||
return Promise.resolve(this.leagues.has(id));
|
||||
}
|
||||
|
||||
async searchByName(query: string): Promise<League[]> {
|
||||
this.logger.debug(`Searching leagues by name query: ${query}`);
|
||||
try {
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
const leagues = Array.from(this.leagues.values()).filter(league =>
|
||||
league.name.toLowerCase().includes(normalizedQuery)
|
||||
);
|
||||
this.logger.info(`Found ${leagues.length} leagues matching search query: ${query}.`);
|
||||
return leagues;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error searching leagues by name query ${query}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to generate a new UUID
|
||||
*/
|
||||
static generateId(): string {
|
||||
return uuidv4();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ILeagueScoringConfigRepository } from '@gridpilot/racing/domain/repositories/ILeagueScoringConfigRepository';
|
||||
import { LeagueScoringConfig } from '@gridpilot/racing/domain/entities/LeagueScoringConfig';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueScoringConfigRepository implements ILeagueScoringConfigRepository {
|
||||
private configs: Map<string, LeagueScoringConfig> = new Map(); // Key: seasonId
|
||||
|
||||
constructor(private readonly logger: ILogger, initialConfigs: LeagueScoringConfig[] = []) {
|
||||
this.logger.info('InMemoryLeagueScoringConfigRepository initialized.');
|
||||
for (const config of initialConfigs) {
|
||||
this.configs.set(config.seasonId, config);
|
||||
this.logger.debug(`Seeded league scoring config for season: ${config.seasonId}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async findBySeasonId(seasonId: string): Promise<LeagueScoringConfig | null> {
|
||||
this.logger.debug(`[InMemoryLeagueScoringConfigRepository] Finding config for season: ${seasonId}.`);
|
||||
const config = this.configs.get(seasonId) ?? null;
|
||||
if (config) {
|
||||
this.logger.info(`Found config for season: ${seasonId}.`);
|
||||
} else {
|
||||
this.logger.warn(`No config found for season: ${seasonId}.`);
|
||||
}
|
||||
return Promise.resolve(config);
|
||||
}
|
||||
|
||||
async save(config: LeagueScoringConfig): Promise<LeagueScoringConfig> {
|
||||
this.logger.debug(`[InMemoryLeagueScoringConfigRepository] Saving config for season: ${config.seasonId}.`);
|
||||
this.configs.set(config.seasonId, config);
|
||||
this.logger.info(`Config for season ${config.seasonId} saved successfully.`);
|
||||
return Promise.resolve(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ILeagueStandingsRepository, RawStanding } from '@gridpilot/core/league/application/ports/ILeagueStandingsRepository';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueStandingsRepository implements ILeagueStandingsRepository {
|
||||
private standings: Map<string, RawStanding[]> = new Map(); // Key: leagueId
|
||||
|
||||
constructor(private readonly logger: ILogger, initialStandings: Record<string, RawStanding[]> = {}) {
|
||||
this.logger.info('InMemoryLeagueStandingsRepository initialized.');
|
||||
for (const leagueId in initialStandings) {
|
||||
// Ensure initialStandings[leagueId] is not undefined before setting
|
||||
if (initialStandings[leagueId] !== undefined) {
|
||||
this.standings.set(leagueId, initialStandings[leagueId]);
|
||||
this.logger.debug(`Seeded standings for league: ${leagueId}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueStandings(leagueId: string): Promise<RawStanding[]> {
|
||||
this.logger.debug(`[InMemoryLeagueStandingsRepository] Getting standings for league: ${leagueId}.`);
|
||||
const leagueStandings = this.standings.get(leagueId) ?? [];
|
||||
this.logger.info(`Found ${leagueStandings.length} standings for league: ${leagueId}.`);
|
||||
return Promise.resolve(leagueStandings);
|
||||
}
|
||||
|
||||
// Helper method for seeding/updating if needed by other in-memory repos
|
||||
public setLeagueStandings(leagueId: string, standings: RawStanding[]): void {
|
||||
this.standings.set(leagueId, standings);
|
||||
this.logger.debug(`[InMemoryLeagueStandingsRepository] Set standings for league: ${leagueId}.`);
|
||||
}
|
||||
}
|
||||
@@ -1,151 +1,88 @@
|
||||
/**
|
||||
* In-Memory Implementation: InMemoryProtestRepository
|
||||
*
|
||||
* Provides an in-memory storage implementation for protests.
|
||||
*/
|
||||
|
||||
import type { Protest } from '../../domain/entities/Protest';
|
||||
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { IProtestRepository } from '@gridpilot/racing/domain/repositories/IProtestRepository';
|
||||
import { Protest, ProtestStatus } from '@gridpilot/racing/domain/entities/Protest';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryProtestRepository implements IProtestRepository {
|
||||
private protests: Map<string, Protest> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger, initialProtests: Protest[] = []) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialProtests: Protest[] = []) {
|
||||
this.logger.info('InMemoryProtestRepository initialized.');
|
||||
initialProtests.forEach(protest => {
|
||||
for (const protest of initialProtests) {
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.debug(`Seeded protest: ${protest.id}`);
|
||||
});
|
||||
this.logger.debug(`Seeded protest: ${protest.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Protest | null> {
|
||||
this.logger.debug(`Finding protest by id: ${id}`);
|
||||
try {
|
||||
const protest = this.protests.get(id) || null;
|
||||
if (protest) {
|
||||
this.logger.info(`Found protest with id: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Protest with id ${id} not found.`);
|
||||
}
|
||||
return protest;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protest by id ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryProtestRepository] Finding protest by ID: ${id}`);
|
||||
const protest = this.protests.get(id) ?? null;
|
||||
if (protest) {
|
||||
this.logger.info(`Found protest by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Protest with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(protest);
|
||||
}
|
||||
|
||||
async findByRaceId(raceId: string): Promise<Protest[]> {
|
||||
this.logger.debug(`Finding protests by race id: ${raceId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.raceId === raceId
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests for race id: ${raceId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests by race id ${raceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryProtestRepository] Finding protests for race: ${raceId}.`);
|
||||
const protests = Array.from(this.protests.values()).filter(p => p.raceId === raceId);
|
||||
this.logger.info(`Found ${protests.length} protests for race ${raceId}.`);
|
||||
return Promise.resolve(protests);
|
||||
}
|
||||
|
||||
async findByProtestingDriverId(driverId: string): Promise<Protest[]> {
|
||||
this.logger.debug(`Finding protests by protesting driver id: ${driverId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.protestingDriverId === driverId
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests by protesting driver id: ${driverId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests by protesting driver id ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryProtestRepository] Finding protests by protesting driver ID: ${driverId}.`);
|
||||
const protests = Array.from(this.protests.values()).filter(p => p.protestingDriverId === driverId);
|
||||
this.logger.info(`Found ${protests.length} protests by protesting driver ${driverId}.`);
|
||||
return Promise.resolve(protests);
|
||||
}
|
||||
|
||||
async findByAccusedDriverId(driverId: string): Promise<Protest[]> {
|
||||
this.logger.debug(`Finding protests by accused driver id: ${driverId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.accusedDriverId === driverId
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests by accused driver id: ${driverId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests by accused driver id ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryProtestRepository] Finding protests by accused driver ID: ${driverId}.`);
|
||||
const protests = Array.from(this.protests.values()).filter(p => p.accusedDriverId === driverId);
|
||||
this.logger.info(`Found ${protests.length} protests by accused driver ${driverId}.`);
|
||||
return Promise.resolve(protests);
|
||||
}
|
||||
|
||||
async findPending(): Promise<Protest[]> {
|
||||
this.logger.debug('Finding pending protests.');
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.isPending()
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} pending protests.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding pending protests:', error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug('[InMemoryProtestRepository] Finding all pending protests.');
|
||||
const pendingProtests = Array.from(this.protests.values()).filter(p => p.status === 'pending');
|
||||
this.logger.info(`Found ${pendingProtests.length} pending protests.`);
|
||||
return Promise.resolve(pendingProtests);
|
||||
}
|
||||
|
||||
async findUnderReviewBy(stewardId: string): Promise<Protest[]> {
|
||||
this.logger.debug(`Finding protests under review by steward: ${stewardId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.reviewedBy === stewardId && protest.isUnderReview()
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests under review by steward: ${stewardId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests under review by steward ${stewardId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryProtestRepository] Finding protests under review by steward: ${stewardId}.`);
|
||||
const underReviewProtests = Array.from(this.protests.values()).filter(p => p.reviewedBy === stewardId && p.status === 'under_review');
|
||||
this.logger.info(`Found ${underReviewProtests.length} protests under review by steward ${stewardId}.`);
|
||||
return Promise.resolve(underReviewProtests);
|
||||
}
|
||||
|
||||
async create(protest: Protest): Promise<void> {
|
||||
this.logger.debug(`Creating protest: ${protest.id}`);
|
||||
try {
|
||||
if (this.protests.has(protest.id)) {
|
||||
this.logger.warn(`Protest with ID ${protest.id} already exists.`);
|
||||
throw new Error(`Protest with ID ${protest.id} already exists`);
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.info(`Protest ${protest.id} created successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating protest ${protest.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryProtestRepository] Creating protest: ${protest.id}.`);
|
||||
if (this.protests.has(protest.id)) {
|
||||
this.logger.warn(`Protest with ID ${protest.id} already exists.`);
|
||||
throw new Error('Protest already exists');
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.info(`Protest ${protest.id} created successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async update(protest: Protest): Promise<void> {
|
||||
this.logger.debug(`Updating protest: ${protest.id}`);
|
||||
try {
|
||||
if (!this.protests.has(protest.id)) {
|
||||
this.logger.warn(`Protest with ID ${protest.id} not found for update.`);
|
||||
throw new Error(`Protest with ID ${protest.id} not found`);
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.info(`Protest ${protest.id} updated successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating protest ${protest.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryProtestRepository] Updating protest: ${protest.id}.`);
|
||||
if (!this.protests.has(protest.id)) {
|
||||
this.logger.warn(`Protest with ID ${protest.id} not found for update.`);
|
||||
throw new Error('Protest not found');
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.info(`Protest ${protest.id} updated successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of protest with id: ${id}`);
|
||||
try {
|
||||
const exists = this.protests.has(id);
|
||||
this.logger.debug(`Protest ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of protest with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryProtestRepository] Checking existence of protest with ID: ${id}.`);
|
||||
return Promise.resolve(this.protests.has(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,178 +1,90 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryRaceRegistrationRepository
|
||||
*
|
||||
* In-memory implementation of IRaceRegistrationRepository.
|
||||
* Stores race registrations in Maps keyed by raceId and driverId.
|
||||
*/
|
||||
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import type { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
|
||||
type RaceRegistrationSeed = Pick<RaceRegistration, 'raceId' | 'driverId' | 'registeredAt'>;
|
||||
import { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository {
|
||||
private registrationsByRace: Map<string, Set<string>>;
|
||||
private registrationsByDriver: Map<string, Set<string>>;
|
||||
private readonly logger: ILogger;
|
||||
private registrations: Map<string, RaceRegistration> = new Map(); // Key: `${raceId}:${driverId}`
|
||||
|
||||
constructor(logger: ILogger, seedRegistrations?: RaceRegistrationSeed[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialRegistrations: RaceRegistration[] = []) {
|
||||
this.logger.info('InMemoryRaceRegistrationRepository initialized.');
|
||||
this.registrationsByRace = new Map();
|
||||
this.registrationsByDriver = new Map();
|
||||
|
||||
if (seedRegistrations) {
|
||||
this.logger.debug('Seeding with initial registrations', { count: seedRegistrations.length });
|
||||
seedRegistrations.forEach((registration) => {
|
||||
this.addToIndexes(registration.raceId, registration.driverId, registration.registeredAt);
|
||||
});
|
||||
for (const reg of initialRegistrations) {
|
||||
this.registrations.set(reg.id, reg);
|
||||
this.logger.debug(`Seeded registration: ${reg.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
private addToIndexes(raceId: string, driverId: string, _registeredAt: Date): void {
|
||||
this.logger.debug('Attempting to add race registration to indexes', { raceId, driverId });
|
||||
let raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) {
|
||||
raceSet = new Set();
|
||||
this.registrationsByRace.set(raceId, raceSet);
|
||||
this.logger.debug('Created new race set as none existed', { raceId });
|
||||
}
|
||||
raceSet.add(driverId);
|
||||
this.logger.debug('Added driver to race set', { raceId, driverId });
|
||||
|
||||
let driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (!driverSet) {
|
||||
driverSet = new Set();
|
||||
this.registrationsByDriver.set(driverId, driverSet);
|
||||
this.logger.debug('Created new driver set as none existed', { driverId });
|
||||
}
|
||||
driverSet.add(raceId);
|
||||
this.logger.debug('Added race to driver set', { raceId, driverId });
|
||||
this.logger.info('Successfully added race registration to indexes', { raceId, driverId });
|
||||
}
|
||||
|
||||
private removeFromIndexes(raceId: string, driverId: string): void {
|
||||
this.logger.debug('Attempting to remove race registration from indexes', { raceId, driverId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (raceSet) {
|
||||
raceSet.delete(driverId);
|
||||
this.logger.debug('Removed driver from race set', { raceId, driverId });
|
||||
if (raceSet.size === 0) {
|
||||
this.registrationsByRace.delete(raceId);
|
||||
this.logger.debug('Deleted race set as it is now empty', { raceId });
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Race set not found during removal, potential inconsistency', { raceId });
|
||||
}
|
||||
|
||||
const driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (driverSet) {
|
||||
driverSet.delete(raceId);
|
||||
this.logger.debug('Removed race from driver set', { raceId, driverId });
|
||||
if (driverSet.size === 0) {
|
||||
this.registrationsByDriver.delete(driverId);
|
||||
this.logger.debug('Deleted driver set as it is now empty', { driverId });
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Driver set not found during removal, potential inconsistency', { driverId });
|
||||
}
|
||||
this.logger.info('Successfully removed race registration from indexes', { raceId, driverId });
|
||||
}
|
||||
|
||||
async isRegistered(raceId: string, driverId: string): Promise<boolean> {
|
||||
this.logger.info('Checking if driver is registered for race', { raceId, driverId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) {
|
||||
this.logger.debug('Race set not found, driver not registered', { raceId, driverId });
|
||||
return false;
|
||||
}
|
||||
const isRegistered = raceSet.has(driverId);
|
||||
this.logger.debug('Registration status result', { raceId, driverId, isRegistered });
|
||||
return isRegistered;
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Checking if driver ${driverId} is registered for race ${raceId}.`);
|
||||
const key = `${raceId}:${driverId}`;
|
||||
return Promise.resolve(this.registrations.has(key));
|
||||
}
|
||||
|
||||
async getRegisteredDrivers(raceId: string): Promise<string[]> {
|
||||
this.logger.info('Attempting to fetch registered drivers for race', { raceId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) {
|
||||
this.logger.debug('No registered drivers found for race', { raceId });
|
||||
return [];
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registered drivers for race ${raceId}.`);
|
||||
const driverIds: string[] = [];
|
||||
for (const registration of this.registrations.values()) {
|
||||
if (registration.raceId === raceId) {
|
||||
driverIds.push(registration.driverId);
|
||||
}
|
||||
}
|
||||
const drivers = Array.from(raceSet.values());
|
||||
this.logger.debug('Found registered drivers for race', { raceId, count: drivers.length });
|
||||
this.logger.info('Successfully fetched registered drivers for race', { raceId, count: drivers.length });
|
||||
return drivers;
|
||||
this.logger.info(`Found ${driverIds.length} registered drivers for race ${raceId}.`);
|
||||
return Promise.resolve(driverIds);
|
||||
}
|
||||
|
||||
async getRegistrationCount(raceId: string): Promise<number> {
|
||||
this.logger.info('Attempting to get registration count for race', { raceId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
const count = raceSet ? raceSet.size : 0;
|
||||
this.logger.debug('Registration count for race', { raceId, count });
|
||||
this.logger.info('Returning registration count for race', { raceId, count });
|
||||
return count;
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registration count for race ${raceId}.`);
|
||||
const count = Array.from(this.registrations.values()).filter(reg => reg.raceId === raceId).length;
|
||||
this.logger.info(`Registration count for race ${raceId}: ${count}.`);
|
||||
return Promise.resolve(count);
|
||||
}
|
||||
|
||||
async register(registration: RaceRegistration): Promise<void> {
|
||||
this.logger.info('Attempting to register driver for race', { raceId: registration.raceId, driverId: registration.driverId });
|
||||
const alreadyRegistered = await this.isRegistered(registration.raceId, registration.driverId);
|
||||
if (alreadyRegistered) {
|
||||
this.logger.warn('Driver already registered for race, registration aborted', { raceId: registration.raceId, driverId: registration.driverId });
|
||||
throw new Error('Already registered for this race');
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Registering driver ${registration.driverId} for race ${registration.raceId}.`);
|
||||
if (await this.isRegistered(registration.raceId, registration.driverId)) {
|
||||
this.logger.warn(`Driver ${registration.driverId} already registered for race ${registration.raceId}.`);
|
||||
throw new Error('Driver already registered for this race');
|
||||
}
|
||||
this.addToIndexes(registration.raceId, registration.driverId, registration.registeredAt);
|
||||
this.logger.info('Driver successfully registered for race', { raceId: registration.raceId, driverId: registration.driverId });
|
||||
this.registrations.set(registration.id, registration);
|
||||
this.logger.info(`Driver ${registration.driverId} registered for race ${registration.raceId}.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async withdraw(raceId: string, driverId: string): Promise<void> {
|
||||
this.logger.info('Attempting to withdraw driver from race', { raceId, driverId });
|
||||
const alreadyRegistered = await this.isRegistered(raceId, driverId);
|
||||
if (!alreadyRegistered) {
|
||||
this.logger.warn('Driver not registered for race, withdrawal aborted', { raceId, driverId });
|
||||
throw new Error('Not registered for this race');
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Withdrawing driver ${driverId} from race ${raceId}.`);
|
||||
const key = `${raceId}:${driverId}`;
|
||||
if (!this.registrations.has(key)) {
|
||||
this.logger.warn(`Driver ${driverId} not registered for race ${raceId}. No withdrawal needed.`);
|
||||
throw new Error('Driver not registered for this race');
|
||||
}
|
||||
this.removeFromIndexes(raceId, driverId);
|
||||
this.logger.info('Driver successfully withdrew from race', { raceId, driverId });
|
||||
this.registrations.delete(key);
|
||||
this.logger.info(`Driver ${driverId} withdrawn from race ${raceId}.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getDriverRegistrations(driverId: string): Promise<string[]> {
|
||||
this.logger.info('Attempting to fetch registrations for driver', { driverId });
|
||||
const driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (!driverSet) {
|
||||
this.logger.debug('No registrations found for driver', { driverId });
|
||||
return [];
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registrations for driver: ${driverId}.`);
|
||||
const raceIds: string[] = [];
|
||||
for (const registration of this.registrations.values()) {
|
||||
if (registration.driverId === driverId) {
|
||||
raceIds.push(registration.raceId);
|
||||
}
|
||||
}
|
||||
const registrations = Array.from(driverSet.values());
|
||||
this.logger.debug('Found registrations for driver', { driverId, count: registrations.length });
|
||||
this.logger.info('Successfully fetched registrations for driver', { driverId, count: registrations.length });
|
||||
return registrations;
|
||||
this.logger.info(`Found ${raceIds.length} registrations for driver ${driverId}.`);
|
||||
return Promise.resolve(raceIds);
|
||||
}
|
||||
|
||||
async clearRaceRegistrations(raceId: string): Promise<void> {
|
||||
this.logger.info('Attempting to clear all registrations for race', { raceId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) {
|
||||
this.logger.debug('No registrations to clear for race (race set not found)', { raceId });
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug('Found registrations to clear', { raceId, count: raceSet.size });
|
||||
for (const driverId of raceSet.values()) {
|
||||
const driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (driverSet) {
|
||||
driverSet.delete(raceId);
|
||||
if (driverSet.size === 0) {
|
||||
this.registrationsByDriver.delete(driverId);
|
||||
this.logger.debug('Deleted driver set as it is now empty during race clear', { raceId, driverId });
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Driver set not found during race clear, potential inconsistency', { raceId, driverId });
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Clearing all registrations for race: ${raceId}.`);
|
||||
const registrationsToDelete: string[] = [];
|
||||
for (const registration of this.registrations.values()) {
|
||||
if (registration.raceId === raceId) {
|
||||
registrationsToDelete.push(registration.id);
|
||||
}
|
||||
this.logger.debug('Removed race from driver set during race clear', { raceId, driverId });
|
||||
}
|
||||
|
||||
this.registrationsByRace.delete(raceId);
|
||||
this.logger.info('Successfully cleared all registrations for race', { raceId });
|
||||
for (const id of registrationsToDelete) {
|
||||
this.registrations.delete(id);
|
||||
}
|
||||
this.logger.info(`Cleared ${registrationsToDelete.length} registrations for race ${raceId}.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,207 +1,110 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryRaceRepository
|
||||
*
|
||||
* In-memory implementation of IRaceRepository.
|
||||
* Stores data in Map structure with UUID generation.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race';
|
||||
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryRaceRepository implements IRaceRepository {
|
||||
private races: Map<string, Race>;
|
||||
private readonly logger: ILogger;
|
||||
private races: Map<string, Race> = new Map();
|
||||
|
||||
constructor(logger: ILogger, seedData?: Race[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialRaces: Race[] = []) {
|
||||
this.logger.info('InMemoryRaceRepository initialized.');
|
||||
this.races = new Map();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(race => {
|
||||
this.races.set(race.id, race);
|
||||
this.logger.debug(`Seeded race: ${race.id}.`);
|
||||
});
|
||||
for (const race of initialRaces) {
|
||||
this.races.set(race.id, race);
|
||||
this.logger.debug(`Seeded race: ${race.id} (${race.track}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Race | null> {
|
||||
this.logger.debug(`Finding race by id: ${id}`);
|
||||
try {
|
||||
const race = this.races.get(id) ?? null;
|
||||
if (race) {
|
||||
this.logger.info(`Found race: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Race with id ${id} not found.`);
|
||||
}
|
||||
return race;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding race by id ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding race by ID: ${id}`);
|
||||
const race = this.races.get(id) ?? null;
|
||||
if (race) {
|
||||
this.logger.info(`Found race by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Race with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(race);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Race[]> {
|
||||
this.logger.debug('Finding all races.');
|
||||
try {
|
||||
const races = Array.from(this.races.values());
|
||||
this.logger.info(`Found ${races.length} races.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all races:', error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug('[InMemoryRaceRepository] Finding all races.');
|
||||
return Promise.resolve(Array.from(this.races.values()));
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<Race[]> {
|
||||
this.logger.debug(`Finding races by league id: ${leagueId}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race => race.leagueId === leagueId)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} races for league id: ${leagueId}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding races by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding races by league ID: ${leagueId}`);
|
||||
const races = Array.from(this.races.values()).filter(race => race.leagueId === leagueId);
|
||||
this.logger.info(`Found ${races.length} races for league ID: ${leagueId}.`);
|
||||
return Promise.resolve(races);
|
||||
}
|
||||
|
||||
async findUpcomingByLeagueId(leagueId: string): Promise<Race[]> {
|
||||
this.logger.debug(`Finding upcoming races by league id: ${leagueId}`);
|
||||
try {
|
||||
const now = new Date();
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.leagueId === leagueId &&
|
||||
race.status === 'scheduled' &&
|
||||
race.scheduledAt > now
|
||||
)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} upcoming races for league id: ${leagueId}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding upcoming races by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding upcoming races by league ID: ${leagueId}`);
|
||||
const now = new Date();
|
||||
const upcomingRaces = Array.from(this.races.values()).filter(race =>
|
||||
race.leagueId === leagueId && race.status === 'scheduled' && race.scheduledAt > now
|
||||
);
|
||||
this.logger.info(`Found ${upcomingRaces.length} upcoming races for league ID: ${leagueId}.`);
|
||||
return Promise.resolve(upcomingRaces);
|
||||
}
|
||||
|
||||
async findCompletedByLeagueId(leagueId: string): Promise<Race[]> {
|
||||
this.logger.debug(`Finding completed races by league id: ${leagueId}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.leagueId === leagueId &&
|
||||
race.status === 'completed'
|
||||
)
|
||||
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} completed races for league id: ${leagueId}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding completed races by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding completed races by league ID: ${leagueId}`);
|
||||
const completedRaces = Array.from(this.races.values()).filter(race =>
|
||||
race.leagueId === leagueId && race.status === 'completed'
|
||||
);
|
||||
this.logger.info(`Found ${completedRaces.length} completed races for league ID: ${leagueId}.`);
|
||||
return Promise.resolve(completedRaces);
|
||||
}
|
||||
|
||||
async findByStatus(status: RaceStatus): Promise<Race[]> {
|
||||
this.logger.debug(`Finding races by status: ${status}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race => race.status === status)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} races with status: ${status}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding races by status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding races by status: ${status}.`);
|
||||
const races = Array.from(this.races.values()).filter(race => race.status === status);
|
||||
this.logger.info(`Found ${races.length} races with status: ${status}.`);
|
||||
return Promise.resolve(races);
|
||||
}
|
||||
|
||||
async findByDateRange(startDate: Date, endDate: Date): Promise<Race[]> {
|
||||
this.logger.debug(`Finding races by date range: ${startDate.toISOString()} - ${endDate.toISOString()}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.scheduledAt >= startDate &&
|
||||
race.scheduledAt <= endDate
|
||||
)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} races in date range.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding races by date range:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding races by date range: ${startDate.toISOString()} - ${endDate.toISOString()}.`);
|
||||
const races = Array.from(this.races.values()).filter(race =>
|
||||
race.scheduledAt >= startDate && race.scheduledAt <= endDate
|
||||
);
|
||||
this.logger.info(`Found ${races.length} races within date range.`);
|
||||
return Promise.resolve(races);
|
||||
}
|
||||
|
||||
async create(race: Race): Promise<Race> {
|
||||
this.logger.debug(`Creating race: ${race.id}`);
|
||||
try {
|
||||
if (await this.exists(race.id)) {
|
||||
this.logger.warn(`Race with ID ${race.id} already exists.`);
|
||||
throw new Error(`Race with ID ${race.id} already exists`);
|
||||
}
|
||||
|
||||
this.races.set(race.id, race);
|
||||
this.logger.info(`Race ${race.id} created successfully.`);
|
||||
return race;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating race ${race.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryRaceRepository] Creating race: ${race.id} (${race.track}).`);
|
||||
if (this.races.has(race.id)) {
|
||||
this.logger.warn(`Race with ID ${race.id} already exists.`);
|
||||
throw new Error('Race already exists');
|
||||
}
|
||||
this.races.set(race.id, race);
|
||||
this.logger.info(`Race ${race.id} created successfully.`);
|
||||
return Promise.resolve(race);
|
||||
}
|
||||
|
||||
async update(race: Race): Promise<Race> {
|
||||
this.logger.debug(`Updating race: ${race.id}`);
|
||||
try {
|
||||
if (!await this.exists(race.id)) {
|
||||
this.logger.warn(`Race with ID ${race.id} not found for update.`);
|
||||
throw new Error(`Race with ID ${race.id} not found`);
|
||||
}
|
||||
|
||||
this.races.set(race.id, race);
|
||||
this.logger.info(`Race ${race.id} updated successfully.`);
|
||||
return race;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating race ${race.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryRaceRepository] Updating race: ${race.id} (${race.track}).`);
|
||||
if (!this.races.has(race.id)) {
|
||||
this.logger.warn(`Race with ID ${race.id} not found for update.`);
|
||||
throw new Error('Race not found');
|
||||
}
|
||||
this.races.set(race.id, race);
|
||||
this.logger.info(`Race ${race.id} updated successfully.`);
|
||||
return Promise.resolve(race);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting race: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`Race with ID ${id} not found for deletion.`);
|
||||
throw new Error(`Race with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.races.delete(id);
|
||||
this.logger.debug(`[InMemoryRaceRepository] Deleting race with ID: ${id}.`);
|
||||
if (this.races.delete(id)) {
|
||||
this.logger.info(`Race ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting race ${id}:`, error);
|
||||
throw error;
|
||||
} else {
|
||||
this.logger.warn(`Race with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of race with id: ${id}`);
|
||||
try {
|
||||
const exists = this.races.has(id);
|
||||
this.logger.debug(`Race ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of race with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Checking existence of race with ID: ${id}.`);
|
||||
return Promise.resolve(this.races.has(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to generate a new UUID
|
||||
*/
|
||||
static generateId(): string {
|
||||
return uuidv4();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ISeasonRepository } from '@gridpilot/racing/domain/repositories/ISeasonRepository';
|
||||
import { Season } from '@gridpilot/racing/domain/entities/Season';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySeasonRepository implements ISeasonRepository {
|
||||
private seasons: Map<string, Season> = new Map(); // Key: seasonId
|
||||
|
||||
constructor(private readonly logger: ILogger, initialSeasons: Season[] = []) {
|
||||
this.logger.info('InMemorySeasonRepository initialized.');
|
||||
for (const season of initialSeasons) {
|
||||
this.seasons.set(season.id, season);
|
||||
this.logger.debug(`Seeded season: ${season.id} (${season.name}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Season | null> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Finding season by ID: ${id}`);
|
||||
const season = this.seasons.get(id) ?? null;
|
||||
if (season) {
|
||||
this.logger.info(`Found season by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Season with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(season);
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<Season[]> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Finding seasons by league ID: ${leagueId}`);
|
||||
const seasons = Array.from(this.seasons.values()).filter(season => season.leagueId === leagueId);
|
||||
this.logger.info(`Found ${seasons.length} seasons for league ID: ${leagueId}.`);
|
||||
return Promise.resolve(seasons);
|
||||
}
|
||||
|
||||
async create(season: Season): Promise<Season> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Creating season: ${season.id} (${season.name})`);
|
||||
if (this.seasons.has(season.id)) {
|
||||
this.logger.warn(`Season with ID ${season.id} already exists.`);
|
||||
throw new Error('Season already exists');
|
||||
}
|
||||
this.seasons.set(season.id, season);
|
||||
this.logger.info(`Season ${season.id} created successfully.`);
|
||||
return Promise.resolve(season);
|
||||
}
|
||||
|
||||
async add(season: Season): Promise<void> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Adding season: ${season.id} (${season.name})`);
|
||||
if (this.seasons.has(season.id)) {
|
||||
this.logger.warn(`Season with ID ${season.id} already exists (add method).`);
|
||||
throw new Error('Season already exists');
|
||||
}
|
||||
this.seasons.set(season.id, season);
|
||||
this.logger.info(`Season ${season.id} added successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async update(season: Season): Promise<void> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Updating season: ${season.id} (${season.name})`);
|
||||
if (!this.seasons.has(season.id)) {
|
||||
this.logger.warn(`Season with ID ${season.id} not found for update.`);
|
||||
throw new Error('Season not found');
|
||||
}
|
||||
this.seasons.set(season.id, season);
|
||||
this.logger.info(`Season ${season.id} updated successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Deleting season with ID: ${id}`);
|
||||
if (this.seasons.delete(id)) {
|
||||
this.logger.info(`Season ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Season with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async listByLeague(leagueId: string): Promise<Season[]> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Listing seasons by league ID: ${leagueId}`);
|
||||
const seasons = Array.from(this.seasons.values()).filter(season => season.leagueId === leagueId);
|
||||
return Promise.resolve(seasons);
|
||||
}
|
||||
|
||||
async listActiveByLeague(leagueId: string): Promise<Season[]> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Listing active seasons by league ID: ${leagueId}`);
|
||||
const activeSeasons = Array.from(this.seasons.values()).filter(season => season.leagueId === leagueId && season.status === 'active');
|
||||
return Promise.resolve(activeSeasons);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +1,98 @@
|
||||
/**
|
||||
* In-Memory Implementation: ISponsorRepository
|
||||
*
|
||||
* Mock repository for testing and development
|
||||
*/
|
||||
|
||||
import type { Sponsor } from '../../domain/entities/Sponsor';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ISponsorRepository } from '@gridpilot/core/racing/domain/repositories/ISponsorRepository';
|
||||
import { Sponsor } from '@gridpilot/core/racing/domain/entities/Sponsor';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySponsorRepository implements ISponsorRepository {
|
||||
private sponsors: Map<string, Sponsor> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
private emailIndex: Map<string, string> = new Map(); // contactEmail -> sponsorId
|
||||
|
||||
constructor(logger: ILogger, seedData?: Sponsor[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialSponsors: Sponsor[] = []) {
|
||||
this.logger.info('InMemorySponsorRepository initialized.');
|
||||
if (seedData) {
|
||||
this.seed(seedData);
|
||||
for (const sponsor of initialSponsors) {
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.emailIndex.set(sponsor.contactEmail.toLowerCase(), sponsor.id);
|
||||
this.logger.debug(`Seeded sponsor: ${sponsor.id} (${sponsor.name}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Sponsor | null> {
|
||||
this.logger.debug(`Finding sponsor by id: ${id}`);
|
||||
try {
|
||||
const sponsor = this.sponsors.get(id) ?? null;
|
||||
if (sponsor) {
|
||||
this.logger.info(`Found sponsor: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor with id ${id} not found.`);
|
||||
}
|
||||
return sponsor;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsor by id ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemorySponsorRepository] Finding sponsor by ID: ${id}`);
|
||||
const sponsor = this.sponsors.get(id) ?? null;
|
||||
if (sponsor) {
|
||||
this.logger.info(`Found sponsor by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(sponsor);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Sponsor[]> {
|
||||
this.logger.debug('Finding all sponsors.');
|
||||
try {
|
||||
const sponsors = Array.from(this.sponsors.values());
|
||||
this.logger.info(`Found ${sponsors.length} sponsors.`);
|
||||
return sponsors;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all sponsors:', error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug('[InMemorySponsorRepository] Finding all sponsors.');
|
||||
return Promise.resolve(Array.from(this.sponsors.values()));
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<Sponsor | null> {
|
||||
this.logger.debug(`Finding sponsor by email: ${email}`);
|
||||
try {
|
||||
for (const sponsor of this.sponsors.values()) {
|
||||
if (sponsor.contactEmail === email) {
|
||||
this.logger.info(`Found sponsor with email: ${email}.`);
|
||||
return sponsor;
|
||||
}
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorRepository] Finding sponsor by email: ${email}`);
|
||||
const sponsorId = this.emailIndex.get(email.toLowerCase());
|
||||
if (!sponsorId) {
|
||||
this.logger.warn(`Sponsor with email ${email} not found.`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsor by email ${email}:`, error);
|
||||
throw error;
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.findById(sponsorId);
|
||||
}
|
||||
|
||||
async create(sponsor: Sponsor): Promise<Sponsor> {
|
||||
this.logger.debug(`Creating sponsor: ${sponsor.id}`);
|
||||
try {
|
||||
if (this.sponsors.has(sponsor.id)) {
|
||||
this.logger.warn(`Sponsor with ID ${sponsor.id} already exists.`);
|
||||
throw new Error('Sponsor with this ID already exists');
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.info(`Sponsor ${sponsor.id} created successfully.`);
|
||||
return sponsor;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating sponsor ${sponsor.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemorySponsorRepository] Creating sponsor: ${sponsor.id} (${sponsor.name})`);
|
||||
if (this.sponsors.has(sponsor.id)) {
|
||||
this.logger.warn(`Sponsor with ID ${sponsor.id} already exists.`);
|
||||
throw new Error('Sponsor already exists');
|
||||
}
|
||||
if (this.emailIndex.has(sponsor.contactEmail.toLowerCase())) {
|
||||
this.logger.warn(`Sponsor with email ${sponsor.contactEmail} already exists.`);
|
||||
throw new Error('Sponsor with this email already exists');
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.emailIndex.set(sponsor.contactEmail.toLowerCase(), sponsor.id);
|
||||
this.logger.info(`Sponsor ${sponsor.id} (${sponsor.name}) created successfully.`);
|
||||
return Promise.resolve(sponsor);
|
||||
}
|
||||
|
||||
async update(sponsor: Sponsor): Promise<Sponsor> {
|
||||
this.logger.debug(`Updating sponsor: ${sponsor.id}`);
|
||||
try {
|
||||
if (!this.sponsors.has(sponsor.id)) {
|
||||
this.logger.warn(`Sponsor with ID ${sponsor.id} not found for update.`);
|
||||
throw new Error('Sponsor not found');
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.info(`Sponsor ${sponsor.id} updated successfully.`);
|
||||
return sponsor;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating sponsor ${sponsor.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemorySponsorRepository] Updating sponsor: ${sponsor.id} (${sponsor.name})`);
|
||||
if (!this.sponsors.has(sponsor.id)) {
|
||||
this.logger.warn(`Sponsor with ID ${sponsor.id} not found for update.`);
|
||||
throw new Error('Sponsor not found');
|
||||
}
|
||||
const existingSponsor = this.sponsors.get(sponsor.id);
|
||||
// If email changed, update index
|
||||
if (existingSponsor && existingSponsor.contactEmail.toLowerCase() !== sponsor.contactEmail.toLowerCase()) {
|
||||
if (this.emailIndex.has(sponsor.contactEmail.toLowerCase()) && this.emailIndex.get(sponsor.contactEmail.toLowerCase()) !== sponsor.id) {
|
||||
this.logger.warn(`Cannot update sponsor ${sponsor.id} to email ${sponsor.contactEmail} as it's already taken.`);
|
||||
throw new Error('Sponsor with this email already exists');
|
||||
}
|
||||
this.emailIndex.delete(existingSponsor.contactEmail.toLowerCase());
|
||||
this.emailIndex.set(sponsor.contactEmail.toLowerCase(), sponsor.id);
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.info(`Sponsor ${sponsor.id} (${sponsor.name}) updated successfully.`);
|
||||
return Promise.resolve(sponsor);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting sponsor: ${id}`);
|
||||
try {
|
||||
if (this.sponsors.delete(id)) {
|
||||
this.logger.info(`Sponsor ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor with id ${id} not found for deletion.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting sponsor ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemorySponsorRepository] Deleting sponsor with ID: ${id}`);
|
||||
const sponsor = this.sponsors.get(id);
|
||||
if (sponsor) {
|
||||
this.sponsors.delete(id);
|
||||
this.emailIndex.delete(sponsor.contactEmail.toLowerCase());
|
||||
this.logger.info(`Sponsor ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of sponsor with id: ${id}`);
|
||||
try {
|
||||
const exists = this.sponsors.has(id);
|
||||
this.logger.debug(`Sponsor ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of sponsor with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorRepository] Checking existence of sponsor with ID: ${id}`);
|
||||
return Promise.resolve(this.sponsors.has(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed initial data
|
||||
*/
|
||||
seed(sponsors: Sponsor[]): void {
|
||||
this.logger.debug(`Seeding ${sponsors.length} sponsors.`);
|
||||
try {
|
||||
for (const sponsor of sponsors) {
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.debug(`Seeded sponsor: ${sponsor.id}.`);
|
||||
}
|
||||
this.logger.info(`Successfully seeded ${sponsors.length} sponsors.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding sponsors:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all sponsors.');
|
||||
this.sponsors.clear();
|
||||
this.logger.info('All sponsors cleared.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,232 +1,112 @@
|
||||
/**
|
||||
* InMemory implementation of ISponsorshipRequestRepository
|
||||
*/
|
||||
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import {
|
||||
SponsorshipRequest,
|
||||
type SponsorableEntityType,
|
||||
type SponsorshipRequestStatus
|
||||
} from '../../domain/entities/SponsorshipRequest';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ISponsorshipRequestRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipRequestRepository';
|
||||
import { SponsorshipRequest, SponsorableEntityType, SponsorshipRequestStatus } from '@gridpilot/racing/domain/entities/SponsorshipRequest';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySponsorshipRequestRepository implements ISponsorshipRequestRepository {
|
||||
private requests: Map<string, SponsorshipRequest> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger, seedData?: SponsorshipRequest[]) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemorySponsorshipRequestRepository initialized.');
|
||||
if (seedData) {
|
||||
this.seed(seedData);
|
||||
constructor(private readonly logger: ILogger, initialRequests: SponsorshipRequest[] = []) {
|
||||
this.logger.info('InMemorySponsorshipRequestRepository initialized.');
|
||||
for (const req of initialRequests) {
|
||||
this.requests.set(req.id, req);
|
||||
this.logger.debug(`Seeded sponsorship request: ${req.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<SponsorshipRequest | null> {
|
||||
this.logger.debug(`Finding sponsorship request by id: ${id}`);
|
||||
try {
|
||||
const request = this.requests.get(id) ?? null;
|
||||
if (request) {
|
||||
this.logger.info(`Found sponsorship request: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsorship request with id ${id} not found.`);
|
||||
}
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship request by id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding request by ID: ${id}`);
|
||||
const request = this.requests.get(id) ?? null;
|
||||
if (request) {
|
||||
this.logger.info(`Found request by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Request with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
async findByEntity(entityType: SponsorableEntityType, entityId: string): Promise<SponsorshipRequest[]> {
|
||||
this.logger.debug(`Finding sponsorship requests by entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.entityType === entityType && request.entityId === entityId
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests for entity: ${entityType}, ${entityId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding requests for entity ${entityType}:${entityId}.`);
|
||||
const requests = Array.from(this.requests.values()).filter(req => req.entityType === entityType && req.entityId === entityId);
|
||||
this.logger.info(`Found ${requests.length} requests for entity ${entityType}:${entityId}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async findPendingByEntity(entityType: SponsorableEntityType, entityId: string): Promise<SponsorshipRequest[]> {
|
||||
this.logger.debug(`Finding pending sponsorship requests by entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request =>
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} pending sponsorship requests for entity: ${entityType}, ${entityId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding pending sponsorship requests by entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding pending requests for entity ${entityType}:${entityId}.`);
|
||||
const requests = Array.from(this.requests.values()).filter(req => req.entityType === entityType && req.entityId === entityId && req.status === 'pending');
|
||||
this.logger.info(`Found ${requests.length} pending requests for entity ${entityType}:${entityId}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async findBySponsorId(sponsorId: string): Promise<SponsorshipRequest[]> {
|
||||
this.logger.debug(`Finding sponsorship requests by sponsor id: ${sponsorId}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.sponsorId === sponsorId
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests for sponsor id: ${sponsorId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by sponsor id ${sponsorId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding requests by sponsor ID: ${sponsorId}.`);
|
||||
const requests = Array.from(this.requests.values()).filter(req => req.sponsorId === sponsorId);
|
||||
this.logger.info(`Found ${requests.length} requests by sponsor ID: ${sponsorId}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async findByStatus(status: SponsorshipRequestStatus): Promise<SponsorshipRequest[]> {
|
||||
this.logger.debug(`Finding sponsorship requests by status: ${status}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.status === status
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests with status: ${status}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding requests by status: ${status}.`);
|
||||
const requests = Array.from(this.requests.values()).filter(req => req.status === status);
|
||||
this.logger.info(`Found ${requests.length} requests with status: ${status}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async findBySponsorIdAndStatus(sponsorId: string, status: SponsorshipRequestStatus): Promise<SponsorshipRequest[]> {
|
||||
this.logger.debug(`Finding sponsorship requests by sponsor id: ${sponsorId} and status: ${status}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.sponsorId === sponsorId && request.status === status
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests for sponsor id: ${sponsorId}, status: ${status}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by sponsor id ${sponsorId}, status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding requests by sponsor ID ${sponsorId} and status ${status}.`);
|
||||
const requests = Array.from(this.requests.values()).filter(req => req.sponsorId === sponsorId && req.status === status);
|
||||
this.logger.info(`Found ${requests.length} requests by sponsor ID ${sponsorId} and status ${status}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async hasPendingRequest(sponsorId: string, entityType: SponsorableEntityType, entityId: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking for pending request from sponsor: ${sponsorId} for entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const has = Array.from(this.requests.values()).some(
|
||||
request =>
|
||||
request.sponsorId === sponsorId &&
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
);
|
||||
this.logger.debug(`Pending request exists: ${has}.`);
|
||||
return has;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking for pending request from sponsor ${sponsorId} for entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Checking for pending request for sponsor ${sponsorId}, entity ${entityType}:${entityId}.`);
|
||||
const exists = Array.from(this.requests.values()).some(req => req.sponsorId === sponsorId && req.entityType === entityType && req.entityId === entityId && req.status === 'pending');
|
||||
this.logger.info(`Pending request for sponsor ${sponsorId}, entity ${entityType}:${entityId} exists: ${exists}.`);
|
||||
return Promise.resolve(exists);
|
||||
}
|
||||
|
||||
async countPendingByEntity(entityType: SponsorableEntityType, entityId: string): Promise<number> {
|
||||
this.logger.debug(`Counting pending requests for entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const count = Array.from(this.requests.values()).filter(
|
||||
request =>
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
).length;
|
||||
this.logger.info(`Counted ${count} pending requests for entity: ${entityType}, ${entityId}.`);
|
||||
return count;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error counting pending requests for entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Counting pending requests for entity ${entityType}:${entityId}.`);
|
||||
const count = Array.from(this.requests.values()).filter(req => req.entityType === entityType && req.entityId === entityId && req.status === 'pending').length;
|
||||
this.logger.info(`Count of pending requests for entity ${entityType}:${entityId}: ${count}.`);
|
||||
return Promise.resolve(count);
|
||||
}
|
||||
|
||||
async create(request: SponsorshipRequest): Promise<SponsorshipRequest> {
|
||||
this.logger.debug(`Creating sponsorship request: ${request.id}`);
|
||||
try {
|
||||
if (this.requests.has(request.id)) {
|
||||
this.logger.warn(`SponsorshipRequest with ID ${request.id} already exists.`);
|
||||
throw new Error(`SponsorshipRequest with ID ${request.id} already exists`);
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.info(`SponsorshipRequest ${request.id} created successfully.`);
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating sponsorship request ${request.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Creating request: ${request.id}.`);
|
||||
if (this.requests.has(request.id)) {
|
||||
this.logger.warn(`Request with ID ${request.id} already exists.`);
|
||||
throw new Error('Sponsorship request already exists');
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.info(`Sponsorship request ${request.id} created successfully.`);
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
async update(request: SponsorshipRequest): Promise<SponsorshipRequest> {
|
||||
this.logger.debug(`Updating sponsorship request: ${request.id}`);
|
||||
try {
|
||||
if (!this.requests.has(request.id)) {
|
||||
this.logger.warn(`SponsorshipRequest ${request.id} not found for update.`);
|
||||
throw new Error(`SponsorshipRequest ${request.id} not found`);
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.info(`SponsorshipRequest ${request.id} updated successfully.`);
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating sponsorship request ${request.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Updating request: ${request.id}.`);
|
||||
if (!this.requests.has(request.id)) {
|
||||
this.logger.warn(`Request with ID ${request.id} not found for update.`);
|
||||
throw new Error('Sponsorship request not found');
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.info(`Sponsorship request ${request.id} updated successfully.`);
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting sponsorship request: ${id}`);
|
||||
try {
|
||||
if (this.requests.delete(id)) {
|
||||
this.logger.info(`SponsorshipRequest ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`SponsorshipRequest with id ${id} not found for deletion.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting sponsorship request ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Deleting request with ID: ${id}.`);
|
||||
if (this.requests.delete(id)) {
|
||||
this.logger.info(`Sponsorship request ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Request with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of sponsorship request with id: ${id}`);
|
||||
try {
|
||||
const exists = this.requests.has(id);
|
||||
this.logger.debug(`Sponsorship request ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of sponsorship request with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Checking existence of request with ID: ${id}.`);
|
||||
return Promise.resolve(this.requests.has(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed initial data
|
||||
*/
|
||||
seed(requests: SponsorshipRequest[]): void {
|
||||
this.logger.debug(`Seeding ${requests.length} sponsorship requests.`);
|
||||
try {
|
||||
for (const request of requests) {
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.debug(`Seeded sponsorship request: ${request.id}.`);
|
||||
}
|
||||
this.logger.info(`Successfully seeded ${requests.length} sponsorship requests.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding sponsorship requests:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data (for testing)
|
||||
*/
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all sponsorship requests.');
|
||||
this.requests.clear();
|
||||
this.logger.info('All sponsorship requests cleared.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
adapters/racing/ports/InMemoryDriverRatingProvider.ts
Normal file
32
adapters/racing/ports/InMemoryDriverRatingProvider.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { DriverRatingProvider } from '@gridpilot/racing/application/ports/DriverRatingProvider';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryDriverRatingProvider implements DriverRatingProvider {
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('InMemoryDriverRatingProvider initialized.');
|
||||
}
|
||||
|
||||
getRating(driverId: string): number | null {
|
||||
this.logger.debug(`[InMemoryDriverRatingProvider] Getting rating for driver: ${driverId}`);
|
||||
// Mock data for demonstration purposes
|
||||
if (driverId === 'driver-1') {
|
||||
return 2500;
|
||||
}
|
||||
if (driverId === 'driver-2') {
|
||||
return 2400;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getRatings(driverIds: string[]): Map<string, number> {
|
||||
this.logger.debug(`[InMemoryDriverRatingProvider] Getting ratings for drivers: ${driverIds.join(', ')}`);
|
||||
const ratingsMap = new Map<string, number>();
|
||||
for (const driverId of driverIds) {
|
||||
const rating = this.getRating(driverId);
|
||||
if (rating !== null) {
|
||||
ratingsMap.set(driverId, rating);
|
||||
}
|
||||
}
|
||||
return ratingsMap;
|
||||
}
|
||||
}
|
||||
33
adapters/racing/services/InMemoryDriverStatsService.ts
Normal file
33
adapters/racing/services/InMemoryDriverStatsService.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { IDriverStatsService, DriverStats } from '@gridpilot/racing/domain/services/IDriverStatsService';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryDriverStatsService implements IDriverStatsService {
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('InMemoryDriverStatsService initialized.');
|
||||
}
|
||||
|
||||
getDriverStats(driverId: string): DriverStats | null {
|
||||
this.logger.debug(`[InMemoryDriverStatsService] Getting stats for driver: ${driverId}`);
|
||||
|
||||
// Mock data for demonstration purposes
|
||||
if (driverId === 'driver-1') {
|
||||
return {
|
||||
rating: 2500,
|
||||
wins: 10,
|
||||
podiums: 15,
|
||||
totalRaces: 50,
|
||||
overallRank: 1,
|
||||
};
|
||||
}
|
||||
if (driverId === 'driver-2') {
|
||||
return {
|
||||
rating: 2400,
|
||||
wins: 8,
|
||||
podiums: 12,
|
||||
totalRaces: 45,
|
||||
overallRank: 2,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
20
adapters/racing/services/InMemoryRankingService.ts
Normal file
20
adapters/racing/services/InMemoryRankingService.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { IRankingService, DriverRanking } from '@gridpilot/racing/domain/services/IRankingService';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryRankingService implements IRankingService {
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('InMemoryRankingService initialized.');
|
||||
}
|
||||
|
||||
getAllDriverRankings(): DriverRanking[] {
|
||||
this.logger.debug('[InMemoryRankingService] Getting all driver rankings.');
|
||||
|
||||
// Mock data for demonstration purposes
|
||||
const mockRankings: DriverRanking[] = [
|
||||
{ driverId: 'driver-1', rating: 2500, overallRank: 1 },
|
||||
{ driverId: 'driver-2', rating: 2400, overallRank: 2 },
|
||||
{ driverId: 'driver-3', rating: 2300, overallRank: 3 },
|
||||
];
|
||||
return mockRankings;
|
||||
}
|
||||
}
|
||||
22
adapters/tsconfig.json
Normal file
22
adapters/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "../", // Base URL is the project root
|
||||
"paths": {
|
||||
"@gridpilot/core/*": ["core/*"],
|
||||
"@gridpilot/shared/*": ["core/shared/*"],
|
||||
"@gridpilot/identity/application/dto/*": ["core/identity/application/dto/*"],
|
||||
"@gridpilot/identity/application/ports/*": ["core/identity/application/ports/*"],
|
||||
"@gridpilot/identity/domain/repositories/*": ["core/identity/domain/repositories/*"],
|
||||
"@gridpilot/identity/domain/services/*": ["core/identity/domain/services/*"],
|
||||
"@gridpilot/racing/domain/repositories/*": ["core/racing/domain/repositories/*"],
|
||||
"@gridpilot/racing/domain/entities/*": ["core/racing/domain/entities/*"],
|
||||
"@gridpilot/racing/application/ports/*": ["core/racing/application/ports/*"]
|
||||
},
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "../" // Root directory is the project root
|
||||
},
|
||||
"include": ["**/*", "../core/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user