This commit is contained in:
2025-12-21 22:35:38 +01:00
parent 3c64f328e2
commit 9bd2e630e6
38 changed files with 736 additions and 684 deletions

View File

@@ -8,7 +8,7 @@ import { IAvatarRepository } from '@core/media/domain/repositories/IAvatarReposi
import { FaceValidationPort } from '@core/media/application/ports/FaceValidationPort';
import { AvatarGenerationPort } from '@core/media/application/ports/AvatarGenerationPort';
import { MediaStoragePort } from '@core/media/application/ports/MediaStoragePort';
import type { Logger } from '@core/shared/application';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
// Import use cases
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
@@ -18,6 +18,22 @@ import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMedi
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
// Import result types
import type { RequestAvatarGenerationResult } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
import type { UploadMediaResult } from '@core/media/application/use-cases/UploadMediaUseCase';
import type { GetMediaResult } from '@core/media/application/use-cases/GetMediaUseCase';
import type { DeleteMediaResult } from '@core/media/application/use-cases/DeleteMediaUseCase';
import type { GetAvatarResult } from '@core/media/application/use-cases/GetAvatarUseCase';
import type { UpdateAvatarResult } from '@core/media/application/use-cases/UpdateAvatarUseCase';
// Import presenters
import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter';
import { UploadMediaPresenter } from './presenters/UploadMediaPresenter';
import { GetMediaPresenter } from './presenters/GetMediaPresenter';
import { DeleteMediaPresenter } from './presenters/DeleteMediaPresenter';
import { GetAvatarPresenter } from './presenters/GetAvatarPresenter';
import { UpdateAvatarPresenter } from './presenters/UpdateAvatarPresenter';
// Define injection tokens
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository';
@@ -35,6 +51,14 @@ export const DELETE_MEDIA_USE_CASE_TOKEN = 'DeleteMediaUseCase';
export const GET_AVATAR_USE_CASE_TOKEN = 'GetAvatarUseCase';
export const UPDATE_AVATAR_USE_CASE_TOKEN = 'UpdateAvatarUseCase';
// Output port tokens
export const REQUEST_AVATAR_GENERATION_OUTPUT_PORT_TOKEN = 'RequestAvatarGenerationOutputPort';
export const UPLOAD_MEDIA_OUTPUT_PORT_TOKEN = 'UploadMediaOutputPort';
export const GET_MEDIA_OUTPUT_PORT_TOKEN = 'GetMediaOutputPort';
export const DELETE_MEDIA_OUTPUT_PORT_TOKEN = 'DeleteMediaOutputPort';
export const GET_AVATAR_OUTPUT_PORT_TOKEN = 'GetAvatarOutputPort';
export const UPDATE_AVATAR_OUTPUT_PORT_TOKEN = 'UpdateAvatarOutputPort';
import type { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest';
import type { Media } from '@core/media/domain/entities/Media';
import type { Avatar } from '@core/media/domain/entities/Avatar';
@@ -110,6 +134,12 @@ class MockLogger implements Logger {
export const MediaProviders: Provider[] = [
MediaService, // Provide the service itself
RequestAvatarGenerationPresenter,
UploadMediaPresenter,
GetMediaPresenter,
DeleteMediaPresenter,
GetAvatarPresenter,
UpdateAvatarPresenter,
{
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
useClass: MockAvatarGenerationRepository,
@@ -138,41 +168,66 @@ export const MediaProviders: Provider[] = [
provide: LOGGER_TOKEN,
useClass: MockLogger,
},
// Output ports
{
provide: REQUEST_AVATAR_GENERATION_OUTPUT_PORT_TOKEN,
useExisting: RequestAvatarGenerationPresenter,
},
{
provide: UPLOAD_MEDIA_OUTPUT_PORT_TOKEN,
useExisting: UploadMediaPresenter,
},
{
provide: GET_MEDIA_OUTPUT_PORT_TOKEN,
useExisting: GetMediaPresenter,
},
{
provide: DELETE_MEDIA_OUTPUT_PORT_TOKEN,
useExisting: DeleteMediaPresenter,
},
{
provide: GET_AVATAR_OUTPUT_PORT_TOKEN,
useExisting: GetAvatarPresenter,
},
{
provide: UPDATE_AVATAR_OUTPUT_PORT_TOKEN,
useExisting: UpdateAvatarPresenter,
},
// Use cases
{
provide: REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarGenerationRepository, faceValidation: FaceValidationPort, avatarGeneration: AvatarGenerationPort, logger: Logger) =>
new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, logger),
inject: [AVATAR_GENERATION_REPOSITORY_TOKEN, FACE_VALIDATION_PORT_TOKEN, AVATAR_GENERATION_PORT_TOKEN, LOGGER_TOKEN],
useFactory: (avatarRepo: IAvatarGenerationRepository, faceValidation: FaceValidationPort, avatarGeneration: AvatarGenerationPort, output: UseCaseOutputPort<RequestAvatarGenerationResult>, logger: Logger) =>
new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, output, logger),
inject: [AVATAR_GENERATION_REPOSITORY_TOKEN, FACE_VALIDATION_PORT_TOKEN, AVATAR_GENERATION_PORT_TOKEN, REQUEST_AVATAR_GENERATION_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
},
{
provide: UPLOAD_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) =>
new UploadMediaUseCase(mediaRepo, mediaStorage, logger),
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN],
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, output: UseCaseOutputPort<UploadMediaResult>, logger: Logger) =>
new UploadMediaUseCase(mediaRepo, mediaStorage, output, logger),
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, UPLOAD_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
},
{
provide: GET_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, logger: Logger) =>
new GetMediaUseCase(mediaRepo, logger),
inject: [MEDIA_REPOSITORY_TOKEN, LOGGER_TOKEN],
useFactory: (mediaRepo: IMediaRepository, output: UseCaseOutputPort<GetMediaResult>, logger: Logger) =>
new GetMediaUseCase(mediaRepo, output, logger),
inject: [MEDIA_REPOSITORY_TOKEN, GET_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
},
{
provide: DELETE_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) =>
new DeleteMediaUseCase(mediaRepo, mediaStorage, logger),
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN],
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, output: UseCaseOutputPort<DeleteMediaResult>, logger: Logger) =>
new DeleteMediaUseCase(mediaRepo, mediaStorage, output, logger),
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, DELETE_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
},
{
provide: GET_AVATAR_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarRepository, logger: Logger) =>
new GetAvatarUseCase(avatarRepo, logger),
inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN],
useFactory: (avatarRepo: IAvatarRepository, output: UseCaseOutputPort<GetAvatarResult>, logger: Logger) =>
new GetAvatarUseCase(avatarRepo, output, logger),
inject: [AVATAR_REPOSITORY_TOKEN, GET_AVATAR_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
},
{
provide: UPDATE_AVATAR_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarRepository, logger: Logger) =>
new UpdateAvatarUseCase(avatarRepo, logger),
inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN],
useFactory: (avatarRepo: IAvatarRepository, output: UseCaseOutputPort<UpdateAvatarResult>, logger: Logger) =>
new UpdateAvatarUseCase(avatarRepo, output, logger),
inject: [AVATAR_REPOSITORY_TOKEN, UPDATE_AVATAR_OUTPUT_PORT_TOKEN, LOGGER_TOKEN],
},
];

