fix issues in core

This commit is contained in:
2025-12-23 17:31:45 +01:00
parent d04a21fe02
commit 4318b380d9
34 changed files with 116 additions and 103 deletions

View File

@@ -1,6 +0,0 @@
export interface Logger {
debug(message: string, ...args: unknown[]): void;
info(message: string, ...args: unknown[]): void;
warn(message: string, ...args: unknown[]): void;
error(message: string, ...args: unknown[]): void;
}

View File

@@ -1,7 +1,6 @@
import { describe, it, expect, vi, type Mock } from 'vitest';
import { GetCurrentUserSessionUseCase } from './GetCurrentUserSessionUseCase';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
describe('GetCurrentUserSessionUseCase', () => {
@@ -11,7 +10,7 @@ describe('GetCurrentUserSessionUseCase', () => {
clearSession: Mock;
};
let logger: Logger;
let output: UseCaseOutputPort<AuthSessionDTO | null> & { present: Mock };
let output: UseCaseOutputPort<AuthSession | null> & { present: Mock };
let useCase: GetCurrentUserSessionUseCase;
beforeEach(() => {
@@ -40,7 +39,7 @@ describe('GetCurrentUserSessionUseCase', () => {
});
it('returns the current auth session when one exists', async () => {
const session: AuthSessionDTO = {
const session: AuthSession = {
user: {
id: 'user-1',
email: 'test@example.com',

View File

@@ -2,9 +2,8 @@ import { describe, it, expect, vi, type Mock } from 'vitest';
import { HandleAuthCallbackUseCase } from './HandleAuthCallbackUseCase';
import type { IdentityProviderPort } from '../ports/IdentityProviderPort';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { AuthCallbackCommandDTO } from '../dto/AuthCallbackCommandDTO';
import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO';
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
import type { AuthCallbackCommand, AuthenticatedUser } from '../ports/IdentityProviderPort';
import type { AuthSession } from '../ports/IdentitySessionPort';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
describe('HandleAuthCallbackUseCase', () => {
@@ -17,7 +16,7 @@ describe('HandleAuthCallbackUseCase', () => {
clearSession: Mock;
};
let logger: Logger;
let output: UseCaseOutputPort<AuthSessionDTO> & { present: Mock };
let output: UseCaseOutputPort<AuthSession> & { present: Mock };
let useCase: HandleAuthCallbackUseCase;
beforeEach(() => {
@@ -48,20 +47,20 @@ describe('HandleAuthCallbackUseCase', () => {
});
it('completes auth and creates a session', async () => {
const command: AuthCallbackCommandDTO = {
const command: AuthCallbackCommand = {
provider: 'IRACING_DEMO',
code: 'auth-code',
state: 'state-123',
returnTo: 'https://app/callback',
};
const user: AuthenticatedUserDTO = {
const user: AuthenticatedUser = {
id: 'user-1',
email: 'test@example.com',
displayName: 'Test User',
};
const session: AuthSessionDTO = {
const session: AuthSession = {
user,
issuedAt: Date.now(),
expiresAt: Date.now() + 1000,

View File

@@ -2,8 +2,7 @@ import { describe, it, expect, vi, type Mock } from 'vitest';
import { SignupWithEmailUseCase } from './SignupWithEmailUseCase';
import type { SignupWithEmailInput } from './SignupWithEmailUseCase';
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
type SignupWithEmailOutput = unknown;
@@ -58,7 +57,7 @@ describe('SignupWithEmailUseCase', () => {
userRepository.findByEmail.mockResolvedValue(null);
const session: AuthSessionDTO = {
const session: AuthSession = {
user: {
id: 'user-1',
email: command.email.toLowerCase(),

View File

@@ -5,7 +5,7 @@
*/
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO';
import type { AuthenticatedUser } from '../ports/IdentityProviderPort';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
@@ -103,7 +103,7 @@ export class SignupWithEmailUseCase {
await this.userRepository.create(newUser);
// Create session
const authenticatedUser: AuthenticatedUserDTO = {
const authenticatedUser: AuthenticatedUser = {
id: newUser.id,
displayName: newUser.displayName,
email: newUser.email,

View File

@@ -1,6 +0,0 @@
/**
* Infrastructure layer exports for notifications package
*/
// This infrastructure layer is empty as the actual implementations
// are in the adapters directory

View File

@@ -2,5 +2,4 @@ export * from './application/Result';
export * as application from './application';
export * as domain from './domain';
export * as errors from './errors';
export * from './presentation';
export * from './application/AsyncUseCase';

View File

@@ -1,6 +0,0 @@
// This must not be used within core. It's in presentation layer, e.g. to be used in an API.
export interface Presenter<InputDTO, ResponseModel> {
present(input: InputDTO): void;
getResponseModel(): ResponseModel | null;
reset(): void;
}

View File

@@ -1 +0,0 @@
export * from './Presenter';

View File

@@ -1,8 +0,0 @@
export interface CurrentUserSocialDTO {
driverId: string;
displayName: string;
avatarUrl: string;
countryCode: string;
primaryTeamId?: string;
primaryLeagueId?: string;
}

View File

@@ -1,22 +0,0 @@
export type FeedItemType =
| 'race_result'
| 'championship_standing'
| 'league_announcement'
| 'friend_joined_league'
| 'friend_won_race';
export interface FeedItemDTO {
id: string;
timestamp: string;
type: FeedItemType;
actorFriendId?: string;
actorDriverId?: string;
leagueId?: string;
raceId?: string;
teamId?: string;
position?: number;
headline: string;
body?: string;
ctaLabel?: string;
ctaHref?: string;
}

View File

@@ -1,9 +1,13 @@
export interface FriendDTO {
export type SocialUserSummary = {
driverId: string;
displayName: string;
avatarUrl: string;
countryCode: string;
primaryTeamId?: string;
primaryLeagueId?: string;
};
export type SocialFriendSummary = SocialUserSummary & {
isOnline: boolean;
lastSeen: Date;
primaryLeagueId?: string;
primaryTeamId?: string;
}
};

View File

@@ -2,8 +2,7 @@ import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { ISocialGraphRepository } from '../../domain/repositories/ISocialGraphRepository';
import type { CurrentUserSocialDTO } from '../dto/CurrentUserSocialDTO';
import type { FriendDTO } from '../dto/FriendDTO';
import type { SocialFriendSummary, SocialUserSummary } from '../types/SocialUser';
export interface GetCurrentUserSocialParams {
driverId: string;
@@ -12,8 +11,8 @@ export interface GetCurrentUserSocialParams {
export type GetCurrentUserSocialInput = GetCurrentUserSocialParams;
export interface GetCurrentUserSocialResult {
currentUser: CurrentUserSocialDTO;
friends: FriendDTO[];
currentUser: SocialUserSummary;
friends: SocialFriendSummary[];
}
export type GetCurrentUserSocialErrorCode = 'REPOSITORY_ERROR';
@@ -61,15 +60,16 @@ export class GetCurrentUserSocialUseCase {
// The social graph context currently only knows about relationships.
// Profile fields for the current user are expected to be enriched by identity/profile contexts.
const friends: FriendDTO[] = friendsDomain.map((friend) => ({
const friends: SocialFriendSummary[] = friendsDomain.map((friend) => ({
driverId: friend.id,
displayName: friend.name.toString(),
avatarUrl: '',
countryCode: '',
isOnline: false,
lastSeen: new Date(),
}));
const currentUser: CurrentUserSocialDTO = {
const currentUser: SocialUserSummary = {
driverId,
displayName: '',
avatarUrl: '',

View File

@@ -1,146 +0,0 @@
import type { Driver } from '@core/racing/domain/entities/Driver';
import type { Logger } from '@core/shared/application';
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import type { FeedItem } from '@core/social/domain/types/FeedItem';
export type Friendship = {
driverId: string;
friendId: string;
};
export type RacingSeedData = {
drivers: Driver[];
friendships: Friendship[];
feedEvents: FeedItem[];
};
export class InMemoryFeedRepository implements IFeedRepository {
private readonly feedEvents: FeedItem[];
private readonly friendships: Friendship[];
private readonly logger: Logger;
constructor(logger: Logger, seed: RacingSeedData) {
this.logger = logger;
this.logger.info('InMemoryFeedRepository initialized.');
this.feedEvents = seed.feedEvents;
this.friendships = seed.friendships;
}
async getFeedForDriver(driverId: string, limit?: number): Promise<FeedItem[]> {
this.logger.debug(`Getting feed for driver: ${driverId}, limit: ${limit}`);
try {
const friendIds = new Set(
this.friendships
.filter((f) => f.driverId === driverId)
.map((f) => f.friendId),
);
const items = this.feedEvents.filter((item) => {
if (item.actorDriverId && friendIds.has(item.actorDriverId)) {
return true;
}
return false;
});
const sorted = items
.slice()
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
this.logger.info(`Found ${sorted.length} feed items for driver: ${driverId}.`);
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
} catch (error) {
this.logger.error(`Error getting feed for driver ${driverId}:`, error as Error);
throw error;
}
}
async getGlobalFeed(limit?: number): Promise<FeedItem[]> {
this.logger.debug(`Getting global feed, limit: ${limit}`);
try {
const sorted = this.feedEvents
.slice()
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
this.logger.info(`Found ${sorted.length} global feed items.`);
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
} catch (error) {
this.logger.error(`Error getting global feed:`, error as Error);
throw error;
}
}
}
export class InMemorySocialGraphRepository implements ISocialGraphRepository {
private readonly friendships: Friendship[];
private readonly driversById: Map<string, Driver>;
private readonly logger: Logger;
constructor(logger: Logger, seed: RacingSeedData) {
this.logger = logger;
this.logger.info('InMemorySocialGraphRepository initialized.');
this.friendships = seed.friendships;
this.driversById = new Map(seed.drivers.map((d) => [d.id, d]));
}
async getFriendIds(driverId: string): Promise<string[]> {
this.logger.debug(`Getting friend IDs for driver: ${driverId}`);
try {
const friendIds = this.friendships
.filter((f) => f.driverId === driverId)
.map((f) => f.friendId);
this.logger.info(`Found ${friendIds.length} friend IDs for driver: ${driverId}.`);
return friendIds;
} catch (error) {
this.logger.error(`Error getting friend IDs for driver ${driverId}:`, error as Error);
throw error;
}
}
async getFriends(driverId: string): Promise<Driver[]> {
this.logger.debug(`Getting friends for driver: ${driverId}`);
try {
const ids = await this.getFriendIds(driverId);
const friends = ids
.map((id) => this.driversById.get(id))
.filter((d): d is Driver => Boolean(d));
this.logger.info(`Found ${friends.length} friends for driver: ${driverId}.`);
return friends;
} catch (error) {
this.logger.error(`Error getting friends for driver ${driverId}:`, error as Error);
throw error;
}
}
async getSuggestedFriends(driverId: string, limit?: number): Promise<Driver[]> {
this.logger.debug(`Getting suggested friends for driver: ${driverId}, limit: ${limit}`);
try {
const directFriendIds = new Set(await this.getFriendIds(driverId));
const suggestions = new Map<string, number>();
for (const friendship of this.friendships) {
if (!directFriendIds.has(friendship.driverId)) continue;
const friendOfFriendId = friendship.friendId;
if (friendOfFriendId === driverId) continue;
if (directFriendIds.has(friendOfFriendId)) continue;
suggestions.set(friendOfFriendId, (suggestions.get(friendOfFriendId) ?? 0) + 1);
}
const rankedIds = Array.from(suggestions.entries())
.sort((a, b) => b[1] - a[1])
.map(([id]) => id);
const drivers = rankedIds
.map((id) => this.driversById.get(id))
.filter((d): d is Driver => Boolean(d));
const result = typeof limit === 'number' ? drivers.slice(0, limit) : drivers;
this.logger.info(`Found ${result.length} suggested friends for driver: ${driverId}.`);
return result;
} catch (error) {
this.logger.error(`Error getting suggested friends for driver ${driverId}:`, error as Error);
throw error;
}
}
}