refactor use cases

This commit is contained in:
2026-01-08 15:34:51 +01:00
parent d984ab24a8
commit 52e9a2f6a7
362 changed files with 5192 additions and 8409 deletions

View File

@@ -1,9 +1,6 @@
import type { GetAnalyticsMetricsOutput } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import type { RecordEngagementOutput } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import type { RecordPageViewOutput } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { Logger } from '@core/shared/application';
import { Provider } from '@nestjs/common';
import {
@@ -13,13 +10,8 @@ import {
const LOGGER_TOKEN = 'Logger';
const RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN = 'RecordPageViewOutputPort_TOKEN';
const RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN = 'RecordEngagementOutputPort_TOKEN';
const GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN = 'GetDashboardDataOutputPort_TOKEN';
const GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN = 'GetAnalyticsMetricsOutputPort_TOKEN';
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import { GetDashboardDataOutput, GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import { GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import { AnalyticsService } from './AnalyticsService';
@@ -34,44 +26,28 @@ export const AnalyticsProviders: Provider[] = [
RecordEngagementPresenter,
GetDashboardDataPresenter,
GetAnalyticsMetricsPresenter,
{
provide: RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN,
useExisting: RecordPageViewPresenter,
},
{
provide: RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN,
useExisting: RecordEngagementPresenter,
},
{
provide: GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN,
useExisting: GetDashboardDataPresenter,
},
{
provide: GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN,
useExisting: GetAnalyticsMetricsPresenter,
},
{
provide: RecordPageViewUseCase,
useFactory: (repo: IPageViewRepository, logger: Logger, output: UseCaseOutputPort<RecordPageViewOutput>) =>
new RecordPageViewUseCase(repo, logger, output),
inject: [ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN, LOGGER_TOKEN, RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN],
useFactory: (repo: IPageViewRepository, logger: Logger) =>
new RecordPageViewUseCase(repo, logger),
inject: [ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: RecordEngagementUseCase,
useFactory: (repo: IEngagementRepository, logger: Logger, output: UseCaseOutputPort<RecordEngagementOutput>) =>
new RecordEngagementUseCase(repo, logger, output),
inject: [ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN, LOGGER_TOKEN, RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN],
useFactory: (repo: IEngagementRepository, logger: Logger) =>
new RecordEngagementUseCase(repo, logger),
inject: [ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: GetDashboardDataUseCase,
useFactory: (logger: Logger, output: UseCaseOutputPort<GetDashboardDataOutput>) =>
new GetDashboardDataUseCase(logger, output),
inject: [LOGGER_TOKEN, GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN],
useFactory: (logger: Logger) =>
new GetDashboardDataUseCase(logger),
inject: [LOGGER_TOKEN],
},
{
provide: GetAnalyticsMetricsUseCase,
useFactory: (logger: Logger, output: UseCaseOutputPort<GetAnalyticsMetricsOutput>, repo: IPageViewRepository) =>
new GetAnalyticsMetricsUseCase(logger, output, repo),
inject: [LOGGER_TOKEN, GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN, ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN],
useFactory: (logger: Logger, repo: IPageViewRepository) =>
new GetAnalyticsMetricsUseCase(logger, repo),
inject: [LOGGER_TOKEN, ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN],
},
];

View File

@@ -12,8 +12,7 @@ describe('AnalyticsService', () => {
const recordPageViewUseCase = {
execute: vi.fn(async () => {
recordPageViewPresenter.present({ pageViewId: 'pv-1' });
return Result.ok(undefined);
return Result.ok({ pageViewId: 'pv-1' });
}),
};
@@ -77,8 +76,7 @@ describe('AnalyticsService', () => {
const recordEngagementPresenter = new RecordEngagementPresenter();
const recordEngagementUseCase = {
execute: vi.fn(async () => {
recordEngagementPresenter.present({ eventId: 'e1', engagementWeight: 7 });
return Result.ok(undefined);
return Result.ok({ eventId: 'e1', engagementWeight: 7 });
}),
};
@@ -154,13 +152,12 @@ describe('AnalyticsService', () => {
const getDashboardDataPresenter = new GetDashboardDataPresenter();
const getDashboardDataUseCase = {
execute: vi.fn(async () => {
getDashboardDataPresenter.present({
return Result.ok({
totalUsers: 1,
activeUsers: 2,
totalRaces: 3,
totalLeagues: 4,
});
return Result.ok(undefined);
}),
};
@@ -217,13 +214,12 @@ describe('AnalyticsService', () => {
const getAnalyticsMetricsPresenter = new GetAnalyticsMetricsPresenter();
const getAnalyticsMetricsUseCase = {
execute: vi.fn(async () => {
getAnalyticsMetricsPresenter.present({
return Result.ok({
pageViews: 10,
uniqueVisitors: 0,
averageSessionDuration: 0,
bounceRate: 0,
});
return Result.ok(undefined);
}),
};
@@ -275,4 +271,4 @@ describe('AnalyticsService', () => {
await expect(service.getAnalyticsMetrics()).rejects.toThrow('Failed to get analytics metrics');
});
});
});

View File

@@ -31,50 +31,42 @@ export class AnalyticsService {
) {}
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutputDTO> {
this.recordPageViewPresenter.reset();
const result = await this.recordPageViewUseCase.execute(input);
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to record page view');
}
return this.recordPageViewPresenter.responseModel;
return this.recordPageViewPresenter.transform(result.unwrap());
}
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutputDTO> {
this.recordEngagementPresenter.reset();
const result = await this.recordEngagementUseCase.execute(input);
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to record engagement');
}
return this.recordEngagementPresenter.responseModel;
return this.recordEngagementPresenter.transform(result.unwrap());
}
async getDashboardData(): Promise<GetDashboardDataOutputDTO> {
this.getDashboardDataPresenter.reset();
const result = await this.getDashboardDataUseCase.execute();
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get dashboard data');
}
return this.getDashboardDataPresenter.responseModel;
return this.getDashboardDataPresenter.transform(result.unwrap());
}
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutputDTO> {
this.getAnalyticsMetricsPresenter.reset();
const result = await this.getAnalyticsMetricsUseCase.execute({});
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get analytics metrics');
}
return this.getAnalyticsMetricsPresenter.responseModel;
return this.getAnalyticsMetricsPresenter.transform(result.unwrap());
}
}
}

View File

@@ -17,7 +17,7 @@ describe('GetAnalyticsMetricsPresenter', () => {
bounceRate: 0.4,
};
presenter.present(output);
presenter.transform(output);
const dto = presenter.getResponseModel();
@@ -35,11 +35,11 @@ describe('GetAnalyticsMetricsPresenter', () => {
});
});
it('getResponseModel throws if not presented', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
it('getResponseModel throws if not transformed', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not transformed');
});
it('responseModel throws if not presented', () => {
expect(() => presenter.responseModel).toThrow('Presenter not presented');
it('responseModel throws if not transformed', () => {
expect(() => presenter.responseModel).toThrow('Presenter not transformed');
});
});

View File

@@ -1,30 +1,30 @@
import type { GetAnalyticsMetricsOutput } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetAnalyticsMetricsOutputDTO } from '../dtos/GetAnalyticsMetricsOutputDTO';
export class GetAnalyticsMetricsPresenter implements UseCaseOutputPort<GetAnalyticsMetricsOutput> {
export class GetAnalyticsMetricsPresenter {
private model: GetAnalyticsMetricsOutputDTO | null = null;
reset(): void {
this.model = null;
}
present(result: GetAnalyticsMetricsOutput): void {
transform(result: GetAnalyticsMetricsOutput): GetAnalyticsMetricsOutputDTO {
this.model = {
pageViews: result.pageViews,
uniqueVisitors: result.uniqueVisitors,
averageSessionDuration: result.averageSessionDuration,
bounceRate: result.bounceRate,
};
return this.model;
}
get responseModel(): GetAnalyticsMetricsOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
if (!this.model) throw new Error('Presenter not transformed');
return this.model;
}
getResponseModel(): GetAnalyticsMetricsOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
if (!this.model) throw new Error('Presenter not transformed');
return this.model;
}
}
}

View File

@@ -17,7 +17,7 @@ describe('GetDashboardDataPresenter', () => {
totalLeagues: 5,
};
presenter.present(output);
presenter.transform(output);
expect(presenter.getResponseModel()).toEqual({
totalUsers: 100,
@@ -33,11 +33,11 @@ describe('GetDashboardDataPresenter', () => {
});
});
it('getResponseModel throws if not presented', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
it('getResponseModel throws if not transformed', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not transformed');
});
it('responseModel throws if not presented', () => {
expect(() => presenter.responseModel).toThrow('Presenter not presented');
it('responseModel throws if not transformed', () => {
expect(() => presenter.responseModel).toThrow('Presenter not transformed');
});
});

View File

@@ -1,30 +1,30 @@
import type { GetDashboardDataOutput } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetDashboardDataOutputDTO } from '../dtos/GetDashboardDataOutputDTO';
export class GetDashboardDataPresenter implements UseCaseOutputPort<GetDashboardDataOutput> {
export class GetDashboardDataPresenter {
private model: GetDashboardDataOutputDTO | null = null;
reset(): void {
this.model = null;
}
present(result: GetDashboardDataOutput): void {
transform(result: GetDashboardDataOutput): GetDashboardDataOutputDTO {
this.model = {
totalUsers: result.totalUsers,
activeUsers: result.activeUsers,
totalRaces: result.totalRaces,
totalLeagues: result.totalLeagues,
};
return this.model;
}
get responseModel(): GetDashboardDataOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
if (!this.model) throw new Error('Presenter not transformed');
return this.model;
}
getResponseModel(): GetDashboardDataOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
if (!this.model) throw new Error('Presenter not transformed');
return this.model;
}
}
}

View File

@@ -15,23 +15,15 @@ describe('RecordEngagementPresenter', () => {
engagementWeight: 10,
};
presenter.present(output);
presenter.transform(output);
expect(presenter.getResponseModel()).toEqual({
eventId: 'event-123',
engagementWeight: 10,
});
expect(presenter.responseModel).toEqual({
eventId: 'event-123',
engagementWeight: 10,
});
});
it('getResponseModel throws if not presented', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
it('responseModel throws if not presented', () => {
expect(() => presenter.responseModel).toThrow('Presenter not presented');
it('responseModel throws if not transformed', () => {
expect(() => presenter.responseModel).toThrow('Presenter not transformed');
});
});

View File

@@ -1,28 +1,19 @@
import type { RecordEngagementOutput } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { RecordEngagementOutputDTO } from '../dtos/RecordEngagementOutputDTO';
export class RecordEngagementPresenter implements UseCaseOutputPort<RecordEngagementOutput> {
export class RecordEngagementPresenter {
private model: RecordEngagementOutputDTO | null = null;
reset(): void {
this.model = null;
}
present(result: RecordEngagementOutput): void {
transform(output: RecordEngagementOutput): RecordEngagementOutputDTO {
this.model = {
eventId: result.eventId,
engagementWeight: result.engagementWeight,
eventId: output.eventId,
engagementWeight: output.engagementWeight,
};
return this.model;
}
get responseModel(): RecordEngagementOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
if (!this.model) throw new Error('Presenter not transformed');
return this.model;
}
getResponseModel(): RecordEngagementOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
return this.model;
}
}
}

View File

@@ -14,21 +14,14 @@ describe('RecordPageViewPresenter', () => {
pageViewId: 'pv-123',
};
presenter.present(output);
presenter.transform(output);
expect(presenter.getResponseModel()).toEqual({
pageViewId: 'pv-123',
});
expect(presenter.responseModel).toEqual({
pageViewId: 'pv-123',
});
});
it('getResponseModel throws if not presented', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
it('responseModel throws if not presented', () => {
expect(() => presenter.responseModel).toThrow('Presenter not presented');
it('responseModel throws if not transformed', () => {
expect(() => presenter.responseModel).toThrow('Presenter not transformed');
});
});

View File

@@ -1,27 +1,18 @@
import type { RecordPageViewOutput } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { RecordPageViewOutputDTO } from '../dtos/RecordPageViewOutputDTO';
export class RecordPageViewPresenter implements UseCaseOutputPort<RecordPageViewOutput> {
export class RecordPageViewPresenter {
private model: RecordPageViewOutputDTO | null = null;
reset(): void {
this.model = null;
}
present(result: RecordPageViewOutput): void {
transform(output: RecordPageViewOutput): RecordPageViewOutputDTO {
this.model = {
pageViewId: result.pageViewId,
pageViewId: output.pageViewId,
};
return this.model;
}
get responseModel(): RecordPageViewOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
if (!this.model) throw new Error('Presenter not transformed');
return this.model;
}
getResponseModel(): RecordPageViewOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
return this.model;
}
}
}