View File

@@ -59,6 +59,12 @@ export class MediaService {
private readonly updateAvatarUseCase: UpdateAvatarUseCase,
@Inject(LOGGER_TOKEN)
private readonly logger: Logger,
private readonly requestAvatarGenerationPresenter: RequestAvatarGenerationPresenter,
private readonly uploadMediaPresenter: UploadMediaPresenter,
private readonly getMediaPresenter: GetMediaPresenter,
private readonly deleteMediaPresenter: DeleteMediaPresenter,
private readonly getAvatarPresenter: GetAvatarPresenter,
private readonly updateAvatarPresenter: UpdateAvatarPresenter,
) {}
async requestAvatarGeneration(
@@ -66,18 +72,23 @@ export class MediaService {
): Promise<RequestAvatarGenerationOutputDTO> {
this.logger.debug('[MediaService] Requesting avatar generation.');
const presenter = new RequestAvatarGenerationPresenter();
presenter.reset();
const result = await this.requestAvatarGenerationUseCase.execute({
userId: input.userId,
facePhotoData: input.facePhotoData,
suitColor: input.suitColor as RacingSuitColor,
});
presenter.present(result);
if (result.isErr()) {
const error = result.unwrapErr();
return {
success: false,
requestId: '',
avatarUrls: [],
errorMessage: error.details?.message ?? 'Failed to request avatar generation',
};
}
return presenter.responseModel;
return this.requestAvatarGenerationPresenter.responseModel;
}
async uploadMedia(
@@ -85,69 +96,87 @@ export class MediaService {
): Promise<UploadMediaOutputDTO> {
this.logger.debug('[MediaService] Uploading media.');
const presenter = new UploadMediaPresenter();
presenter.reset();
const result = await this.uploadMediaUseCase.execute({
file: input.file,
uploadedBy: input.userId ?? '',
metadata: input.metadata,
metadata: input.metadata || {},
});
presenter.present(result);
if (result.isErr()) {
const error = result.unwrapErr();
return {
success: false,
error: error.details?.message ?? 'Upload failed',
};
}
return presenter.responseModel;
return this.uploadMediaPresenter.responseModel;
}
async getMedia(mediaId: string): Promise<GetMediaOutputDTO | null> {
this.logger.debug(`[MediaService] Getting media: ${mediaId}`);
const presenter = new GetMediaPresenter();
presenter.reset();
const result = await this.getMediaUseCase.execute({ mediaId });
presenter.present(result);
return presenter.responseModel;
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'MEDIA_NOT_FOUND') {
return null;
}
throw new Error(error.details?.message ?? 'Failed to get media');
}
return this.getMediaPresenter.responseModel;
}
async deleteMedia(mediaId: string): Promise<DeleteMediaOutputDTO> {
this.logger.debug(`[MediaService] Deleting media: ${mediaId}`);
const presenter = new DeleteMediaPresenter();
presenter.reset();
const result = await this.deleteMediaUseCase.execute({ mediaId });
presenter.present(result);
return presenter.responseModel;
if (result.isErr()) {
const error = result.unwrapErr();
return {
success: false,
error: error.details?.message ?? 'Failed to delete media',
};
}
return this.deleteMediaPresenter.responseModel;
}
async getAvatar(driverId: string): Promise<GetAvatarOutputDTO | null> {
this.logger.debug(`[MediaService] Getting avatar for driver: ${driverId}`);
const presenter = new GetAvatarPresenter();
presenter.reset();
const result = await this.getAvatarUseCase.execute({ driverId });
presenter.present(result);
return presenter.responseModel;
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'AVATAR_NOT_FOUND') {
return null;
}
throw new Error(error.details?.message ?? 'Failed to get avatar');
}
return this.getAvatarPresenter.responseModel;
}
async updateAvatar(driverId: string, input: UpdateAvatarInput): Promise<UpdateAvatarOutputDTO> {
this.logger.debug(`[MediaService] Updating avatar for driver: ${driverId}`);
const presenter = new UpdateAvatarPresenter();
presenter.reset();
const result = await this.updateAvatarUseCase.execute({
driverId,
mediaUrl: input.mediaUrl,
mediaUrl: input.avatarUrl,
});
presenter.present(result);
if (result.isErr()) {
const error = result.unwrapErr();
return {
success: false,
error: error.details?.message ?? 'Failed to update avatar',
};
}
return presenter.responseModel;
return this.updateAvatarPresenter.responseModel;
}
}

