refactor use cases
This commit is contained in:
@@ -1,12 +1,23 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { DeleteMediaUseCase } from './DeleteMediaUseCase';
|
||||
import {
|
||||
DeleteMediaUseCase,
|
||||
type DeleteMediaInput,
|
||||
type DeleteMediaResult,
|
||||
type DeleteMediaErrorCode,
|
||||
} from './DeleteMediaUseCase';
|
||||
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||
import type { MediaStoragePort } from '../ports/MediaStoragePort';
|
||||
import type { IDeleteMediaPresenter } from '../presenters/IDeleteMediaPresenter';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Media } from '../../domain/entities/Media';
|
||||
import { MediaUrl } from '../../domain/value-objects/MediaUrl';
|
||||
|
||||
interface TestOutputPort extends UseCaseOutputPort<DeleteMediaResult> {
|
||||
present: Mock;
|
||||
result?: DeleteMediaResult;
|
||||
}
|
||||
|
||||
describe('DeleteMediaUseCase', () => {
|
||||
let mediaRepo: {
|
||||
findById: Mock;
|
||||
@@ -16,7 +27,7 @@ describe('DeleteMediaUseCase', () => {
|
||||
deleteMedia: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let presenter: IDeleteMediaPresenter & { result?: any };
|
||||
let output: TestOutputPort;
|
||||
let useCase: DeleteMediaUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -36,29 +47,35 @@ describe('DeleteMediaUseCase', () => {
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
presenter = {
|
||||
present: vi.fn((result) => {
|
||||
(presenter as any).result = result;
|
||||
output = {
|
||||
present: vi.fn((result: DeleteMediaResult) => {
|
||||
output.result = result;
|
||||
}),
|
||||
} as unknown as IDeleteMediaPresenter & { result?: any };
|
||||
} as unknown as TestOutputPort;
|
||||
|
||||
useCase = new DeleteMediaUseCase(
|
||||
mediaRepo as unknown as IMediaRepository,
|
||||
mediaStorage as unknown as MediaStoragePort,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns error result when media is not found', async () => {
|
||||
it('returns MEDIA_NOT_FOUND when media is not found', async () => {
|
||||
mediaRepo.findById.mockResolvedValue(null);
|
||||
|
||||
await useCase.execute({ mediaId: 'missing' }, presenter);
|
||||
const input: DeleteMediaInput = { mediaId: 'missing' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(mediaRepo.findById).toHaveBeenCalledWith('missing');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Media not found',
|
||||
});
|
||||
expect(result).toBeInstanceOf(Result);
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<
|
||||
DeleteMediaErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
expect(err.code).toBe('MEDIA_NOT_FOUND');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes media from storage and repository on success', async () => {
|
||||
@@ -68,30 +85,39 @@ describe('DeleteMediaUseCase', () => {
|
||||
originalName: 'file.png',
|
||||
mimeType: 'image/png',
|
||||
size: 123,
|
||||
url: MediaUrl.create('https://example.com/file.png'),
|
||||
url: 'https://example.com/file.png',
|
||||
type: 'image',
|
||||
uploadedBy: 'user-1',
|
||||
});
|
||||
|
||||
mediaRepo.findById.mockResolvedValue(media);
|
||||
|
||||
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||
const input: DeleteMediaInput = { mediaId: 'media-1' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(mediaRepo.findById).toHaveBeenCalledWith('media-1');
|
||||
expect(mediaStorage.deleteMedia).toHaveBeenCalledWith(media.url.value);
|
||||
expect(mediaRepo.delete).toHaveBeenCalledWith('media-1');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({ success: true });
|
||||
});
|
||||
|
||||
it('handles errors and presents failure result', async () => {
|
||||
mediaRepo.findById.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while deleting media',
|
||||
expect(output.present).toHaveBeenCalledWith({
|
||||
mediaId: 'media-1',
|
||||
deleted: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles repository errors by returning REPOSITORY_ERROR', async () => {
|
||||
mediaRepo.findById.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
const input: DeleteMediaInput = { mediaId: 'media-1' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<
|
||||
DeleteMediaErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,71 +6,73 @@
|
||||
|
||||
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||
import type { MediaStoragePort } from '../ports/MediaStoragePort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IDeleteMediaPresenter } from '../presenters/IDeleteMediaPresenter';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export interface DeleteMediaInput {
|
||||
mediaId: string;
|
||||
}
|
||||
|
||||
export interface DeleteMediaResult {
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
mediaId: string;
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export interface IDeleteMediaPresenter {
|
||||
present(result: DeleteMediaResult): void;
|
||||
}
|
||||
export type DeleteMediaErrorCode = 'MEDIA_NOT_FOUND' | 'REPOSITORY_ERROR';
|
||||
|
||||
export type DeleteMediaApplicationError = ApplicationErrorCode<
|
||||
DeleteMediaErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
|
||||
export class DeleteMediaUseCase {
|
||||
constructor(
|
||||
private readonly mediaRepo: IMediaRepository,
|
||||
private readonly mediaStorage: MediaStoragePort,
|
||||
private readonly output: UseCaseOutputPort<DeleteMediaResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: DeleteMediaInput,
|
||||
presenter: IDeleteMediaPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[DeleteMediaUseCase] Deleting media', {
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
async execute(input: DeleteMediaInput): Promise<Result<void, DeleteMediaApplicationError>> {
|
||||
this.logger.info('[DeleteMediaUseCase] Deleting media', {
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
|
||||
try {
|
||||
const media = await this.mediaRepo.findById(input.mediaId);
|
||||
|
||||
if (!media) {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Media not found',
|
||||
return Result.err({
|
||||
code: 'MEDIA_NOT_FOUND',
|
||||
details: { message: 'Media not found' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete from storage
|
||||
await this.mediaStorage.deleteMedia(media.url.value);
|
||||
|
||||
// Delete from repository
|
||||
await this.mediaRepo.delete(input.mediaId);
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
this.output.present({
|
||||
mediaId: input.mediaId,
|
||||
deleted: true,
|
||||
});
|
||||
|
||||
this.logger.info('[DeleteMediaUseCase] Media deleted successfully', {
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
this.logger.error('[DeleteMediaUseCase] Error deleting media', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
error: err.message,
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while deleting media',
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message ?? 'Unexpected repository error' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetAvatarUseCase } from './GetAvatarUseCase';
|
||||
import {
|
||||
GetAvatarUseCase,
|
||||
type GetAvatarInput,
|
||||
type GetAvatarResult,
|
||||
type GetAvatarErrorCode,
|
||||
} from './GetAvatarUseCase';
|
||||
import type { IAvatarRepository } from '../../domain/repositories/IAvatarRepository';
|
||||
import type { IGetAvatarPresenter } from '../presenters/IGetAvatarPresenter';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Avatar } from '../../domain/entities/Avatar';
|
||||
import { MediaUrl } from '../../domain/value-objects/MediaUrl';
|
||||
|
||||
interface TestPresenter extends IGetAvatarPresenter {
|
||||
result?: any;
|
||||
interface TestOutputPort extends UseCaseOutputPort<GetAvatarResult> {
|
||||
present: Mock;
|
||||
result?: GetAvatarResult;
|
||||
}
|
||||
|
||||
describe('GetAvatarUseCase', () => {
|
||||
@@ -16,7 +22,7 @@ describe('GetAvatarUseCase', () => {
|
||||
save: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let presenter: TestPresenter;
|
||||
let output: TestOutputPort;
|
||||
let useCase: GetAvatarUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -32,44 +38,51 @@ describe('GetAvatarUseCase', () => {
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
presenter = {
|
||||
present: vi.fn((result) => {
|
||||
presenter.result = result;
|
||||
output = {
|
||||
present: vi.fn((result: GetAvatarResult) => {
|
||||
output.result = result;
|
||||
}),
|
||||
} as unknown as TestPresenter;
|
||||
} as unknown as TestOutputPort;
|
||||
|
||||
useCase = new GetAvatarUseCase(
|
||||
avatarRepo as unknown as IAvatarRepository,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('presents error when no avatar exists for driver', async () => {
|
||||
it('returns AVATAR_NOT_FOUND when no avatar exists for driver', async () => {
|
||||
avatarRepo.findActiveByDriverId.mockResolvedValue(null);
|
||||
|
||||
await useCase.execute({ driverId: 'driver-1' }, presenter);
|
||||
const input: GetAvatarInput = { driverId: 'driver-1' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(avatarRepo.findActiveByDriverId).toHaveBeenCalledWith('driver-1');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Avatar not found',
|
||||
});
|
||||
expect(result).toBeInstanceOf(Result);
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<
|
||||
GetAvatarErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
expect(err.code).toBe('AVATAR_NOT_FOUND');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('presents avatar details when avatar exists', async () => {
|
||||
const avatar = Avatar.create({
|
||||
id: 'avatar-1',
|
||||
driverId: 'driver-1',
|
||||
mediaUrl: MediaUrl.create('https://example.com/avatar.png'),
|
||||
mediaUrl: 'https://example.com/avatar.png',
|
||||
});
|
||||
|
||||
avatarRepo.findActiveByDriverId.mockResolvedValue(avatar);
|
||||
|
||||
await useCase.execute({ driverId: 'driver-1' }, presenter);
|
||||
const input: GetAvatarInput = { driverId: 'driver-1' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(avatarRepo.findActiveByDriverId).toHaveBeenCalledWith('driver-1');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
expect(output.present).toHaveBeenCalledWith({
|
||||
avatar: {
|
||||
id: avatar.id,
|
||||
driverId: avatar.driverId,
|
||||
@@ -79,15 +92,19 @@ describe('GetAvatarUseCase', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles errors by logging and presenting failure', async () => {
|
||||
it('handles repository errors by returning REPOSITORY_ERROR', async () => {
|
||||
avatarRepo.findActiveByDriverId.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
await useCase.execute({ driverId: 'driver-1' }, presenter);
|
||||
const input: GetAvatarInput = { driverId: 'driver-1' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while retrieving avatar',
|
||||
});
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<
|
||||
GetAvatarErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,55 +5,53 @@
|
||||
*/
|
||||
|
||||
import type { IAvatarRepository } from '../../domain/repositories/IAvatarRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IGetAvatarPresenter } from '../presenters/IGetAvatarPresenter';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export interface GetAvatarInput {
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface GetAvatarResult {
|
||||
success: boolean;
|
||||
avatar?: {
|
||||
avatar: {
|
||||
id: string;
|
||||
driverId: string;
|
||||
mediaUrl: string;
|
||||
selectedAt: Date;
|
||||
};
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IGetAvatarPresenter {
|
||||
present(result: GetAvatarResult): void;
|
||||
}
|
||||
export type GetAvatarErrorCode = 'AVATAR_NOT_FOUND' | 'REPOSITORY_ERROR';
|
||||
|
||||
export type GetAvatarApplicationError = ApplicationErrorCode<
|
||||
GetAvatarErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
|
||||
export class GetAvatarUseCase {
|
||||
constructor(
|
||||
private readonly avatarRepo: IAvatarRepository,
|
||||
private readonly output: UseCaseOutputPort<GetAvatarResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: GetAvatarInput,
|
||||
presenter: IGetAvatarPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[GetAvatarUseCase] Getting avatar', {
|
||||
driverId: input.driverId,
|
||||
});
|
||||
async execute(input: GetAvatarInput): Promise<Result<void, GetAvatarApplicationError>> {
|
||||
this.logger.info('[GetAvatarUseCase] Getting avatar', {
|
||||
driverId: input.driverId,
|
||||
});
|
||||
|
||||
try {
|
||||
const avatar = await this.avatarRepo.findActiveByDriverId(input.driverId);
|
||||
|
||||
if (!avatar) {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Avatar not found',
|
||||
return Result.err({
|
||||
code: 'AVATAR_NOT_FOUND',
|
||||
details: { message: 'Avatar not found' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
this.output.present({
|
||||
avatar: {
|
||||
id: avatar.id,
|
||||
driverId: avatar.driverId,
|
||||
@@ -62,15 +60,17 @@ export class GetAvatarUseCase {
|
||||
},
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
this.logger.error('[GetAvatarUseCase] Error getting avatar', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
this.logger.error('[GetAvatarUseCase] Error getting avatar', err, {
|
||||
driverId: input.driverId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while retrieving avatar',
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message ?? 'Unexpected repository error' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetMediaUseCase } from './GetMediaUseCase';
|
||||
import {
|
||||
GetMediaUseCase,
|
||||
type GetMediaInput,
|
||||
type GetMediaResult,
|
||||
type GetMediaErrorCode,
|
||||
} from './GetMediaUseCase';
|
||||
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||
import type { IGetMediaPresenter } from '../presenters/IGetMediaPresenter';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Media } from '../../domain/entities/Media';
|
||||
import { MediaUrl } from '../../domain/value-objects/MediaUrl';
|
||||
|
||||
interface TestPresenter extends IGetMediaPresenter {
|
||||
result?: any;
|
||||
interface TestOutputPort extends UseCaseOutputPort<GetMediaResult> {
|
||||
present: Mock;
|
||||
result?: GetMediaResult;
|
||||
}
|
||||
|
||||
describe('GetMediaUseCase', () => {
|
||||
@@ -15,7 +22,7 @@ describe('GetMediaUseCase', () => {
|
||||
findById: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let presenter: TestPresenter;
|
||||
let output: TestOutputPort;
|
||||
let useCase: GetMediaUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -30,28 +37,31 @@ describe('GetMediaUseCase', () => {
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
presenter = {
|
||||
present: vi.fn((result) => {
|
||||
presenter.result = result;
|
||||
output = {
|
||||
present: vi.fn((result: GetMediaResult) => {
|
||||
output.result = result;
|
||||
}),
|
||||
} as unknown as TestPresenter;
|
||||
} as unknown as TestOutputPort;
|
||||
|
||||
useCase = new GetMediaUseCase(
|
||||
mediaRepo as unknown as IMediaRepository,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('presents error when media is not found', async () => {
|
||||
it('returns MEDIA_NOT_FOUND when media is not found', async () => {
|
||||
mediaRepo.findById.mockResolvedValue(null);
|
||||
|
||||
await useCase.execute({ mediaId: 'missing' }, presenter);
|
||||
const input: GetMediaInput = { mediaId: 'missing' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(mediaRepo.findById).toHaveBeenCalledWith('missing');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Media not found',
|
||||
});
|
||||
expect(result).toBeInstanceOf(Result);
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<GetMediaErrorCode, { message: string }>;
|
||||
expect(err.code).toBe('MEDIA_NOT_FOUND');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('presents media details when media exists', async () => {
|
||||
@@ -68,11 +78,12 @@ describe('GetMediaUseCase', () => {
|
||||
|
||||
mediaRepo.findById.mockResolvedValue(media);
|
||||
|
||||
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||
const input: GetMediaInput = { mediaId: 'media-1' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(mediaRepo.findById).toHaveBeenCalledWith('media-1');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
expect(output.present).toHaveBeenCalledWith({
|
||||
media: {
|
||||
id: media.id,
|
||||
filename: media.filename,
|
||||
@@ -88,15 +99,15 @@ describe('GetMediaUseCase', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles errors by logging and presenting failure', async () => {
|
||||
it('handles repository errors by returning REPOSITORY_ERROR', async () => {
|
||||
mediaRepo.findById.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||
const input: GetMediaInput = { mediaId: 'media-1' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while retrieving media',
|
||||
});
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<GetMediaErrorCode, { message: string }>;
|
||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
*/
|
||||
|
||||
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IGetMediaPresenter } from '../presenters/IGetMediaPresenter';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export interface GetMediaInput {
|
||||
mediaId: string;
|
||||
}
|
||||
|
||||
export interface GetMediaResult {
|
||||
success: boolean;
|
||||
media?: {
|
||||
media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
originalName: string;
|
||||
@@ -26,40 +26,35 @@ export interface GetMediaResult {
|
||||
uploadedAt: Date;
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IGetMediaPresenter {
|
||||
present(result: GetMediaResult): void;
|
||||
}
|
||||
export type GetMediaErrorCode = 'MEDIA_NOT_FOUND' | 'REPOSITORY_ERROR';
|
||||
|
||||
export class GetMediaUseCase {
|
||||
constructor(
|
||||
private readonly mediaRepo: IMediaRepository,
|
||||
private readonly output: UseCaseOutputPort<GetMediaResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: GetMediaInput,
|
||||
presenter: IGetMediaPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[GetMediaUseCase] Getting media', {
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
): Promise<Result<void, ApplicationErrorCode<GetMediaErrorCode, { message: string }>>> {
|
||||
this.logger.info('[GetMediaUseCase] Getting media', {
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
|
||||
try {
|
||||
const media = await this.mediaRepo.findById(input.mediaId);
|
||||
|
||||
if (!media) {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Media not found',
|
||||
return Result.err({
|
||||
code: 'MEDIA_NOT_FOUND',
|
||||
details: { message: 'Media not found' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
this.output.present({
|
||||
media: {
|
||||
id: media.id,
|
||||
filename: media.filename,
|
||||
@@ -74,15 +69,16 @@ export class GetMediaUseCase {
|
||||
},
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
this.logger.error('[GetMediaUseCase] Error getting media', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error('[GetMediaUseCase] Error getting media', err, {
|
||||
mediaId: input.mediaId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while retrieving media',
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
||||
import type { FaceValidationPort } from '../ports/FaceValidationPort';
|
||||
import type { AvatarGenerationPort } from '../ports/AvatarGenerationPort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { AvatarGenerationRequest } from '../../domain/entities/AvatarGenerationRequest';
|
||||
import type { IRequestAvatarGenerationPresenter } from '../presenters/IRequestAvatarGenerationPresenter';
|
||||
import type { RacingSuitColor } from '../../domain/types/AvatarGenerationRequest';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export interface RequestAvatarGenerationInput {
|
||||
userId: string;
|
||||
@@ -20,63 +21,68 @@ export interface RequestAvatarGenerationInput {
|
||||
style?: 'realistic' | 'cartoon' | 'pixel-art';
|
||||
}
|
||||
|
||||
export interface RequestAvatarGenerationResult {
|
||||
requestId: string;
|
||||
status: 'validating' | 'generating' | 'completed';
|
||||
avatarUrls?: string[];
|
||||
}
|
||||
|
||||
export type RequestAvatarGenerationErrorCode =
|
||||
| 'FACE_VALIDATION_FAILED'
|
||||
| 'GENERATION_FAILED'
|
||||
| 'REPOSITORY_ERROR';
|
||||
|
||||
export type RequestAvatarGenerationApplicationError = ApplicationErrorCode<
|
||||
RequestAvatarGenerationErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
|
||||
export class RequestAvatarGenerationUseCase {
|
||||
constructor(
|
||||
private readonly avatarRepo: IAvatarGenerationRepository,
|
||||
private readonly faceValidation: FaceValidationPort,
|
||||
private readonly avatarGeneration: AvatarGenerationPort,
|
||||
private readonly output: UseCaseOutputPort<RequestAvatarGenerationResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: RequestAvatarGenerationInput,
|
||||
presenter: IRequestAvatarGenerationPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[RequestAvatarGenerationUseCase] Starting avatar generation request', {
|
||||
userId: input.userId,
|
||||
suitColor: input.suitColor,
|
||||
});
|
||||
): Promise<Result<void, RequestAvatarGenerationApplicationError>> {
|
||||
this.logger.info('[RequestAvatarGenerationUseCase] Starting avatar generation request', {
|
||||
userId: input.userId,
|
||||
suitColor: input.suitColor,
|
||||
});
|
||||
|
||||
// Create the avatar generation request entity
|
||||
try {
|
||||
const requestId = uuidv4();
|
||||
const request = AvatarGenerationRequest.create({
|
||||
id: requestId,
|
||||
userId: input.userId,
|
||||
facePhotoUrl: input.facePhotoData, // Assuming facePhotoData is a URL or base64
|
||||
facePhotoUrl: input.facePhotoData,
|
||||
suitColor: input.suitColor,
|
||||
style: input.style,
|
||||
});
|
||||
|
||||
// Save initial request
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
// Present initial status
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'validating',
|
||||
});
|
||||
|
||||
// Validate face photo
|
||||
request.markAsValidating();
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
const validationResult = await this.faceValidation.validateFacePhoto(input.facePhotoData);
|
||||
|
||||
if (!validationResult.isValid || !validationResult.hasFace || validationResult.faceCount !== 1) {
|
||||
const errorMessage = validationResult.errorMessage || 'Invalid face photo: must contain exactly one face';
|
||||
const errorMessage =
|
||||
validationResult.errorMessage || 'Invalid face photo: must contain exactly one face';
|
||||
request.fail(errorMessage);
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'failed',
|
||||
errorMessage,
|
||||
return Result.err({
|
||||
code: 'FACE_VALIDATION_FAILED',
|
||||
details: { message: errorMessage },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate avatars
|
||||
request.markAsGenerating();
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
@@ -85,7 +91,7 @@ export class RequestAvatarGenerationUseCase {
|
||||
prompt: request.buildPrompt(),
|
||||
suitColor: input.suitColor,
|
||||
style: input.style || 'realistic',
|
||||
count: 3, // Generate 3 avatar options
|
||||
count: 3,
|
||||
};
|
||||
|
||||
const generationResult = await this.avatarGeneration.generateAvatars(generationOptions);
|
||||
@@ -95,20 +101,17 @@ export class RequestAvatarGenerationUseCase {
|
||||
request.fail(errorMessage);
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'failed',
|
||||
errorMessage,
|
||||
return Result.err({
|
||||
code: 'GENERATION_FAILED',
|
||||
details: { message: errorMessage },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Complete the request
|
||||
const avatarUrls = generationResult.avatars.map(avatar => avatar.url);
|
||||
request.completeWithAvatars(avatarUrls);
|
||||
await this.avatarRepo.save(request);
|
||||
|
||||
presenter.present({
|
||||
this.output.present({
|
||||
requestId,
|
||||
status: 'completed',
|
||||
avatarUrls,
|
||||
@@ -120,16 +123,17 @@ export class RequestAvatarGenerationUseCase {
|
||||
avatarCount: avatarUrls.length,
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
this.logger.error('[RequestAvatarGenerationUseCase] Error during avatar generation', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
this.logger.error('[RequestAvatarGenerationUseCase] Error during avatar generation', err, {
|
||||
userId: input.userId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
requestId: uuidv4(), // Fallback ID
|
||||
status: 'failed',
|
||||
errorMessage: 'Internal error occurred during avatar generation',
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message ?? 'Internal error occurred during avatar generation' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
*/
|
||||
|
||||
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { ISelectAvatarPresenter } from '../presenters/ISelectAvatarPresenter';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export interface SelectAvatarInput {
|
||||
requestId: string;
|
||||
@@ -14,47 +15,48 @@ export interface SelectAvatarInput {
|
||||
}
|
||||
|
||||
export interface SelectAvatarResult {
|
||||
success: boolean;
|
||||
selectedAvatarUrl?: string;
|
||||
errorMessage?: string;
|
||||
requestId: string;
|
||||
selectedAvatarUrl: string;
|
||||
}
|
||||
|
||||
export interface ISelectAvatarPresenter {
|
||||
present(result: SelectAvatarResult): void;
|
||||
}
|
||||
export type SelectAvatarErrorCode =
|
||||
| 'REQUEST_NOT_FOUND'
|
||||
| 'REQUEST_NOT_COMPLETED'
|
||||
| 'REPOSITORY_ERROR';
|
||||
|
||||
export type SelectAvatarApplicationError = ApplicationErrorCode<
|
||||
SelectAvatarErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
|
||||
export class SelectAvatarUseCase {
|
||||
constructor(
|
||||
private readonly avatarRepo: IAvatarGenerationRepository,
|
||||
private readonly output: UseCaseOutputPort<SelectAvatarResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: SelectAvatarInput,
|
||||
presenter: ISelectAvatarPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[SelectAvatarUseCase] Selecting avatar', {
|
||||
requestId: input.requestId,
|
||||
selectedIndex: input.selectedIndex,
|
||||
});
|
||||
async execute(input: SelectAvatarInput): Promise<Result<void, SelectAvatarApplicationError>> {
|
||||
this.logger.info('[SelectAvatarUseCase] Selecting avatar', {
|
||||
requestId: input.requestId,
|
||||
selectedIndex: input.selectedIndex,
|
||||
});
|
||||
|
||||
try {
|
||||
const request = await this.avatarRepo.findById(input.requestId);
|
||||
|
||||
if (!request) {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Avatar generation request not found',
|
||||
return Result.err({
|
||||
code: 'REQUEST_NOT_FOUND',
|
||||
details: { message: 'Avatar generation request not found' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.status !== 'completed') {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Avatar generation is not completed yet',
|
||||
return Result.err({
|
||||
code: 'REQUEST_NOT_COMPLETED',
|
||||
details: { message: 'Avatar generation is not completed yet' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
request.selectAvatar(input.selectedIndex);
|
||||
@@ -62,8 +64,8 @@ export class SelectAvatarUseCase {
|
||||
|
||||
const selectedAvatarUrl = request.selectedAvatarUrl;
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
this.output.present({
|
||||
requestId: input.requestId,
|
||||
selectedAvatarUrl,
|
||||
});
|
||||
|
||||
@@ -72,15 +74,17 @@ export class SelectAvatarUseCase {
|
||||
selectedAvatarUrl,
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
this.logger.error('[SelectAvatarUseCase] Error selecting avatar', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
this.logger.error('[SelectAvatarUseCase] Error selecting avatar', err, {
|
||||
requestId: input.requestId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while selecting avatar',
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message ?? 'Unexpected repository error' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
* Handles the business logic for updating a driver's avatar.
|
||||
*/
|
||||
|
||||
import type { IAvatarRepository } from '../../domain/repositories/IAvatarRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Avatar } from '../../domain/entities/Avatar';
|
||||
import type { IUpdateAvatarPresenter } from '../presenters/IUpdateAvatarPresenter';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Avatar } from '../../domain/entities/Avatar';
|
||||
import type { IAvatarRepository } from '../../domain/repositories/IAvatarRepository';
|
||||
|
||||
export interface UpdateAvatarInput {
|
||||
driverId: string;
|
||||
@@ -16,39 +17,38 @@ export interface UpdateAvatarInput {
|
||||
}
|
||||
|
||||
export interface UpdateAvatarResult {
|
||||
success: boolean;
|
||||
errorMessage?: string;
|
||||
avatarId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface IUpdateAvatarPresenter {
|
||||
present(result: UpdateAvatarResult): void;
|
||||
}
|
||||
export type UpdateAvatarErrorCode = 'REPOSITORY_ERROR';
|
||||
|
||||
export type UpdateAvatarApplicationError = ApplicationErrorCode<
|
||||
UpdateAvatarErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
|
||||
export class UpdateAvatarUseCase {
|
||||
constructor(
|
||||
private readonly avatarRepo: IAvatarRepository,
|
||||
private readonly output: UseCaseOutputPort<UpdateAvatarResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: UpdateAvatarInput,
|
||||
presenter: IUpdateAvatarPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[UpdateAvatarUseCase] Updating avatar', {
|
||||
driverId: input.driverId,
|
||||
mediaUrl: input.mediaUrl,
|
||||
});
|
||||
async execute(input: UpdateAvatarInput): Promise<Result<void, UpdateAvatarApplicationError>> {
|
||||
this.logger.info('[UpdateAvatarUseCase] Updating avatar', {
|
||||
driverId: input.driverId,
|
||||
mediaUrl: input.mediaUrl,
|
||||
});
|
||||
|
||||
// Deactivate current active avatar
|
||||
try {
|
||||
const currentAvatar = await this.avatarRepo.findActiveByDriverId(input.driverId);
|
||||
if (currentAvatar) {
|
||||
currentAvatar.deactivate();
|
||||
await this.avatarRepo.save(currentAvatar);
|
||||
}
|
||||
|
||||
// Create new avatar
|
||||
const avatarId = uuidv4();
|
||||
const avatarId = uuidv4(); // TODO this ID should be a value object
|
||||
const newAvatar = Avatar.create({
|
||||
id: avatarId,
|
||||
driverId: input.driverId,
|
||||
@@ -57,8 +57,9 @@ export class UpdateAvatarUseCase {
|
||||
|
||||
await this.avatarRepo.save(newAvatar);
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
this.output.present({
|
||||
avatarId,
|
||||
driverId: input.driverId,
|
||||
});
|
||||
|
||||
this.logger.info('[UpdateAvatarUseCase] Avatar updated successfully', {
|
||||
@@ -66,15 +67,17 @@ export class UpdateAvatarUseCase {
|
||||
avatarId,
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
this.logger.error('[UpdateAvatarUseCase] Error updating avatar', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
this.logger.error('[UpdateAvatarUseCase] Error updating avatar', err, {
|
||||
driverId: input.driverId,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while updating avatar',
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message ?? 'Internal error occurred while updating avatar' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
|
||||
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||
import type { MediaStoragePort } from '../ports/MediaStoragePort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Media } from '../../domain/entities/Media';
|
||||
import type { IUploadMediaPresenter } from '../presenters/IUploadMediaPresenter';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface UploadMediaInput {
|
||||
@@ -18,34 +19,32 @@ export interface UploadMediaInput {
|
||||
}
|
||||
|
||||
export interface UploadMediaResult {
|
||||
success: boolean;
|
||||
mediaId?: string;
|
||||
url?: string;
|
||||
errorMessage?: string;
|
||||
mediaId: string;
|
||||
url: string | undefined;
|
||||
}
|
||||
|
||||
export interface IUploadMediaPresenter {
|
||||
present(result: UploadMediaResult): void;
|
||||
}
|
||||
export type UploadMediaErrorCode =
|
||||
| 'UPLOAD_FAILED'
|
||||
| 'REPOSITORY_ERROR';
|
||||
|
||||
export class UploadMediaUseCase {
|
||||
constructor(
|
||||
private readonly mediaRepo: IMediaRepository,
|
||||
private readonly mediaStorage: MediaStoragePort,
|
||||
private readonly output: UseCaseOutputPort<UploadMediaResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: UploadMediaInput,
|
||||
presenter: IUploadMediaPresenter,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.info('[UploadMediaUseCase] Starting media upload', {
|
||||
filename: input.file.originalname,
|
||||
size: input.file.size,
|
||||
uploadedBy: input.uploadedBy,
|
||||
});
|
||||
): Promise<Result<void, ApplicationErrorCode<UploadMediaErrorCode, { message: string }>>> {
|
||||
this.logger.info('[UploadMediaUseCase] Starting media upload', {
|
||||
filename: input.file.originalname,
|
||||
size: input.file.size,
|
||||
uploadedBy: input.uploadedBy,
|
||||
});
|
||||
|
||||
try {
|
||||
// Upload file to storage service
|
||||
const uploadResult = await this.mediaStorage.uploadMedia(input.file.buffer, {
|
||||
filename: input.file.originalname,
|
||||
@@ -54,11 +53,13 @@ export class UploadMediaUseCase {
|
||||
});
|
||||
|
||||
if (!uploadResult.success) {
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: uploadResult.errorMessage || 'Failed to upload media',
|
||||
return Result.err({
|
||||
code: 'UPLOAD_FAILED',
|
||||
details: {
|
||||
message:
|
||||
uploadResult.errorMessage ?? 'Failed to upload media',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine media type
|
||||
@@ -85,8 +86,7 @@ export class UploadMediaUseCase {
|
||||
// Save to repository
|
||||
await this.mediaRepo.save(media);
|
||||
|
||||
presenter.present({
|
||||
success: true,
|
||||
this.output.present({
|
||||
mediaId,
|
||||
url: uploadResult.url,
|
||||
});
|
||||
@@ -96,15 +96,17 @@ export class UploadMediaUseCase {
|
||||
url: uploadResult.url,
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error('[UploadMediaUseCase] Error uploading media', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
error: err.message,
|
||||
filename: input.file.originalname,
|
||||
});
|
||||
|
||||
presenter.present({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred during media upload',
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user