presenter refactoring
This commit is contained in:
@@ -42,8 +42,8 @@ describe('AnalyticsController', () => {
|
||||
userAgent: 'Mozilla/5.0',
|
||||
country: 'US',
|
||||
};
|
||||
const output = { pageViewId: 'pv-123' };
|
||||
service.recordPageView.mockResolvedValue(output);
|
||||
const presenterMock = { viewModel: { pageViewId: 'pv-123' } };
|
||||
service.recordPageView.mockResolvedValue(presenterMock as any);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
@@ -54,7 +54,7 @@ describe('AnalyticsController', () => {
|
||||
|
||||
expect(service.recordPageView).toHaveBeenCalledWith(input);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(output);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(presenterMock.viewModel);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,8 +69,8 @@ describe('AnalyticsController', () => {
|
||||
actorId: 'actor-789',
|
||||
metadata: { key: 'value' },
|
||||
};
|
||||
const output = { eventId: 'event-123', engagementWeight: 10 };
|
||||
service.recordEngagement.mockResolvedValue(output);
|
||||
const presenterMock = { eventId: 'event-123', engagementWeight: 10 };
|
||||
service.recordEngagement.mockResolvedValue(presenterMock as any);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
@@ -81,41 +81,45 @@ describe('AnalyticsController', () => {
|
||||
|
||||
expect(service.recordEngagement).toHaveBeenCalledWith(input);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(output);
|
||||
expect(mockRes.json).toHaveBeenCalledWith((presenterMock as any).viewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDashboardData', () => {
|
||||
it('should return dashboard data', async () => {
|
||||
const output = {
|
||||
totalUsers: 100,
|
||||
activeUsers: 50,
|
||||
totalRaces: 20,
|
||||
totalLeagues: 5,
|
||||
const presenterMock = {
|
||||
viewModel: {
|
||||
totalUsers: 100,
|
||||
activeUsers: 50,
|
||||
totalRaces: 20,
|
||||
totalLeagues: 5,
|
||||
},
|
||||
};
|
||||
service.getDashboardData.mockResolvedValue(output);
|
||||
service.getDashboardData.mockResolvedValue(presenterMock as any);
|
||||
|
||||
const result = await controller.getDashboardData();
|
||||
|
||||
expect(service.getDashboardData).toHaveBeenCalled();
|
||||
expect(result).toEqual(output);
|
||||
expect(result).toEqual(presenterMock.viewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAnalyticsMetrics', () => {
|
||||
it('should return analytics metrics', async () => {
|
||||
const output = {
|
||||
pageViews: 1000,
|
||||
uniqueVisitors: 500,
|
||||
averageSessionDuration: 300,
|
||||
bounceRate: 0.4,
|
||||
const presenterMock = {
|
||||
viewModel: {
|
||||
pageViews: 1000,
|
||||
uniqueVisitors: 500,
|
||||
averageSessionDuration: 300,
|
||||
bounceRate: 0.4,
|
||||
},
|
||||
};
|
||||
service.getAnalyticsMetrics.mockResolvedValue(output);
|
||||
service.getAnalyticsMetrics.mockResolvedValue(presenterMock as any);
|
||||
|
||||
const result = await controller.getAnalyticsMetrics();
|
||||
|
||||
expect(service.getAnalyticsMetrics).toHaveBeenCalled();
|
||||
expect(result).toEqual(output);
|
||||
expect(result).toEqual(presenterMock.viewModel);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,11 +10,7 @@ import { GetAnalyticsMetricsOutputDTO } from './dtos/GetAnalyticsMetricsOutputDT
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
|
||||
type RecordPageViewInput = RecordPageViewInputDTO;
|
||||
type RecordPageViewOutput = RecordPageViewOutputDTO;
|
||||
type RecordEngagementInput = RecordEngagementInputDTO;
|
||||
type RecordEngagementOutput = RecordEngagementOutputDTO;
|
||||
type GetDashboardDataOutput = GetDashboardDataOutputDTO;
|
||||
type GetAnalyticsMetricsOutput = GetAnalyticsMetricsOutputDTO;
|
||||
|
||||
@ApiTags('analytics')
|
||||
@Controller('analytics')
|
||||
@@ -31,8 +27,8 @@ export class AnalyticsController {
|
||||
@Body() input: RecordPageViewInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordPageViewOutput = await this.analyticsService.recordPageView(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
const presenter = await this.analyticsService.recordPageView(input);
|
||||
res.status(HttpStatus.CREATED).json(presenter.viewModel);
|
||||
}
|
||||
|
||||
@Post('engagement')
|
||||
@@ -43,21 +39,23 @@ export class AnalyticsController {
|
||||
@Body() input: RecordEngagementInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordEngagementOutput = await this.analyticsService.recordEngagement(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
const presenter = await this.analyticsService.recordEngagement(input);
|
||||
res.status(HttpStatus.CREATED).json(presenter.viewModel);
|
||||
}
|
||||
|
||||
@Get('dashboard')
|
||||
@ApiOperation({ summary: 'Get analytics dashboard data' })
|
||||
@ApiResponse({ status: 200, description: 'Dashboard data', type: GetDashboardDataOutputDTO })
|
||||
async getDashboardData(): Promise<GetDashboardDataOutput> {
|
||||
return await this.analyticsService.getDashboardData();
|
||||
async getDashboardData(): Promise<GetDashboardDataOutputDTO> {
|
||||
const presenter = await this.analyticsService.getDashboardData();
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Get('metrics')
|
||||
@ApiOperation({ summary: 'Get analytics metrics' })
|
||||
@ApiResponse({ status: 200, description: 'Analytics metrics', type: GetAnalyticsMetricsOutputDTO })
|
||||
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutput> {
|
||||
return await this.analyticsService.getAnalyticsMetrics();
|
||||
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutputDTO> {
|
||||
const presenter = await this.analyticsService.getAnalyticsMetrics();
|
||||
return presenter.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/Rec
|
||||
import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import { GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
|
||||
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
|
||||
import { RecordPageViewPresenter } from './presenters/RecordPageViewPresenter';
|
||||
import { RecordEngagementPresenter } from './presenters/RecordEngagementPresenter';
|
||||
import { GetDashboardDataPresenter } from './presenters/GetDashboardDataPresenter';
|
||||
import { GetAnalyticsMetricsPresenter } from './presenters/GetAnalyticsMetricsPresenter';
|
||||
|
||||
type RecordPageViewInput = RecordPageViewInputDTO;
|
||||
type RecordPageViewOutput = RecordPageViewOutputDTO;
|
||||
type RecordEngagementInput = RecordEngagementInputDTO;
|
||||
type RecordEngagementOutput = RecordEngagementOutputDTO;
|
||||
type GetDashboardDataOutput = GetDashboardDataOutputDTO;
|
||||
type GetAnalyticsMetricsOutput = GetAnalyticsMetricsOutputDTO;
|
||||
|
||||
const RECORD_PAGE_VIEW_USE_CASE_TOKEN = 'RecordPageViewUseCase_TOKEN';
|
||||
const RECORD_ENGAGEMENT_USE_CASE_TOKEN = 'RecordEngagementUseCase_TOKEN';
|
||||
@@ -31,19 +31,27 @@ export class AnalyticsService {
|
||||
@Inject(GET_ANALYTICS_METRICS_USE_CASE_TOKEN) private readonly getAnalyticsMetricsUseCase: GetAnalyticsMetricsUseCase,
|
||||
) {}
|
||||
|
||||
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
|
||||
return await this.recordPageViewUseCase.execute(input);
|
||||
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewPresenter> {
|
||||
const presenter = new RecordPageViewPresenter();
|
||||
await this.recordPageViewUseCase.execute(input, presenter);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
|
||||
return await this.recordEngagementUseCase.execute(input);
|
||||
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementPresenter> {
|
||||
const presenter = new RecordEngagementPresenter();
|
||||
await this.recordEngagementUseCase.execute(input, presenter);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getDashboardData(): Promise<GetDashboardDataOutput> {
|
||||
return await this.getDashboardDataUseCase.execute();
|
||||
async getDashboardData(): Promise<GetDashboardDataPresenter> {
|
||||
const presenter = new GetDashboardDataPresenter();
|
||||
await this.getDashboardDataUseCase.execute(undefined, presenter);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutput> {
|
||||
return await this.getAnalyticsMetricsUseCase.execute();
|
||||
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsPresenter> {
|
||||
const presenter = new GetAnalyticsMetricsPresenter();
|
||||
await this.getAnalyticsMetricsUseCase.execute(undefined, presenter);
|
||||
return presenter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { GetAnalyticsMetricsPresenter } from './GetAnalyticsMetricsPresenter';
|
||||
import type { GetAnalyticsMetricsOutput } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
|
||||
|
||||
describe('GetAnalyticsMetricsPresenter', () => {
|
||||
let presenter: GetAnalyticsMetricsPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new GetAnalyticsMetricsPresenter();
|
||||
});
|
||||
|
||||
it('maps use case output to DTO correctly', () => {
|
||||
const output: GetAnalyticsMetricsOutput = {
|
||||
pageViews: 1000,
|
||||
uniqueVisitors: 500,
|
||||
averageSessionDuration: 300,
|
||||
bounceRate: 0.4,
|
||||
};
|
||||
|
||||
presenter.present(output);
|
||||
|
||||
expect(presenter.viewModel).toEqual({
|
||||
pageViews: 1000,
|
||||
uniqueVisitors: 500,
|
||||
averageSessionDuration: 300,
|
||||
bounceRate: 0.4,
|
||||
});
|
||||
});
|
||||
|
||||
it('reset clears state and causes viewModel to throw', () => {
|
||||
const output: GetAnalyticsMetricsOutput = {
|
||||
pageViews: 1000,
|
||||
uniqueVisitors: 500,
|
||||
averageSessionDuration: 300,
|
||||
bounceRate: 0.4,
|
||||
};
|
||||
|
||||
presenter.present(output);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { GetAnalyticsMetricsOutput } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
|
||||
import type { GetAnalyticsMetricsOutputDTO } from '../dtos/GetAnalyticsMetricsOutputDTO';
|
||||
|
||||
export class GetAnalyticsMetricsPresenter {
|
||||
private result: GetAnalyticsMetricsOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(output: GetAnalyticsMetricsOutput): void {
|
||||
this.result = {
|
||||
pageViews: output.pageViews,
|
||||
uniqueVisitors: output.uniqueVisitors,
|
||||
averageSessionDuration: output.averageSessionDuration,
|
||||
bounceRate: output.bounceRate,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): GetAnalyticsMetricsOutputDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { GetDashboardDataPresenter } from './GetDashboardDataPresenter';
|
||||
import type { GetDashboardDataOutput } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
|
||||
|
||||
describe('GetDashboardDataPresenter', () => {
|
||||
let presenter: GetDashboardDataPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new GetDashboardDataPresenter();
|
||||
});
|
||||
|
||||
it('maps use case output to DTO correctly', () => {
|
||||
const output: GetDashboardDataOutput = {
|
||||
totalUsers: 100,
|
||||
activeUsers: 50,
|
||||
totalRaces: 20,
|
||||
totalLeagues: 5,
|
||||
};
|
||||
|
||||
presenter.present(output);
|
||||
|
||||
expect(presenter.viewModel).toEqual({
|
||||
totalUsers: 100,
|
||||
activeUsers: 50,
|
||||
totalRaces: 20,
|
||||
totalLeagues: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('reset clears state and causes viewModel to throw', () => {
|
||||
const output: GetDashboardDataOutput = {
|
||||
totalUsers: 100,
|
||||
activeUsers: 50,
|
||||
totalRaces: 20,
|
||||
totalLeagues: 5,
|
||||
};
|
||||
|
||||
presenter.present(output);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { GetDashboardDataOutput } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
|
||||
import type { GetDashboardDataOutputDTO } from '../dtos/GetDashboardDataOutputDTO';
|
||||
|
||||
export class GetDashboardDataPresenter {
|
||||
private result: GetDashboardDataOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(output: GetDashboardDataOutput): void {
|
||||
this.result = {
|
||||
totalUsers: output.totalUsers,
|
||||
activeUsers: output.activeUsers,
|
||||
totalRaces: output.totalRaces,
|
||||
totalLeagues: output.totalLeagues,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): GetDashboardDataOutputDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { RecordEngagementPresenter } from './RecordEngagementPresenter';
|
||||
import type { RecordEngagementOutput } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
|
||||
describe('RecordEngagementPresenter', () => {
|
||||
let presenter: RecordEngagementPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new RecordEngagementPresenter();
|
||||
});
|
||||
|
||||
it('maps use case output to DTO correctly', () => {
|
||||
const output: RecordEngagementOutput = {
|
||||
eventId: 'event-123',
|
||||
engagementWeight: 10,
|
||||
} as RecordEngagementOutput;
|
||||
|
||||
presenter.present(output);
|
||||
|
||||
expect(presenter.viewModel).toEqual({
|
||||
eventId: 'event-123',
|
||||
engagementWeight: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('reset clears state and causes viewModel to throw', () => {
|
||||
const output: RecordEngagementOutput = {
|
||||
eventId: 'event-123',
|
||||
engagementWeight: 10,
|
||||
} as RecordEngagementOutput;
|
||||
|
||||
presenter.present(output);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { RecordEngagementOutput } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import type { RecordEngagementOutputDTO } from '../dtos/RecordEngagementOutputDTO';
|
||||
|
||||
export class RecordEngagementPresenter {
|
||||
private result: RecordEngagementOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(output: RecordEngagementOutput): void {
|
||||
this.result = {
|
||||
eventId: output.eventId,
|
||||
engagementWeight: output.engagementWeight,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): RecordEngagementOutputDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { RecordPageViewPresenter } from './RecordPageViewPresenter';
|
||||
import type { RecordPageViewOutput } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
|
||||
describe('RecordPageViewPresenter', () => {
|
||||
let presenter: RecordPageViewPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new RecordPageViewPresenter();
|
||||
});
|
||||
|
||||
it('maps use case output to DTO correctly', () => {
|
||||
const output: RecordPageViewOutput = {
|
||||
pageViewId: 'pv-123',
|
||||
} as RecordPageViewOutput;
|
||||
|
||||
presenter.present(output);
|
||||
|
||||
expect(presenter.viewModel).toEqual({
|
||||
pageViewId: 'pv-123',
|
||||
});
|
||||
});
|
||||
|
||||
it('reset clears state and causes viewModel to throw', () => {
|
||||
const output: RecordPageViewOutput = {
|
||||
pageViewId: 'pv-123',
|
||||
} as RecordPageViewOutput;
|
||||
|
||||
presenter.present(output);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { RecordPageViewOutput } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import type { RecordPageViewOutputDTO } from '../dtos/RecordPageViewOutputDTO';
|
||||
|
||||
export class RecordPageViewPresenter {
|
||||
private result: RecordPageViewOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(output: RecordPageViewOutput): void {
|
||||
this.result = {
|
||||
pageViewId: output.pageViewId,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): RecordPageViewOutputDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user