View File

@@ -1,41 +1,19 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
DeleteMediaResult,
DeleteMediaErrorCode,
} from '@core/media/application/use-cases/DeleteMediaUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { DeleteMediaResult } from '@core/media/application/use-cases/DeleteMediaUseCase';
import type { DeleteMediaOutputDTO } from '../dtos/DeleteMediaOutputDTO';
type DeleteMediaResponseModel = DeleteMediaOutputDTO;
export type DeleteMediaApplicationError = ApplicationErrorCode<
DeleteMediaErrorCode,
{ message: string }
>;
export class DeleteMediaPresenter {
export class DeleteMediaPresenter implements UseCaseOutputPort<DeleteMediaResult> {
private model: DeleteMediaResponseModel | null = null;
reset(): void {
this.model = null;
}
present(result: Result<DeleteMediaResult, DeleteMediaApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
this.model = {
success: false,
error: error.details?.message ?? 'Failed to delete media',
};
return;
}
const output = result.unwrap();
present(result: DeleteMediaResult): void {
this.model = {
success: output.deleted,
error: undefined,
success: result.deleted,
};
}

View File

@@ -1,41 +1,19 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetAvatarResult,
GetAvatarErrorCode,
} from '@core/media/application/use-cases/GetAvatarUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetAvatarResult } from '@core/media/application/use-cases/GetAvatarUseCase';
import type { GetAvatarOutputDTO } from '../dtos/GetAvatarOutputDTO';
export type GetAvatarResponseModel = GetAvatarOutputDTO | null;
export type GetAvatarApplicationError = ApplicationErrorCode<
GetAvatarErrorCode,
{ message: string }
>;
export class GetAvatarPresenter {
export class GetAvatarPresenter implements UseCaseOutputPort<GetAvatarResult> {
private model: GetAvatarResponseModel | null = null;
reset(): void {
this.model = null;
}
present(result: Result<GetAvatarResult, GetAvatarApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'AVATAR_NOT_FOUND') {
this.model = null;
return;
}
throw new Error(error.details?.message ?? 'Failed to get avatar');
}
const output = result.unwrap();
present(result: GetAvatarResult): void {
this.model = {
avatarUrl: output.avatar.mediaUrl,
avatarUrl: result.avatar.mediaUrl,
};
}

View File

@@ -1,37 +1,18 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetMediaResult, GetMediaErrorCode } from '@core/media/application/use-cases/GetMediaUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetMediaResult } from '@core/media/application/use-cases/GetMediaUseCase';
import type { GetMediaOutputDTO } from '../dtos/GetMediaOutputDTO';
export type GetMediaResponseModel = GetMediaOutputDTO | null;
export type GetMediaApplicationError = ApplicationErrorCode<
GetMediaErrorCode,
{ message: string }
>;
export class GetMediaPresenter {
export class GetMediaPresenter implements UseCaseOutputPort<GetMediaResult> {
private model: GetMediaResponseModel | null = null;
reset(): void {
this.model = null;
}
present(result: Result<GetMediaResult, GetMediaApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'MEDIA_NOT_FOUND') {
this.model = null;
return;
}
throw new Error(error.details?.message ?? 'Failed to get media');
}
const output = result.unwrap();
const media = output.media;
present(result: GetMediaResult): void {
const media = result.media;
this.model = {
id: media.id,

View File

@@ -1,50 +1,21 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
RequestAvatarGenerationResult,
RequestAvatarGenerationErrorCode,
} from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { RequestAvatarGenerationResult } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
import type { RequestAvatarGenerationOutputDTO } from '../dtos/RequestAvatarGenerationOutputDTO';
type RequestAvatarGenerationResponseModel = RequestAvatarGenerationOutputDTO;
export type RequestAvatarGenerationApplicationError = ApplicationErrorCode<
RequestAvatarGenerationErrorCode,
{ message: string }
>;
export class RequestAvatarGenerationPresenter {
export class RequestAvatarGenerationPresenter implements UseCaseOutputPort<RequestAvatarGenerationResult> {
private model: RequestAvatarGenerationResponseModel | null = null;
reset() {
this.model = null;
}
present(
result: Result<
RequestAvatarGenerationResult,
RequestAvatarGenerationApplicationError
>,
): void {
if (result.isErr()) {
const error = result.unwrapErr();
this.model = {
success: false,
requestId: '',
avatarUrls: [],
errorMessage: error.details?.message ?? 'Failed to request avatar generation',
};
return;
}
const output = result.unwrap();
present(result: RequestAvatarGenerationResult): void {
this.model = {
success: output.status === 'completed',
requestId: output.requestId,
avatarUrls: output.avatarUrls,
errorMessage: undefined,
success: result.status === 'completed',
requestId: result.requestId,
avatarUrls: result.avatarUrls || [],
};
}

View File

@@ -1,36 +1,19 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
UpdateAvatarResult,
UpdateAvatarErrorCode,
} from '@core/media/application/use-cases/UpdateAvatarUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { UpdateAvatarResult } from '@core/media/application/use-cases/UpdateAvatarUseCase';
import type { UpdateAvatarOutputDTO } from '../dtos/UpdateAvatarOutputDTO';
type UpdateAvatarResponseModel = UpdateAvatarOutputDTO;
export type UpdateAvatarApplicationError = ApplicationErrorCode<
UpdateAvatarErrorCode,
{ message: string }
>;
export class UpdateAvatarPresenter {
export class UpdateAvatarPresenter implements UseCaseOutputPort<UpdateAvatarResult> {
private model: UpdateAvatarResponseModel | null = null;
reset(): void {
this.model = null;
}
present(result: Result<UpdateAvatarResult, UpdateAvatarApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to update avatar');
}
const output = result.unwrap();
present(result: UpdateAvatarResult): void {
this.model = {
success: true,
error: undefined,
};
}

View File

@@ -1,43 +1,21 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
UploadMediaResult,
UploadMediaErrorCode,
} from '@core/media/application/use-cases/UploadMediaUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { UploadMediaResult } from '@core/media/application/use-cases/UploadMediaUseCase';
import type { UploadMediaOutputDTO } from '../dtos/UploadMediaOutputDTO';
type UploadMediaResponseModel = UploadMediaOutputDTO;
export type UploadMediaApplicationError = ApplicationErrorCode<
UploadMediaErrorCode,
{ message: string }
>;
export class UploadMediaPresenter {
export class UploadMediaPresenter implements UseCaseOutputPort<UploadMediaResult> {
private model: UploadMediaResponseModel | null = null;
reset(): void {
this.model = null;
}
present(result: Result<UploadMediaResult, UploadMediaApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
this.model = {
success: false,
error: error.details?.message ?? 'Upload failed',
};
return;
}
const output = result.unwrap();
present(result: UploadMediaResult): void {
this.model = {
success: true,
mediaId: output.mediaId,
url: output.url,
error: undefined,
mediaId: result.mediaId,
url: result.url,
};
}