From 8adf55e2aca35030f851569b429cf839495e1ce0 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 15 Dec 2025 15:33:42 +0100 Subject: [PATCH] wip --- .../application/analytics/analytics.module.ts | 33 --- .../analytics/analytics.service.spec.ts | 91 ------ .../analytics/analytics.service.ts | 27 -- .../record-engagement.use-case.spec.ts | 119 -------- .../analytics/record-engagement.use-case.ts | 68 ----- .../record-page-view.use-case.spec.ts | 80 ----- .../analytics/record-page-view.use-case.ts | 60 ---- .../in-memory-engagement.repository.ts | 81 ------ .../in-memory-page-view.repository.ts | 81 ------ .../analytics/mappers/PageViewMapper.spec.ts | 108 ------- .../analytics/mappers/PageViewMapper.ts | 39 --- .../analytics/typeorm-page-view.entity.ts | 36 --- .../typeorm-page-view.repository.spec.ts | 275 ------------------ .../analytics/typeorm-page-view.repository.ts | 74 ----- {testing => core/testing-support}/index.ts | 0 .../testing-support}/package.json | 0 .../testing-support}/src/faker/faker.ts | 0 .../testing-support}/src/images/images.ts | 0 .../src/media/DemoAvatarGenerationAdapter.ts | 0 .../src/media/DemoFaceValidationAdapter.ts | 0 .../src/media/DemoImageServiceAdapter.ts | 0 .../InMemoryAvatarGenerationRepository.ts | 0 .../testing-support}/src/racing/DemoCars.ts | 0 .../src/racing/DemoDriverStats.ts | 0 .../testing-support}/src/racing/DemoTracks.ts | 0 .../src/racing/RacingFeedSeed.ts | 0 .../src/racing/RacingSeedCore.ts | 0 .../src/racing/RacingSponsorshipSeed.ts | 0 .../src/racing/RacingStaticSeed.ts | 0 .../testing-support}/tsconfig.json | 0 package-lock.json | 3 +- 31 files changed, 2 insertions(+), 1173 deletions(-) delete mode 100644 apps/api/src/application/analytics/analytics.module.ts delete mode 100644 apps/api/src/application/analytics/analytics.service.spec.ts delete mode 100644 apps/api/src/application/analytics/analytics.service.ts delete mode 100644 apps/api/src/application/analytics/record-engagement.use-case.spec.ts delete mode 100644 apps/api/src/application/analytics/record-engagement.use-case.ts delete mode 100644 apps/api/src/application/analytics/record-page-view.use-case.spec.ts delete mode 100644 apps/api/src/application/analytics/record-page-view.use-case.ts delete mode 100644 apps/api/src/infrastructure/analytics/in-memory-engagement.repository.ts delete mode 100644 apps/api/src/infrastructure/analytics/in-memory-page-view.repository.ts delete mode 100644 apps/api/src/infrastructure/analytics/mappers/PageViewMapper.spec.ts delete mode 100644 apps/api/src/infrastructure/analytics/mappers/PageViewMapper.ts delete mode 100644 apps/api/src/infrastructure/analytics/typeorm-page-view.entity.ts delete mode 100644 apps/api/src/infrastructure/analytics/typeorm-page-view.repository.spec.ts delete mode 100644 apps/api/src/infrastructure/analytics/typeorm-page-view.repository.ts rename {testing => core/testing-support}/index.ts (100%) rename {testing => core/testing-support}/package.json (100%) rename {testing => core/testing-support}/src/faker/faker.ts (100%) rename {testing => core/testing-support}/src/images/images.ts (100%) rename {testing => core/testing-support}/src/media/DemoAvatarGenerationAdapter.ts (100%) rename {testing => core/testing-support}/src/media/DemoFaceValidationAdapter.ts (100%) rename {testing => core/testing-support}/src/media/DemoImageServiceAdapter.ts (100%) rename {testing => core/testing-support}/src/media/InMemoryAvatarGenerationRepository.ts (100%) rename {testing => core/testing-support}/src/racing/DemoCars.ts (100%) rename {testing => core/testing-support}/src/racing/DemoDriverStats.ts (100%) rename {testing => core/testing-support}/src/racing/DemoTracks.ts (100%) rename {testing => core/testing-support}/src/racing/RacingFeedSeed.ts (100%) rename {testing => core/testing-support}/src/racing/RacingSeedCore.ts (100%) rename {testing => core/testing-support}/src/racing/RacingSponsorshipSeed.ts (100%) rename {testing => core/testing-support}/src/racing/RacingStaticSeed.ts (100%) rename {testing => core/testing-support}/tsconfig.json (100%) diff --git a/apps/api/src/application/analytics/analytics.module.ts b/apps/api/src/application/analytics/analytics.module.ts deleted file mode 100644 index 270dfc3aa..000000000 --- a/apps/api/src/application/analytics/analytics.module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Module, ConsoleLogger } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { AnalyticsService } from './analytics.service'; -import { AnalyticsController } from '../../presentation/analytics.controller'; -import { RecordPageViewUseCase } from './record-page-view.use-case'; -import { RecordEngagementUseCase } from './record-engagement.use-case'; -import { TypeOrmPageViewRepository } from '../../infrastructure/analytics/typeorm-page-view.repository'; -import { InMemoryEngagementRepository } from '../../infrastructure/analytics/in-memory-engagement.repository'; -import { PageViewEntity } from '../../infrastructure/analytics/typeorm-page-view.entity'; - -@Module({ - imports: [TypeOrmModule.forFeature([PageViewEntity])], - controllers: [AnalyticsController], - providers: [ - AnalyticsService, - RecordPageViewUseCase, - RecordEngagementUseCase, - { - provide: 'IPageViewRepository', - useClass: TypeOrmPageViewRepository, - }, - { - provide: 'IEngagementRepository', - useClass: InMemoryEngagementRepository, - }, - { - provide: 'ILogger', - useClass: ConsoleLogger, // Using ConsoleLogger for now - }, - ], - exports: [AnalyticsService], -}) -export class AnalyticsModule {} diff --git a/apps/api/src/application/analytics/analytics.service.spec.ts b/apps/api/src/application/analytics/analytics.service.spec.ts deleted file mode 100644 index 484bf4159..000000000 --- a/apps/api/src/application/analytics/analytics.service.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AnalyticsService } from './analytics.service'; -import { RecordPageViewUseCase, RecordPageViewInput } from './record-page-view.use-case'; -import { - RecordEngagementUseCase, - RecordEngagementInput, - RecordEngagementOutput, -} from './record-engagement.use-case'; -import { ILogger } from '@gridpilot/shared/logging/ILogger'; -import { EntityType, VisitorType } from '@gridpilot/analytics/domain/types/PageView'; -import { EngagementAction, EngagementEntityType } from '@gridpilot/analytics/domain/types/EngagementEvent'; - -describe('AnalyticsService', () => { - let service: AnalyticsService; - let recordPageViewUseCase: RecordPageViewUseCase; - let recordEngagementUseCase: RecordEngagementUseCase; - let logger: ILogger; - - const mockRecordPageViewInput: RecordPageViewInput = { - entityType: EntityType.LEAGUE, - entityId: 'league-123', - visitorType: VisitorType.ANONYMOUS, - sessionId: 'session-abc', - }; - - const mockRecordEngagementInput: RecordEngagementInput = { - action: 'click_sponsor_logo', - entityType: 'sponsor', - entityId: 'sponsor-456', - actorType: 'driver', - actorId: 'driver-789', - sessionId: 'session-def', - metadata: { campaign: 'summer-promo' }, - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AnalyticsService, - { - provide: RecordPageViewUseCase, - useValue: { - execute: jest.fn().mockResolvedValue({ pageViewId: 'new-pv-123' }), - }, - }, - { - provide: RecordEngagementUseCase, - useValue: { - execute: jest.fn().mockResolvedValue({ eventId: 'new-eng-456', engagementWeight: 10 }), - }, - }, - { - provide: 'ILogger', - useValue: { - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - } as ILogger, - }, - ], - }).compile(); - - service = module.get(AnalyticsService); - recordPageViewUseCase = module.get(RecordPageViewUseCase); - recordEngagementUseCase = module.get(RecordEngagementUseCase); - logger = module.get('ILogger'); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - it('should call recordPageViewUseCase.execute and return pageViewId', async () => { - const result = await service.recordPageView(mockRecordPageViewInput); - - expect(recordPageViewUseCase.execute).toHaveBeenCalledTimes(1); - expect(recordPageViewUseCase.execute).toHaveBeenCalledWith(mockRecordPageViewInput); - expect(result).toEqual({ pageViewId: 'new-pv-123' }); - expect(logger.debug).toHaveBeenCalledWith('AnalyticsService: Recording page view', { input: mockRecordPageViewInput }); - }); - - it('should call recordEngagementUseCase.execute and return engagement details', async () => { - const result = await service.recordEngagement(mockRecordEngagementInput); - - expect(recordEngagementUseCase.execute).toHaveBeenCalledTimes(1); - expect(recordEngagementUseCase.execute).toHaveBeenCalledWith(mockRecordEngagementInput); - expect(result).toEqual({ eventId: 'new-eng-456', engagementWeight: 10 }); - expect(logger.debug).toHaveBeenCalledWith('AnalyticsService: Recording engagement', { input: mockRecordEngagementInput }); - }); -}); diff --git a/apps/api/src/application/analytics/analytics.service.ts b/apps/api/src/application/analytics/analytics.service.ts deleted file mode 100644 index cf7fbf9ba..000000000 --- a/apps/api/src/application/analytics/analytics.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable, Inject } from '@nestjs/common'; -import { RecordPageViewUseCase, RecordPageViewInput } from './record-page-view.use-case'; -import { - RecordEngagementUseCase, - RecordEngagementInput, - RecordEngagementOutput, -} from './record-engagement.use-case'; -import { ILogger } from '@gridpilot/shared/logging/ILogger'; - -@Injectable() -export class AnalyticsService { - constructor( - private readonly recordPageViewUseCase: RecordPageViewUseCase, - private readonly recordEngagementUseCase: RecordEngagementUseCase, - @Inject('ILogger') private readonly logger: ILogger, - ) {} - - async recordPageView(input: RecordPageViewInput): Promise<{ pageViewId: string }> { - this.logger.debug('AnalyticsService: Recording page view', { input }); - return this.recordPageViewUseCase.execute(input); - } - - async recordEngagement(input: RecordEngagementInput): Promise { - this.logger.debug('AnalyticsService: Recording engagement', { input }); - return this.recordEngagementUseCase.execute(input); - } -} diff --git a/apps/api/src/application/analytics/record-engagement.use-case.spec.ts b/apps/api/src/application/analytics/record-engagement.use-case.spec.ts deleted file mode 100644 index f636031d9..000000000 --- a/apps/api/src/application/analytics/record-engagement.use-case.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { RecordEngagementUseCase, RecordEngagementInput } from './record-engagement.use-case'; -import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository'; -import { ILogger } from '@gridpilot/shared/logging/ILogger'; -import { EngagementEvent } from '@gridpilot/analytics/domain/entities/EngagementEvent'; - -describe('RecordEngagementUseCase', () => { - let useCase: RecordEngagementUseCase; - let engagementRepository: IEngagementRepository; - let logger: ILogger; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - RecordEngagementUseCase, - { - provide: 'IEngagementRepository', - useValue: { - save: jest.fn(), - findById: jest.fn(), - findByEntityId: jest.fn(), - findByAction: jest.fn(), - findByDateRange: jest.fn(), - countByAction: jest.fn(), - getSponsorClicksForEntity: jest.fn(), - }, - }, - { - provide: 'ILogger', - useValue: { - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }, - }, - ], - }).compile(); - - useCase = module.get(RecordEngagementUseCase); - engagementRepository = module.get('IEngagementRepository'); - logger = module.get('ILogger'); - }); - - it('should be defined', () => { - expect(useCase).toBeDefined(); - }); - - it('should record an engagement event and save it to the repository', async () => { - const input: RecordEngagementInput = { - action: 'click_sponsor_logo', - entityType: 'sponsor', - entityId: 'sponsor-123', - actorType: 'driver', - actorId: 'driver-456', - sessionId: 'session-789', - metadata: { campaign: 'spring-sale' }, - }; - - const result = await useCase.execute(input); - - expect(result).toBeDefined(); - expect(result.eventId).toMatch(/^eng-\d{13}-[a-z0-9]{9}$/); - expect(result.engagementWeight).toBeGreaterThan(0); - expect(engagementRepository.save).toHaveBeenCalledTimes(1); - const savedEvent: EngagementEvent = (engagementRepository.save as jest.Mock).mock.calls[0][0]; - expect(savedEvent).toBeInstanceOf(EngagementEvent); - expect(savedEvent.action).toBe(input.action); - expect(savedEvent.entityType).toBe(input.entityType); - expect(savedEvent.entityId).toBe(input.entityId); - expect(savedEvent.actorType).toBe(input.actorType); - expect(savedEvent.actorId).toBe(input.actorId); - expect(savedEvent.sessionId).toBe(input.sessionId); - expect(savedEvent.metadata).toEqual(input.metadata); - expect(logger.info).toHaveBeenCalledWith( - 'Engagement recorded successfully', - { eventId: expect.any(String), input }, - ); - }); - - it('should handle engagement events without actorId or metadata', async () => { - const input: RecordEngagementInput = { - action: 'view_standings', - entityType: 'league', - entityId: 'home-page', - actorType: 'anonymous', - sessionId: 'session-abc', - }; - - const result = await useCase.execute(input); - - expect(result).toBeDefined(); - expect(result.eventId).toMatch(/^eng-\d{13}-[a-z0-9]{9}$/); - expect(result.engagementWeight).toBeGreaterThan(0); - expect(engagementRepository.save).toHaveBeenCalledTimes(1); - const savedEvent: EngagementEvent = (engagementRepository.save as jest.Mock).mock.calls[0][0]; - expect(savedEvent.actorId).toBeUndefined(); - expect(savedEvent.metadata).toBeUndefined(); - }); - - it('should log an error if saving to repository fails', async () => { - const input: RecordEngagementInput = { - action: 'click_sponsor_url', - entityType: 'sponsor', - entityId: 'ad-001', - actorType: 'anonymous', - sessionId: 'session-xyz', - }; - const error = new Error('Repository save failed'); - (engagementRepository.save as jest.Mock).mockRejectedValue(error); - - await expect(useCase.execute(input)).rejects.toThrow(error); - expect(logger.error).toHaveBeenCalledWith( - 'Error recording engagement', - error, - { input }, - ); - }); -}); diff --git a/apps/api/src/application/analytics/record-engagement.use-case.ts b/apps/api/src/application/analytics/record-engagement.use-case.ts deleted file mode 100644 index 38d039838..000000000 --- a/apps/api/src/application/analytics/record-engagement.use-case.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Inject } from '@nestjs/common'; - -/** - * Use Case: RecordEngagementUseCase - * - * Records an engagement event when a visitor interacts with an entity. - */ - -import type { AsyncUseCase } from '@gridpilot/shared/application'; -import type { ILogger } from '@gridpilot/shared/logging/ILogger'; -import { EngagementEvent, type EngagementAction, type EngagementEntityType } from '@gridpilot/analytics/domain/entities/EngagementEvent'; -import type { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository'; - -export interface RecordEngagementInput { - action: EngagementAction; - entityType: EngagementEntityType; - entityId: string; - actorId?: string; - actorType: 'anonymous' | 'driver' | 'sponsor'; - sessionId: string; - metadata?: Record; -} - -export interface RecordEngagementOutput { - eventId: string; - engagementWeight: number; -} - -export class RecordEngagementUseCase - implements AsyncUseCase { - constructor( - @Inject('IEngagementRepository') private readonly engagementRepository: IEngagementRepository, - @Inject('ILogger') private readonly logger: ILogger, - ) {} - - async execute(input: RecordEngagementInput): Promise { - this.logger.debug('Executing RecordEngagementUseCase', { input }); - try { - const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - const baseProps: Omit[0], 'timestamp'> = { - id: eventId, - action: input.action, - entityType: input.entityType, - entityId: input.entityId, - actorType: input.actorType, - sessionId: input.sessionId, - }; - - const event = EngagementEvent.create({ - ...baseProps, - ...(input.actorId !== undefined ? { actorId: input.actorId } : {}), - ...(input.metadata !== undefined ? { metadata: input.metadata } : {}), - }); - - await this.engagementRepository.save(event); - this.logger.info('Engagement recorded successfully', { eventId, input }); - - return { - eventId, - engagementWeight: event.getEngagementWeight(), - }; - } catch (error) { - this.logger.error('Error recording engagement', error, { input }); - throw error; - } - } -} diff --git a/apps/api/src/application/analytics/record-page-view.use-case.spec.ts b/apps/api/src/application/analytics/record-page-view.use-case.spec.ts deleted file mode 100644 index bad51f697..000000000 --- a/apps/api/src/application/analytics/record-page-view.use-case.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { RecordPageViewUseCase, RecordPageViewInput, RecordPageViewOutput } from './record-page-view.use-case'; -import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository'; -import { ILogger } from '@gridpilot/shared/logging/ILogger'; -import { PageView } from '@gridpilot/analytics/domain/entities/PageView'; -import { EntityType, VisitorType } from '@gridpilot/analytics/domain/types/PageView'; - -describe('RecordPageViewUseCase', () => { - let useCase: RecordPageViewUseCase; - let pageViewRepository: IPageViewRepository; - let logger: ILogger; - - const mockRecordPageViewInput: RecordPageViewInput = { - entityType: EntityType.LEAGUE, - entityId: 'league-123', - visitorType: VisitorType.ANONYMOUS, - sessionId: 'session-abc', - referrer: 'https://example.com', - userAgent: 'test-agent', - country: 'US', - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - RecordPageViewUseCase, - { - provide: 'IPageViewRepository', - useValue: { - save: jest.fn(), - findById: jest.fn(), - findByEntityId: jest.fn(), - findByDateRange: jest.fn(), - findBySession: jest.fn(), - countByEntityId: jest.fn(), - countUniqueVisitors: jest.fn(), - } as IPageViewRepository, - }, - { - provide: 'ILogger', - useValue: { - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - } as ILogger, - }, - ], - }).compile(); - - useCase = module.get(RecordPageViewUseCase); - pageViewRepository = module.get('IPageViewRepository'); - logger = module.get('ILogger'); - }); - - it('should be defined', () => { - expect(useCase).toBeDefined(); - }); - - it('should record a page view and return its ID', async () => { - const result: RecordPageViewOutput = await useCase.execute(mockRecordPageViewInput); - - expect(result).toHaveProperty('pageViewId'); - expect(pageViewRepository.save).toHaveBeenCalledTimes(1); - const savedPageView: PageView = (pageViewRepository.save as jest.Mock).mock.calls[0][0]; - expect(savedPageView.entityId).toBe(mockRecordPageViewInput.entityId); - expect(savedPageView.entityType).toBe(mockRecordPageViewInput.entityType); - expect(savedPageView.sessionId).toBe(mockRecordPageViewInput.sessionId); - expect(logger.debug).toHaveBeenCalledWith('Executing RecordPageViewUseCase', { input: mockRecordPageViewInput }); - expect(logger.info).toHaveBeenCalledWith('Page view recorded successfully', { pageViewId: result.pageViewId, input: mockRecordPageViewInput }); - }); - - it('should handle errors during page view recording', async () => { - const error = new Error('Repository error'); - (pageViewRepository.save as jest.Mock).mockRejectedValue(error); - - await expect(useCase.execute(mockRecordPageViewInput)).rejects.toThrow(error); - expect(logger.error).toHaveBeenCalledWith('Error recording page view', error, { input: mockRecordPageViewInput }); - }); -}); diff --git a/apps/api/src/application/analytics/record-page-view.use-case.ts b/apps/api/src/application/analytics/record-page-view.use-case.ts deleted file mode 100644 index 0e870856a..000000000 --- a/apps/api/src/application/analytics/record-page-view.use-case.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Injectable, Inject } from '@nestjs/common'; -import type { AsyncUseCase } from '@gridpilot/shared/application'; -import type { ILogger } from '@gridpilot/shared/logging/ILogger'; -import { PageView } from '@gridpilot/analytics/domain/entities/PageView'; -import type { EntityType, VisitorType } from '@gridpilot/analytics/domain/entities/PageView'; // Re-exported there -import type { IPageViewRepository } from '@gridpilot/analytics/domain/repositories/IPageViewRepository'; - -export interface RecordPageViewInput { - entityType: EntityType; - entityId: string; - visitorId?: string; - visitorType: VisitorType; - sessionId: string; - referrer?: string; - userAgent?: string; - country?: string; -} - -export interface RecordPageViewOutput { - pageViewId: string; -} - -@Injectable() -export class RecordPageViewUseCase - implements AsyncUseCase { - constructor( - @Inject('IPageViewRepository') private readonly pageViewRepository: IPageViewRepository, - @Inject('ILogger') private readonly logger: ILogger, - ) {} - - async execute(input: RecordPageViewInput): Promise { - this.logger.debug('Executing RecordPageViewUseCase', { input }); - try { - const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - const baseProps: Omit[0], 'timestamp'> = { - id: pageViewId, - entityType: input.entityType, - entityId: input.entityId, - visitorType: input.visitorType, - sessionId: input.sessionId, - }; - - const pageView = PageView.create({ - ...baseProps, - ...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}), - ...(input.referrer !== undefined ? { referrer: input.referrer } : {}), - ...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}), - ...(input.country !== undefined ? { country: input.country } : {}), - }); - - await this.pageViewRepository.save(pageView); - this.logger.info('Page view recorded successfully', { pageViewId, input }); - return { pageViewId }; - } catch (error) { - this.logger.error('Error recording page view', error, { input }); - throw error; - } - } -} diff --git a/apps/api/src/infrastructure/analytics/in-memory-engagement.repository.ts b/apps/api/src/infrastructure/analytics/in-memory-engagement.repository.ts deleted file mode 100644 index bcccc75dd..000000000 --- a/apps/api/src/infrastructure/analytics/in-memory-engagement.repository.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EngagementEvent, EngagementAction, EngagementEntityType } from '@gridpilot/analytics/domain/entities/EngagementEvent'; -import type { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository'; - -@Injectable() -export class InMemoryEngagementRepository implements IEngagementRepository { - private readonly engagements: Map = new Map(); - - async save(event: EngagementEvent): Promise { - this.engagements.set(event.id, event); - } - - async findById(id: string): Promise { - return this.engagements.get(id) || null; - } - - async findByEntityId( - entityType: EngagementEntityType, - entityId: string, - ): Promise { - return Array.from(this.engagements.values()).filter( - (e) => e.entityType === entityType && e.entityId === entityId, - ); - } - - async findByAction(action: EngagementAction): Promise { - return Array.from(this.engagements.values()).filter( - (e) => e.action === action, - ); - } - - async findByDateRange( - startDate: Date, - endDate: Date, - ): Promise { - return Array.from(this.engagements.values()).filter((e) => { - const eventDate = new Date(e.timestamp); - return eventDate >= startDate && eventDate <= endDate; - }); - } - - async countByAction( - action: EngagementAction, - entityId?: string, - since?: Date, - ): Promise { - let count = 0; - for (const event of this.engagements.values()) { - if (event.action === action) { - if (entityId && event.entityId !== entityId) { - continue; - } - if (since && new Date(event.timestamp) < since) { - continue; - } - count++; - } - } - return count; - } - - async getSponsorClicksForEntity( - entityId: string, - since?: Date, - ): Promise { - let count = 0; - for (const event of this.engagements.values()) { - if (event.action === 'click_sponsor_url' && event.entityId === entityId) { - if (since && new Date(event.timestamp) < since) { - continue; - } - count++; - } - } - return count; - } - - clear(): void { - this.engagements.clear(); - } -} diff --git a/apps/api/src/infrastructure/analytics/in-memory-page-view.repository.ts b/apps/api/src/infrastructure/analytics/in-memory-page-view.repository.ts deleted file mode 100644 index 3d2b9af27..000000000 --- a/apps/api/src/infrastructure/analytics/in-memory-page-view.repository.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PageView, EntityType, VisitorType } from '@gridpilot/analytics/domain/entities/PageView'; -import type { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository'; - -@Injectable() -export class InMemoryPageViewRepository implements IPageViewRepository { - private readonly pageViews: Map = new Map(); - - async save(pageView: PageView): Promise { - this.pageViews.set(pageView.id, pageView); - } - - async findById(id: string): Promise { - return this.pageViews.get(id) || null; - } - - async findByEntityId( - entityType: EntityType, - entityId: string, - ): Promise { - return Array.from(this.pageViews.values()).filter( - (pv) => pv.entityType === entityType && pv.entityId === entityId, - ); - } - - async findByDateRange( - startDate: Date, - endDate: Date, - ): Promise { - return Array.from(this.pageViews.values()).filter((pv) => { - const pageViewDate = new Date(pv.timestamp); - return pageViewDate >= startDate && pageViewDate <= endDate; - }); - } - - async findBySession(sessionId: string): Promise { - return Array.from(this.pageViews.values()).filter( - (pv) => pv.sessionId === sessionId, - ); - } - - async countByEntityId( - entityType: EntityType, - entityId: string, - since?: Date, - ): Promise { - let count = 0; - for (const pageView of this.pageViews.values()) { - if (pageView.entityType === entityType && pageView.entityId === entityId) { - if (since && new Date(pageView.timestamp) < since) { - continue; - } - count++; - } - } - return count; - } - - async countUniqueVisitors( - entityType: EntityType, - entityId: string, - since?: Date, - ): Promise { - const uniqueVisitorIds = new Set(); - for (const pageView of this.pageViews.values()) { - if (pageView.entityType === entityType && pageView.entityId === entityId) { - if (since && new Date(pageView.timestamp) < since) { - continue; - } - if (pageView.visitorId) { - uniqueVisitorIds.add(pageView.visitorId); - } - } - } - return uniqueVisitorIds.size; - } - - clear(): void { - this.pageViews.clear(); - } -} diff --git a/apps/api/src/infrastructure/analytics/mappers/PageViewMapper.spec.ts b/apps/api/src/infrastructure/analytics/mappers/PageViewMapper.spec.ts deleted file mode 100644 index a25bfe08d..000000000 --- a/apps/api/src/infrastructure/analytics/mappers/PageViewMapper.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { PageViewMapper } from './PageViewMapper'; -import { PageView } from '@gridpilot/analytics/domain/entities/PageView'; -import { PageViewEntity } from '../typeorm-page-view.entity'; -import { EntityType, VisitorType } from '@gridpilot/analytics/domain/types/PageView'; - -describe('PageViewMapper', () => { - const now = new Date(); - - const pageViewProps = { - id: 'test-id', - entityType: EntityType.LEAGUE, - entityId: 'entity-id', - visitorType: VisitorType.ANONYMOUS, - sessionId: 'session-id', - timestamp: now, - visitorId: 'visitor-id', - referrer: 'fart.com', - userAgent: 'Mozilla', - country: 'US', - durationMs: 1000, - }; - - const pageViewDomain = PageView.create(pageViewProps); - - it('should correctly map a PageView domain entity to a PageViewEntity persistence entity', () => { - const pageViewEntity = PageViewMapper.toPersistence(pageViewDomain); - - expect(pageViewEntity).toBeInstanceOf(PageViewEntity); - expect(pageViewEntity.id).toEqual(pageViewDomain.id); - expect(pageViewEntity.entityType).toEqual(pageViewDomain.entityType); - expect(pageViewEntity.entityId).toEqual(pageViewDomain.entityId); - expect(pageViewEntity.visitorType).toEqual(pageViewDomain.visitorType); - expect(pageViewEntity.sessionId).toEqual(pageViewDomain.sessionId); - expect(pageViewEntity.timestamp.toISOString()).toEqual(pageViewDomain.timestamp.toISOString()); - expect(pageViewEntity.visitorId).toEqual(pageViewDomain.visitorId); - expect(pageViewEntity.referrer).toEqual(pageViewDomain.referrer); - expect(pageViewEntity.userAgent).toEqual(pageViewDomain.userAgent); - expect(pageViewEntity.country).toEqual(pageViewDomain.country); - expect(pageViewEntity.durationMs).toEqual(pageViewDomain.durationMs); - }); - - it('should correctly map a PageViewEntity persistence entity to a PageView domain entity', () => { - const pageViewEntity = new PageViewEntity(); - pageViewEntity.id = pageViewProps.id; - pageViewEntity.entityType = pageViewProps.entityType as any; // Cast as any because entityType is string in entity - pageViewEntity.entityId = pageViewProps.entityId; - pageViewEntity.visitorType = pageViewProps.visitorType as any; - pageViewEntity.sessionId = pageViewProps.sessionId; - pageViewEntity.timestamp = pageViewProps.timestamp; - pageViewEntity.visitorId = pageViewProps.visitorId; - pageViewEntity.referrer = pageViewProps.referrer; - pageViewEntity.userAgent = pageViewProps.userAgent; - pageViewEntity.country = pageViewProps.country; - pageViewEntity.durationMs = pageViewProps.durationMs; - - const pageView = PageViewMapper.toDomain(pageViewEntity); - - expect(pageView).toBeInstanceOf(PageView); - expect(pageView.id).toEqual(pageViewEntity.id); - expect(pageView.entityType).toEqual(pageViewEntity.entityType); - expect(pageView.entityId).toEqual(pageViewEntity.entityId); - expect(pageView.visitorType).toEqual(pageViewEntity.visitorType); - expect(pageView.sessionId).toEqual(pageViewEntity.sessionId); - expect(pageView.timestamp.toISOString()).toEqual(pageViewEntity.timestamp.toISOString()); - expect(pageView.visitorId).toEqual(pageViewEntity.visitorId); - expect(pageView.referrer).toEqual(pageViewEntity.referrer); - expect(pageView.userAgent).toEqual(pageViewEntity.userAgent); - expect(pageView.country).toEqual(pageViewEntity.country); - expect(pageView.durationMs).toEqual(pageViewEntity.durationMs); - }); - - it('should handle optional properties correctly when mapping to persistence', () => { - const minimalProps = { - id: 'minimal-id', - entityType: EntityType.RACE, - entityId: 'minimal-entity', - visitorType: VisitorType.DRIVER, - sessionId: 'minimal-session', - timestamp: now, - }; - const minimalDomain = PageView.create(minimalProps); - const entity = PageViewMapper.toPersistence(minimalDomain); - - expect(entity.visitorId).toBeUndefined(); - expect(entity.referrer).toBeUndefined(); - expect(entity.userAgent).toBeUndefined(); - expect(entity.country).toBeUndefined(); - expect(entity.durationMs).toBeUndefined(); - }); - - it('should handle optional properties correctly when mapping to domain', () => { - const minimalEntity = new PageViewEntity(); - minimalEntity.id = 'minimal-id-entity'; - minimalEntity.entityType = EntityType.RACE as any; - minimalEntity.entityId = 'minimal-entity-entity'; - minimalEntity.visitorType = VisitorType.DRIVER as any; - minimalEntity.sessionId = 'minimal-session-entity'; - minimalEntity.timestamp = now; - - const domain = PageViewMapper.toDomain(minimalEntity); - - expect(domain.visitorId).toBeUndefined(); - expect(domain.referrer).toBeUndefined(); - expect(domain.userAgent).toBeUndefined(); - expect(domain.country).toBeUndefined(); - expect(domain.durationMs).toBeUndefined(); - }); -}); diff --git a/apps/api/src/infrastructure/analytics/mappers/PageViewMapper.ts b/apps/api/src/infrastructure/analytics/mappers/PageViewMapper.ts deleted file mode 100644 index 0271a4249..000000000 --- a/apps/api/src/infrastructure/analytics/mappers/PageViewMapper.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { PageView } from '../../domain/entities/PageView'; -import { PageViewEntity } from '../../../../apps/api/src/infrastructure/analytics/typeorm-page-view.entity'; -import { EntityType, VisitorType } from '../../domain/types/PageView'; -import { PageViewProps } from '../../domain/types/PageView'; - -export class PageViewMapper { - public static toDomain(entity: PageViewEntity): PageView { - const props: Omit & { timestamp?: Date } = { - id: entity.id, - entityType: entity.entityType as EntityType, - entityId: entity.entityId, - visitorType: entity.visitorType as VisitorType, - sessionId: entity.sessionId, - timestamp: entity.timestamp, - ...(entity.visitorId !== undefined && entity.visitorId !== null ? { visitorId: entity.visitorId } : {}), - ...(entity.referrer !== undefined && entity.referrer !== null ? { referrer: entity.referrer } : {}), - ...(entity.userAgent !== undefined && entity.userAgent !== null ? { userAgent: entity.userAgent } : {}), - ...(entity.country !== undefined && entity.country !== null ? { country: entity.country } : {}), - ...(entity.durationMs !== undefined && entity.durationMs !== null ? { durationMs: entity.durationMs } : {}), - }; - return PageView.create(props); - } - - public static toPersistence(domain: PageView): PageViewEntity { - const entity = new PageViewEntity(); - entity.id = domain.id; - entity.entityType = domain.entityType; - entity.entityId = domain.entityId; - entity.visitorType = domain.visitorType; - entity.sessionId = domain.sessionId; - entity.timestamp = domain.timestamp; - if (domain.visitorId !== undefined) entity.visitorId = domain.visitorId; - if (domain.referrer !== undefined) entity.referrer = domain.referrer; - if (domain.userAgent !== undefined) entity.userAgent = domain.userAgent; - if (domain.country !== undefined) entity.country = domain.country; - if (domain.durationMs !== undefined) entity.durationMs = domain.durationMs; - return entity; - } -} diff --git a/apps/api/src/infrastructure/analytics/typeorm-page-view.entity.ts b/apps/api/src/infrastructure/analytics/typeorm-page-view.entity.ts deleted file mode 100644 index 5267274ed..000000000 --- a/apps/api/src/infrastructure/analytics/typeorm-page-view.entity.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Entity, PrimaryColumn, Column } from 'typeorm'; -@Entity('page_views') -export class PageViewEntity { - @PrimaryColumn({ type: 'uuid' }) - id: string; - - @Column({ type: 'enum', enum: ['league', 'driver', 'team', 'race', 'sponsor'] }) - entityType: string; - - @Column({ type: 'uuid' }) - entityId: string; - - @Column({ type: 'uuid', nullable: true }) - visitorId?: string; - - @Column({ type: 'enum', enum: ['anonymous', 'driver', 'sponsor'] }) - visitorType: string; - - @Column({ type: 'uuid' }) - sessionId: string; - - @Column({ type: 'varchar', length: 2048, nullable: true }) - referrer?: string; - - @Column({ type: 'varchar', length: 512, nullable: true }) - userAgent?: string; - - @Column({ type: 'varchar', length: 2, nullable: true }) - country?: string; - - @Column({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' }) - timestamp: Date; - - @Column({ type: 'integer', nullable: true }) - durationMs?: number; -} diff --git a/apps/api/src/infrastructure/analytics/typeorm-page-view.repository.spec.ts b/apps/api/src/infrastructure/analytics/typeorm-page-view.repository.spec.ts deleted file mode 100644 index 8bfc98766..000000000 --- a/apps/api/src/infrastructure/analytics/typeorm-page-view.repository.spec.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { TypeOrmPageViewRepository } from './typeorm-page-view.repository'; -import { PageViewEntity } from './typeorm-page-view.entity'; -import { PageView } from '@gridpilot/analytics/domain/entities/PageView'; -import { EntityType, VisitorType } from '@gridpilot/analytics/domain/types/PageView'; -import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository'; -import { v4 as uuid } from 'uuid'; - -describe('TypeOrmPageViewRepository (Integration)', () => { - jest.setTimeout(30000); // Increase timeout for integration tests - let repository: IPageViewRepository; - let pageViewTypeOrmRepository: Repository; - let module: TestingModule; - const mockPageViewEntities = new Map(); - - const mockPageViewRepository = { - save: jest.fn(entity => { - mockPageViewEntities.set(entity.id, entity); - return entity; - }), - findOne: jest.fn(({ where: { id } }) => { - return Promise.resolve(mockPageViewEntities.get(id) || null); - }), - find: jest.fn((query: any) => { - const { where, order } = query; - let results = Array.from(mockPageViewEntities.values()); - - if (where) { - if (where.entityId) { - results = results.filter(pv => pv.entityId === where.entityId); - } - if (where.entityType) { - results = results.filter(pv => pv.entityType === where.entityType); - } - if (where.sessionId) { - results = results.filter(pv => pv.sessionId === where.sessionId); - } - // Handle Between operator for timestamp - if (where.timestamp && typeof where.timestamp === 'object') { - // TypeORM's Between operator passes an object like { type: 'between', value: [date1, date2] } - const timestampCondition = where.timestamp; - if (timestampCondition.type === 'between' && timestampCondition.value.length === 2) { - const [startDate, endDate] = timestampCondition.value; - results = results.filter(pv => pv.timestamp >= startDate && pv.timestamp <= endDate); - } else if (timestampCondition.type === 'moreThanOrEqual') { - results = results.filter(pv => pv.timestamp >= timestampCondition.value); - } - } - } - - if (order && order.timestamp) { - results.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); // DESC order - } - return Promise.resolve(results); - }), - count: jest.fn((query: any) => { - const { where } = query; - let results = Array.from(mockPageViewEntities.values()); - if (where) { - if (where.entityId) { - results = results.filter(pv => pv.entityId === where.entityId); - } - if (where.entityType) { - results = results.filter(pv => pv.entityType === where.entityType); - } - if (where.timestamp && where.timestamp.type === 'moreThanOrEqual') { - results = results.filter(pv => pv.timestamp >= where.timestamp.value); - } - } - return Promise.resolve(results.length); - }), - createQueryBuilder: jest.fn(() => { - let queryResult = Array.from(mockPageViewEntities.values()); - let conditions: Array<(pv: PageViewEntity) => boolean> = []; - - const queryBuilder: any = { - select: jest.fn().mockReturnThis(), - where: jest.fn((condition, parameters) => { - if (parameters.entityType) { - conditions.push(pv => pv.entityType === parameters.entityType); - } - if (parameters.entityId) { - conditions.push(pv => pv.entityId === parameters.entityId); - } - if (parameters.since) { - conditions.push(pv => pv.timestamp >= parameters.since); - } - return queryBuilder; - }), - andWhere: jest.fn((condition, parameters) => { - if (parameters.entityId) { - conditions.push(pv => pv.entityId === parameters.entityId); - } - if (parameters.until) { - conditions.push(pv => pv.timestamp <= parameters.until); - } - if (parameters.since) { // For countUniqueVisitors's second andWhere - conditions.push(pv => pv.timestamp >= parameters.since); - } - return queryBuilder; - }), - getRawOne: jest.fn(() => { - const filteredResult = queryResult.filter(pv => conditions.every(cond => cond(pv))); - return Promise.resolve({ count: new Set(filteredResult.map(pv => pv.visitorId)).size }); - }), - getMany: jest.fn(() => { - const filteredResult = queryResult.filter(pv => conditions.every(cond => cond(pv))); - return Promise.resolve(filteredResult); - }), - }; - return queryBuilder; - }), - query: jest.fn(() => Promise.resolve([])), - }; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - TypeOrmPageViewRepository, - { - provide: getRepositoryToken(PageViewEntity), - useValue: mockPageViewRepository, - }, - { provide: 'IPageViewRepository', useClass: TypeOrmPageViewRepository }, - ], - }).compile(); - - repository = module.get('IPageViewRepository'); - pageViewTypeOrmRepository = module.get>( - getRepositoryToken(PageViewEntity), - ); - }); - - beforeEach(async () => { - // Clear the database before each test - mockPageViewEntities.clear(); // Clear the mock in-memory database - jest.clearAllMocks(); - }); - - afterAll(async () => { - // await module.close(); - }); - - it('should be defined', () => { - expect(repository).toBeDefined(); - }); - - it('should save a page view', async () => { - const pageView = PageView.create({ - id: uuid(), - entityType: EntityType.LEAGUE, - entityId: uuid(), - visitorType: VisitorType.ANONYMOUS, - sessionId: uuid(), - timestamp: new Date(), - }); - - await repository.save(pageView); - - const foundPageView = await repository.findById(pageView.id); - expect(foundPageView).toBeDefined(); - expect(foundPageView?.id).toBe(pageView.id); - expect(foundPageView?.entityType).toBe(pageView.entityType); - }); - - it('should find a page view by ID', async () => { - const pageView = PageView.create({ - id: uuid(), - entityType: EntityType.DRIVER, - entityId: uuid(), - visitorType: VisitorType.DRIVER, - sessionId: uuid(), - timestamp: new Date(), - }); - await repository.save(pageView); - - const foundPageView = await repository.findById(pageView.id); - expect(foundPageView).toBeDefined(); - expect(foundPageView?.id).toBe(pageView.id); - }); - - it('should return null if page view not found by ID', async () => { - const foundPageView = await repository.findById(uuid()); - expect(foundPageView).toBeNull(); - }); - - it('should find page views by entity ID', async () => { - const entityId = uuid(); - const pageView1 = PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId, visitorType: VisitorType.ANONYMOUS, sessionId: uuid(), timestamp: new Date() }); - const pageView2 = PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId, visitorType: VisitorType.ANONYMOUS, sessionId: uuid(), timestamp: new Date(Date.now() - 1000) }); - await repository.save(pageView1); - await repository.save(pageView2); - - const foundViews = await repository.findByEntityId(EntityType.LEAGUE, entityId); - expect(foundViews).toHaveLength(2); - expect(foundViews[0]?.id).toEqual(pageView1.id); // Should be ordered by timestamp DESC - }); - - it('should count page views by entity ID', async () => { - const entityId = uuid(); - const pageView1 = PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId, visitorType: VisitorType.ANONYMOUS, sessionId: uuid(), timestamp: new Date() }); - const pageView2 = PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId, visitorType: VisitorType.ANONYMOUS, sessionId: uuid(), timestamp: new Date(Date.now() - 1000 * 60 * 60) }); - await repository.save(pageView1); - await repository.save(pageView2); - - const count = await repository.countByEntityId(EntityType.LEAGUE, entityId); - expect(count).toBe(2); - }); - - it('should count unique visitors by entity ID', async () => { - const entityId = uuid(); - const visitorId1 = uuid(); - const visitorId2 = uuid(); - - // Two views from visitor1 - await repository.save(PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId, visitorId: visitorId1, visitorType: VisitorType.DRIVER, sessionId: uuid(), timestamp: new Date() })); - await repository.save(PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId, visitorId: visitorId1, visitorType: VisitorType.DRIVER, sessionId: uuid(), timestamp: new Date() })); - // One view from visitor2 - await repository.save(PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId, visitorId: visitorId2, visitorType: VisitorType.DRIVER, sessionId: uuid(), timestamp: new Date() })); - - const count = await repository.countUniqueVisitors(EntityType.LEAGUE, entityId); - expect(count).toBe(2); - }); - - it('should return 0 for count unique visitors if no visitors', async () => { - const count = await repository.countUniqueVisitors(EntityType.LEAGUE, uuid()); - expect(count).toBe(0); - }); - - it('should find page views by date range', async () => { - const entityId = uuid(); - const today = new Date(); - const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000); - const twoDaysAgo = new Date(today.getTime() - 2 * 24 * 60 * 60 * 1000); - - const pvToday = PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId, visitorType: VisitorType.ANONYMOUS, sessionId: uuid(), timestamp: today }); - const pvYesterday = PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId, visitorType: VisitorType.ANONYMOUS, sessionId: uuid(), timestamp: yesterday }); - const pvTwoDaysAgo = PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId, visitorType: VisitorType.ANONYMOUS, sessionId: uuid(), timestamp: twoDaysAgo }); - - await repository.save(pvToday); - await repository.save(pvYesterday); - await repository.save(pvTwoDaysAgo); - - const result = await repository.findByDateRange(twoDaysAgo, today); - expect(result.length).toBe(3); - expect(result.map(p => p?.id)).toEqual(expect.arrayContaining([pvToday.id, pvYesterday.id, pvTwoDaysAgo.id])); - - const resultToday = await repository.findByDateRange(today, today); - expect(resultToday.length).toBe(1); - expect(resultToday[0]?.id).toBe(pvToday.id); - }); - - it('should find page views by session ID', async () => { - const sessionId = uuid(); - const entityId1 = uuid(); - const entityId2 = uuid(); - - const pv1 = PageView.create({ id: uuid(), entityType: EntityType.LEAGUE, entityId: entityId1, visitorType: VisitorType.ANONYMOUS, sessionId, timestamp: new Date() }); - const pv2 = PageView.create({ id: uuid(), entityType: EntityType.DRIVER, entityId: entityId2, visitorType: VisitorType.ANONYMOUS, sessionId, timestamp: new Date(Date.now() - 100) }); - const pvOtherSession = PageView.create({ id: uuid(), entityType: EntityType.TEAM, entityId: uuid(), visitorType: VisitorType.ANONYMOUS, sessionId: uuid(), timestamp: new Date() }); - - await repository.save(pv1); - await repository.save(pv2); - await repository.save(pvOtherSession); - - const foundViews = await repository.findBySession(sessionId); - expect(foundViews).toHaveLength(2); - expect(foundViews.map(p => p.id)).toEqual(expect.arrayContaining([pv1.id, pv2.id])); - // Should be ordered by timestamp DESC - expect(foundViews[0]?.id).toBe(pv1.id); - expect(foundViews[1]?.id).toBe(pv2.id); - }); -}); diff --git a/apps/api/src/infrastructure/analytics/typeorm-page-view.repository.ts b/apps/api/src/infrastructure/analytics/typeorm-page-view.repository.ts deleted file mode 100644 index f8e2874b1..000000000 --- a/apps/api/src/infrastructure/analytics/typeorm-page-view.repository.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, MoreThanOrEqual, Between } from 'typeorm'; -import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository'; -import { PageViewEntity } from './typeorm-page-view.entity'; -import { PageViewMapper } from './mappers/PageViewMapper'; -import { PageView } from '@gridpilot/analytics/domain/entities/PageView'; -import { EntityType } from '@gridpilot/analytics/domain/types/PageView'; - -@Injectable() -export class TypeOrmPageViewRepository implements IPageViewRepository { - constructor( - @InjectRepository(PageViewEntity) - private readonly pageViewRepository: Repository, - ) {} - - async save(pageView: PageView): Promise { - const pageViewEntity = PageViewMapper.toPersistence(pageView); - await this.pageViewRepository.save(pageViewEntity); - } - - async findById(id: string): Promise { - const entity = await this.pageViewRepository.findOne({ - where: { id }, - }); - return entity ? PageViewMapper.toDomain(entity) : null; - } - - async findByEntityId(entityType: EntityType, entityId: string): Promise { - const entities = await this.pageViewRepository.find({ - where: { entityType, entityId }, - order: { timestamp: 'DESC' }, - }); - return entities.map(PageViewMapper.toDomain); - } - - async findByDateRange(startDate: Date, endDate: Date): Promise { - const entities = await this.pageViewRepository.find({ - where: { timestamp: Between(startDate, endDate) }, - order: { timestamp: 'DESC' }, - }); - return entities.map(PageViewMapper.toDomain); - } - - async findBySession(sessionId: string): Promise { - const entities = await this.pageViewRepository.find({ - where: { sessionId }, - order: { timestamp: 'DESC' }, - }); - return entities.map(PageViewMapper.toDomain); - } - - async countByEntityId(entityType: EntityType, entityId: string, since?: Date): Promise { - const where: any = { entityType, entityId }; - if (since) { - where.timestamp = MoreThanOrEqual(since); - } - return this.pageViewRepository.count({ where }); - } - - async countUniqueVisitors(entityType: EntityType, entityId: string, since?: Date): Promise { - const query = this.pageViewRepository.createQueryBuilder('page_view') - .select('COUNT(DISTINCT "visitorId")', 'count') - .where('page_view.entityType = :entityType', { entityType }) - .andWhere('page_view.entityId = :entityId', { entityId }); - - if (since) { - query.andWhere('page_view.timestamp >= :since', { since }); - } - - const result = await query.getRawOne(); - return parseInt(result.count, 10) || 0; - } -} diff --git a/testing/index.ts b/core/testing-support/index.ts similarity index 100% rename from testing/index.ts rename to core/testing-support/index.ts diff --git a/testing/package.json b/core/testing-support/package.json similarity index 100% rename from testing/package.json rename to core/testing-support/package.json diff --git a/testing/src/faker/faker.ts b/core/testing-support/src/faker/faker.ts similarity index 100% rename from testing/src/faker/faker.ts rename to core/testing-support/src/faker/faker.ts diff --git a/testing/src/images/images.ts b/core/testing-support/src/images/images.ts similarity index 100% rename from testing/src/images/images.ts rename to core/testing-support/src/images/images.ts diff --git a/testing/src/media/DemoAvatarGenerationAdapter.ts b/core/testing-support/src/media/DemoAvatarGenerationAdapter.ts similarity index 100% rename from testing/src/media/DemoAvatarGenerationAdapter.ts rename to core/testing-support/src/media/DemoAvatarGenerationAdapter.ts diff --git a/testing/src/media/DemoFaceValidationAdapter.ts b/core/testing-support/src/media/DemoFaceValidationAdapter.ts similarity index 100% rename from testing/src/media/DemoFaceValidationAdapter.ts rename to core/testing-support/src/media/DemoFaceValidationAdapter.ts diff --git a/testing/src/media/DemoImageServiceAdapter.ts b/core/testing-support/src/media/DemoImageServiceAdapter.ts similarity index 100% rename from testing/src/media/DemoImageServiceAdapter.ts rename to core/testing-support/src/media/DemoImageServiceAdapter.ts diff --git a/testing/src/media/InMemoryAvatarGenerationRepository.ts b/core/testing-support/src/media/InMemoryAvatarGenerationRepository.ts similarity index 100% rename from testing/src/media/InMemoryAvatarGenerationRepository.ts rename to core/testing-support/src/media/InMemoryAvatarGenerationRepository.ts diff --git a/testing/src/racing/DemoCars.ts b/core/testing-support/src/racing/DemoCars.ts similarity index 100% rename from testing/src/racing/DemoCars.ts rename to core/testing-support/src/racing/DemoCars.ts diff --git a/testing/src/racing/DemoDriverStats.ts b/core/testing-support/src/racing/DemoDriverStats.ts similarity index 100% rename from testing/src/racing/DemoDriverStats.ts rename to core/testing-support/src/racing/DemoDriverStats.ts diff --git a/testing/src/racing/DemoTracks.ts b/core/testing-support/src/racing/DemoTracks.ts similarity index 100% rename from testing/src/racing/DemoTracks.ts rename to core/testing-support/src/racing/DemoTracks.ts diff --git a/testing/src/racing/RacingFeedSeed.ts b/core/testing-support/src/racing/RacingFeedSeed.ts similarity index 100% rename from testing/src/racing/RacingFeedSeed.ts rename to core/testing-support/src/racing/RacingFeedSeed.ts diff --git a/testing/src/racing/RacingSeedCore.ts b/core/testing-support/src/racing/RacingSeedCore.ts similarity index 100% rename from testing/src/racing/RacingSeedCore.ts rename to core/testing-support/src/racing/RacingSeedCore.ts diff --git a/testing/src/racing/RacingSponsorshipSeed.ts b/core/testing-support/src/racing/RacingSponsorshipSeed.ts similarity index 100% rename from testing/src/racing/RacingSponsorshipSeed.ts rename to core/testing-support/src/racing/RacingSponsorshipSeed.ts diff --git a/testing/src/racing/RacingStaticSeed.ts b/core/testing-support/src/racing/RacingStaticSeed.ts similarity index 100% rename from testing/src/racing/RacingStaticSeed.ts rename to core/testing-support/src/racing/RacingStaticSeed.ts diff --git a/testing/tsconfig.json b/core/testing-support/tsconfig.json similarity index 100% rename from testing/tsconfig.json rename to core/testing-support/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 6abefffef..348ccf33e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.1.0", "workspaces": [ "core/*", - "apps/*" + "apps/*", + "testing/*" ], "dependencies": { "@gridpilot/social": "file:core/social",