refactor adapters
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryAchievementRepository
|
||||
*
|
||||
* In-memory implementation of IAchievementRepository
|
||||
*/
|
||||
|
||||
import {
|
||||
Achievement,
|
||||
AchievementCategory,
|
||||
DRIVER_ACHIEVEMENTS,
|
||||
STEWARD_ACHIEVEMENTS,
|
||||
ADMIN_ACHIEVEMENTS,
|
||||
COMMUNITY_ACHIEVEMENTS,
|
||||
} from '../../domain/entities/Achievement';
|
||||
import { UserAchievement } from '../../domain/entities/UserAchievement';
|
||||
import type { IAchievementRepository } from '../../domain/repositories/IAchievementRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryAchievementRepository implements IAchievementRepository {
|
||||
private achievements: Map<string, Achievement> = new Map();
|
||||
private userAchievements: Map<string, UserAchievement> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryAchievementRepository initialized.');
|
||||
// Seed with predefined achievements
|
||||
this.seedAchievements();
|
||||
}
|
||||
|
||||
private seedAchievements(): void {
|
||||
this.logger.debug('Seeding predefined achievements.');
|
||||
const allAchievements = [
|
||||
...DRIVER_ACHIEVEMENTS,
|
||||
...STEWARD_ACHIEVEMENTS,
|
||||
...ADMIN_ACHIEVEMENTS,
|
||||
...COMMUNITY_ACHIEVEMENTS,
|
||||
];
|
||||
|
||||
for (const props of allAchievements) {
|
||||
const achievement = Achievement.create(props);
|
||||
this.achievements.set(achievement.id, achievement);
|
||||
this.logger.debug(`Seeded achievement: ${achievement.id} (${achievement.name}).`);
|
||||
}
|
||||
this.logger.info(`Seeded ${allAchievements.length} predefined achievements.`);
|
||||
}
|
||||
|
||||
// Achievement operations
|
||||
async findAchievementById(id: string): Promise<Achievement | null> {
|
||||
this.logger.debug(`Finding achievement by id: ${id}`);
|
||||
try {
|
||||
const achievement = this.achievements.get(id) ?? null;
|
||||
if (achievement) {
|
||||
this.logger.info(`Found achievement: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Achievement with id ${id} not found.`);
|
||||
}
|
||||
return achievement;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding achievement by id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findAllAchievements(): Promise<Achievement[]> {
|
||||
this.logger.debug('Finding all achievements.');
|
||||
try {
|
||||
const achievements = Array.from(this.achievements.values());
|
||||
this.logger.info(`Found ${achievements.length} achievements.`);
|
||||
return achievements;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all achievements:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findAchievementsByCategory(category: AchievementCategory): Promise<Achievement[]> {
|
||||
this.logger.debug(`Finding achievements by category: ${category}`);
|
||||
try {
|
||||
const achievements = Array.from(this.achievements.values())
|
||||
.filter(a => a.category === category);
|
||||
this.logger.info(`Found ${achievements.length} achievements for category: ${category}.`);
|
||||
return achievements;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding achievements by category ${category}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createAchievement(achievement: Achievement): Promise<Achievement> {
|
||||
this.logger.debug(`Creating achievement: ${achievement.id}`);
|
||||
try {
|
||||
if (this.achievements.has(achievement.id)) {
|
||||
this.logger.warn(`Achievement with ID ${achievement.id} already exists.`);
|
||||
throw new Error('Achievement with this ID already exists');
|
||||
}
|
||||
this.achievements.set(achievement.id, achievement);
|
||||
this.logger.info(`Achievement ${achievement.id} created successfully.`);
|
||||
return achievement;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating achievement ${achievement.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// UserAchievement operations
|
||||
async findUserAchievementById(id: string): Promise<UserAchievement | null> {
|
||||
this.logger.debug(`Finding user achievement by id: ${id}`);
|
||||
try {
|
||||
const userAchievement = this.userAchievements.get(id) ?? null;
|
||||
if (userAchievement) {
|
||||
this.logger.info(`Found user achievement: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`User achievement with id ${id} not found.`);
|
||||
}
|
||||
return userAchievement;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding user achievement by id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findUserAchievementsByUserId(userId: string): Promise<UserAchievement[]> {
|
||||
this.logger.debug(`Finding user achievements by user id: ${userId}`);
|
||||
try {
|
||||
const userAchievements = Array.from(this.userAchievements.values())
|
||||
.filter(ua => ua.userId === userId);
|
||||
this.logger.info(`Found ${userAchievements.length} user achievements for user id: ${userId}.`);
|
||||
return userAchievements;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding user achievements by user id ${userId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findUserAchievementByUserAndAchievement(
|
||||
userId: string,
|
||||
achievementId: string
|
||||
): Promise<UserAchievement | null> {
|
||||
this.logger.debug(`Finding user achievement for user: ${userId}, achievement: ${achievementId}`);
|
||||
try {
|
||||
for (const ua of this.userAchievements.values()) {
|
||||
if (ua.userId === userId && ua.achievementId === achievementId) {
|
||||
this.logger.info(`Found user achievement for user: ${userId}, achievement: ${achievementId}.`);
|
||||
return ua;
|
||||
}
|
||||
}
|
||||
this.logger.warn(`User achievement for user ${userId}, achievement ${achievementId} not found.`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding user achievement for user ${userId}, achievement ${achievementId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async hasUserEarnedAchievement(userId: string, achievementId: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking if user ${userId} earned achievement ${achievementId}`);
|
||||
try {
|
||||
const ua = await this.findUserAchievementByUserAndAchievement(userId, achievementId);
|
||||
const hasEarned = ua !== null && ua.isComplete();
|
||||
this.logger.debug(`User ${userId} earned achievement ${achievementId}: ${hasEarned}.`);
|
||||
return hasEarned;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking if user ${userId} earned achievement ${achievementId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
|
||||
this.logger.debug(`Creating user achievement: ${userAchievement.id}`);
|
||||
try {
|
||||
if (this.userAchievements.has(userAchievement.id)) {
|
||||
this.logger.warn(`UserAchievement with ID ${userAchievement.id} already exists.`);
|
||||
throw new Error('UserAchievement with this ID already exists');
|
||||
}
|
||||
this.userAchievements.set(userAchievement.id, userAchievement);
|
||||
this.logger.info(`UserAchievement ${userAchievement.id} created successfully.`);
|
||||
return userAchievement;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating user achievement ${userAchievement.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
|
||||
this.logger.debug(`Updating user achievement: ${userAchievement.id}`);
|
||||
try {
|
||||
if (!this.userAchievements.has(userAchievement.id)) {
|
||||
this.logger.warn(`UserAchievement with ID ${userAchievement.id} not found for update.`);
|
||||
throw new Error('UserAchievement not found');
|
||||
}
|
||||
this.userAchievements.set(userAchievement.id, userAchievement);
|
||||
this.logger.info(`UserAchievement ${userAchievement.id} updated successfully.`);
|
||||
return userAchievement;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating user achievement ${userAchievement.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Stats
|
||||
async getAchievementLeaderboard(limit: number): Promise<{ userId: string; points: number; count: number }[]> {
|
||||
this.logger.debug(`Getting achievement leaderboard with limit: ${limit}`);
|
||||
try {
|
||||
const userStats = new Map<string, { points: number; count: number }>();
|
||||
|
||||
for (const ua of this.userAchievements.values()) {
|
||||
if (!ua.isComplete()) continue;
|
||||
|
||||
const achievement = this.achievements.get(ua.achievementId);
|
||||
if (!achievement) {
|
||||
this.logger.warn(`Achievement ${ua.achievementId} not found while building leaderboard.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = userStats.get(ua.userId) ?? { points: 0, count: 0 };
|
||||
userStats.set(ua.userId, {
|
||||
points: existing.points + achievement.points,
|
||||
count: existing.count + 1,
|
||||
});
|
||||
}
|
||||
|
||||
const leaderboard = Array.from(userStats.entries())
|
||||
.map(([userId, stats]) => ({ userId, ...stats }))
|
||||
.sort((a, b) => b.points - a.points)
|
||||
.slice(0, limit);
|
||||
this.logger.info(`Generated achievement leaderboard with ${leaderboard.length} entries.`);
|
||||
return leaderboard;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting achievement leaderboard:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getUserAchievementStats(userId: string): Promise<{
|
||||
total: number;
|
||||
points: number;
|
||||
byCategory: Record<AchievementCategory, number>
|
||||
}> {
|
||||
this.logger.debug(`Getting achievement stats for user: ${userId}`);
|
||||
try {
|
||||
const userAchievements = await this.findUserAchievementsByUserId(userId);
|
||||
const completedAchievements = userAchievements.filter(ua => ua.isComplete());
|
||||
this.logger.debug(`Found ${completedAchievements.length} completed achievements for user ${userId}.`);
|
||||
|
||||
const byCategory: Record<AchievementCategory, number> = {
|
||||
driver: 0,
|
||||
steward: 0,
|
||||
admin: 0,
|
||||
community: 0,
|
||||
};
|
||||
|
||||
let points = 0;
|
||||
|
||||
for (const ua of completedAchievements) {
|
||||
const achievement = this.achievements.get(ua.achievementId);
|
||||
if (achievement) {
|
||||
points += achievement.points;
|
||||
byCategory[achievement.category]++;
|
||||
} else {
|
||||
this.logger.warn(`Achievement ${ua.achievementId} not found while calculating user stats for user ${userId}.`);
|
||||
}
|
||||
}
|
||||
|
||||
const stats = {
|
||||
total: completedAchievements.length,
|
||||
points,
|
||||
byCategory,
|
||||
};
|
||||
this.logger.info(`Generated achievement stats for user ${userId}:`, stats);
|
||||
return stats;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting user achievement stats for user ${userId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test helpers
|
||||
clearUserAchievements(): void {
|
||||
this.logger.debug('Clearing all user achievements.');
|
||||
this.userAchievements.clear();
|
||||
this.logger.info('All user achievements cleared.');
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all achievement data.');
|
||||
this.achievements.clear();
|
||||
this.userAchievements.clear();
|
||||
this.logger.info('All achievement data cleared.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Infrastructure: InMemorySponsorAccountRepository
|
||||
*
|
||||
* In-memory implementation of ISponsorAccountRepository for development/testing.
|
||||
*/
|
||||
|
||||
import type { ISponsorAccountRepository } from '../../domain/repositories/ISponsorAccountRepository';
|
||||
import type { SponsorAccount } from '../../domain/entities/SponsorAccount';
|
||||
import type { UserId } from '../../domain/value-objects/UserId';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySponsorAccountRepository implements ISponsorAccountRepository {
|
||||
private accounts: Map<string, SponsorAccount> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger, seedData?: SponsorAccount[]) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemorySponsorAccountRepository initialized.');
|
||||
if (seedData) {
|
||||
this.seed(seedData);
|
||||
}
|
||||
}
|
||||
|
||||
async save(account: SponsorAccount): Promise<void> {
|
||||
this.logger.debug(`Saving sponsor account: ${account.getId().value}`);
|
||||
try {
|
||||
this.accounts.set(account.getId().value, account);
|
||||
this.logger.info(`Sponsor account ${account.getId().value} saved successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving sponsor account ${account.getId().value}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: UserId): Promise<SponsorAccount | null> {
|
||||
this.logger.debug(`Finding sponsor account by id: ${id.value}`);
|
||||
try {
|
||||
const account = this.accounts.get(id.value) ?? null;
|
||||
if (account) {
|
||||
this.logger.info(`Found sponsor account: ${id.value}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor account with id ${id.value} not found.`);
|
||||
}
|
||||
return account;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsor account by id ${id.value}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findBySponsorId(sponsorId: string): Promise<SponsorAccount | null> {
|
||||
this.logger.debug(`Finding sponsor account by sponsor id: ${sponsorId}`);
|
||||
try {
|
||||
const account = Array.from(this.accounts.values()).find(
|
||||
a => a.getSponsorId() === sponsorId
|
||||
) ?? null;
|
||||
if (account) {
|
||||
this.logger.info(`Found sponsor account for sponsor id: ${sponsorId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor account for sponsor id ${sponsorId} not found.`);
|
||||
}
|
||||
return account;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsor account by sponsor id ${sponsorId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<SponsorAccount | null> {
|
||||
this.logger.debug(`Finding sponsor account by email: ${email}`);
|
||||
try {
|
||||
const normalizedEmail = email.toLowerCase().trim();
|
||||
const account = Array.from(this.accounts.values()).find(
|
||||
a => a.getEmail().toLowerCase() === normalizedEmail
|
||||
) ?? null;
|
||||
if (account) {
|
||||
this.logger.info(`Found sponsor account by email: ${email}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor account with email ${email} not found.`);
|
||||
}
|
||||
return account;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsor account by email ${email}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: UserId): Promise<void> {
|
||||
this.logger.debug(`Deleting sponsor account: ${id.value}`);
|
||||
try {
|
||||
if (this.accounts.delete(id.value)) {
|
||||
this.logger.info(`Sponsor account ${id.value} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor account with id ${id.value} not found for deletion.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting sponsor account ${id.value}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for testing
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all sponsor accounts.');
|
||||
this.accounts.clear();
|
||||
this.logger.info('All sponsor accounts cleared.');
|
||||
}
|
||||
|
||||
// Helper for seeding demo data
|
||||
seed(accounts: SponsorAccount[]): void {
|
||||
this.logger.debug(`Seeding ${accounts.length} sponsor accounts.`);
|
||||
try {
|
||||
accounts.forEach(a => {
|
||||
this.accounts.set(a.getId().value, a);
|
||||
this.logger.debug(`Seeded sponsor account: ${a.getId().value}.`);
|
||||
});
|
||||
this.logger.info(`Successfully seeded ${accounts.length} sponsor accounts.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding sponsor accounts:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryUserRatingRepository
|
||||
*
|
||||
* In-memory implementation of IUserRatingRepository
|
||||
*/
|
||||
|
||||
import { UserRating } from '../../domain/value-objects/UserRating';
|
||||
import type { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryUserRatingRepository implements IUserRatingRepository {
|
||||
private ratings: Map<string, UserRating> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger, seedData?: UserRating[]) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryUserRatingRepository initialized.');
|
||||
if (seedData) {
|
||||
seedData.forEach(rating => this.ratings.set(rating.userId, rating));
|
||||
this.logger.debug(`Seeded ${seedData.length} user ratings.`);
|
||||
}
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<UserRating | null> {
|
||||
this.logger.debug(`Finding user rating for user id: ${userId}`);
|
||||
try {
|
||||
const rating = this.ratings.get(userId) ?? null;
|
||||
if (rating) {
|
||||
this.logger.info(`Found user rating for user id: ${userId}.`);
|
||||
} else {
|
||||
this.logger.warn(`User rating for user id ${userId} not found.`);
|
||||
}
|
||||
return rating;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding user rating for user id ${userId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByUserIds(userIds: string[]): Promise<UserRating[]> {
|
||||
this.logger.debug(`Finding user ratings for user ids: ${userIds.join(', ')}`);
|
||||
try {
|
||||
const results: UserRating[] = [];
|
||||
for (const userId of userIds) {
|
||||
const rating = this.ratings.get(userId);
|
||||
if (rating) {
|
||||
results.push(rating);
|
||||
} else {
|
||||
this.logger.warn(`User rating for user id ${userId} not found.`);
|
||||
}
|
||||
}
|
||||
this.logger.info(`Found ${results.length} user ratings for ${userIds.length} requested users.`);
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding user ratings for user ids ${userIds.join(', ')}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async save(rating: UserRating): Promise<UserRating> {
|
||||
this.logger.debug(`Saving user rating for user id: ${rating.userId}`);
|
||||
try {
|
||||
if (this.ratings.has(rating.userId)) {
|
||||
this.logger.debug(`Updating existing user rating for user id: ${rating.userId}.`);
|
||||
} else {
|
||||
this.logger.debug(`Creating new user rating for user id: ${rating.userId}.`);
|
||||
}
|
||||
this.ratings.set(rating.userId, rating);
|
||||
this.logger.info(`User rating for user id ${rating.userId} saved successfully.`);
|
||||
return rating;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving user rating for user id ${rating.userId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getTopDrivers(limit: number): Promise<UserRating[]> {
|
||||
this.logger.debug(`Getting top ${limit} drivers.`);
|
||||
try {
|
||||
const topDrivers = Array.from(this.ratings.values())
|
||||
.filter(r => r.driver.sampleSize > 0)
|
||||
.sort((a, b) => b.driver.value - a.driver.value)
|
||||
.slice(0, limit);
|
||||
this.logger.info(`Retrieved ${topDrivers.length} top drivers.`);
|
||||
return topDrivers;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting top drivers:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getTopTrusted(limit: number): Promise<UserRating[]> {
|
||||
this.logger.debug(`Getting top ${limit} trusted users.`);
|
||||
try {
|
||||
const topTrusted = Array.from(this.ratings.values())
|
||||
.filter(r => r.trust.sampleSize > 0)
|
||||
.sort((a, b) => b.trust.value - a.trust.value)
|
||||
.slice(0, limit);
|
||||
this.logger.info(`Retrieved ${topTrusted.length} top trusted users.`);
|
||||
return topTrusted;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting top trusted users:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getEligibleStewards(): Promise<UserRating[]> {
|
||||
this.logger.debug('Getting eligible stewards.');
|
||||
try {
|
||||
const eligibleStewards = Array.from(this.ratings.values())
|
||||
.filter(r => r.canBeSteward());
|
||||
this.logger.info(`Found ${eligibleStewards.length} eligible stewards.`);
|
||||
return eligibleStewards;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting eligible stewards:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByDriverTier(tier: 'rookie' | 'amateur' | 'semi-pro' | 'pro' | 'elite'): Promise<UserRating[]> {
|
||||
this.logger.debug(`Finding user ratings by driver tier: ${tier}`);
|
||||
try {
|
||||
const ratingsByTier = Array.from(this.ratings.values())
|
||||
.filter(r => r.getDriverTier() === tier);
|
||||
this.logger.info(`Found ${ratingsByTier.length} user ratings for driver tier: ${tier}.`);
|
||||
return ratingsByTier;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding user ratings by driver tier ${tier}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(userId: string): Promise<void> {
|
||||
this.logger.debug(`Deleting user rating for user id: ${userId}`);
|
||||
try {
|
||||
if (this.ratings.delete(userId)) {
|
||||
this.logger.info(`User rating for user id ${userId} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`User rating for user id ${userId} not found for deletion.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting user rating for user id ${userId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all user ratings.');
|
||||
this.ratings.clear();
|
||||
this.logger.info('All user ratings cleared.');
|
||||
}
|
||||
}
|
||||
117
adapters/identity/persistence/inmemory/InMemoryUserRepository.ts
Normal file
117
adapters/identity/persistence/inmemory/InMemoryUserRepository.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* In-Memory User Repository
|
||||
*
|
||||
* Stores users in memory for demo/development purposes.
|
||||
*/
|
||||
|
||||
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryUserRepository implements IUserRepository {
|
||||
private users: Map<string, StoredUser> = new Map();
|
||||
private emailIndex: Map<string, string> = new Map(); // email -> userId
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger, initialUsers: StoredUser[] = []) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryUserRepository initialized.');
|
||||
for (const user of initialUsers) {
|
||||
this.users.set(user.id, user);
|
||||
this.emailIndex.set(user.email.toLowerCase(), user.id);
|
||||
this.logger.debug(`Seeded user: ${user.id} (${user.email}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<StoredUser | null> {
|
||||
this.logger.debug(`Finding user by email: ${email}`);
|
||||
try {
|
||||
const userId = this.emailIndex.get(email.toLowerCase());
|
||||
if (!userId) {
|
||||
this.logger.warn(`User with email ${email} not found.`);
|
||||
return null;
|
||||
}
|
||||
const user = this.users.get(userId) ?? null;
|
||||
if (user) {
|
||||
this.logger.info(`Found user by email: ${email}.`);
|
||||
} else {
|
||||
this.logger.warn(`User with ID ${userId} (from email index) not found.`);
|
||||
}
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding user by email ${email}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<StoredUser | null> {
|
||||
this.logger.debug(`Finding user by id: ${id}`);
|
||||
try {
|
||||
const user = this.users.get(id) ?? null;
|
||||
if (user) {
|
||||
this.logger.info(`Found user: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`User with id ${id} not found.`);
|
||||
}
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding user by id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(user: StoredUser): Promise<StoredUser> {
|
||||
this.logger.debug(`Creating user: ${user.id} with email: ${user.email}`);
|
||||
try {
|
||||
if (this.emailIndex.has(user.email.toLowerCase())) {
|
||||
this.logger.warn(`Email ${user.email} already exists.`);
|
||||
throw new Error('Email already exists');
|
||||
}
|
||||
this.users.set(user.id, user);
|
||||
this.emailIndex.set(user.email.toLowerCase(), user.id);
|
||||
this.logger.info(`User ${user.id} (${user.email}) created successfully.`);
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating user ${user.id} (${user.email}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(user: StoredUser): Promise<StoredUser> {
|
||||
this.logger.debug(`Updating user: ${user.id} with email: ${user.email}`);
|
||||
try {
|
||||
const existing = this.users.get(user.id);
|
||||
if (!existing) {
|
||||
this.logger.warn(`User with ID ${user.id} not found for update.`);
|
||||
throw new Error('User not found');
|
||||
}
|
||||
// If email changed, update index
|
||||
if (existing.email.toLowerCase() !== user.email.toLowerCase()) {
|
||||
if (this.emailIndex.has(user.email.toLowerCase()) && this.emailIndex.get(user.email.toLowerCase()) !== user.id) {
|
||||
this.logger.warn(`Cannot update user ${user.id} to email ${user.email} as it's already taken.`);
|
||||
throw new Error('Email already exists for another user');
|
||||
}
|
||||
this.logger.debug(`Updating email index from ${existing.email} to ${user.email}.`);
|
||||
this.emailIndex.delete(existing.email.toLowerCase());
|
||||
this.emailIndex.set(user.email.toLowerCase(), user.id);
|
||||
}
|
||||
this.users.set(user.id, user);
|
||||
this.logger.info(`User ${user.id} (${user.email}) updated successfully.`);
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating user ${user.id} (${user.email}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async emailExists(email: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of email: ${email}`);
|
||||
try {
|
||||
const exists = this.emailIndex.has(email.toLowerCase());
|
||||
this.logger.debug(`Email ${email} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of email ${email}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user