This commit is contained in:
2025-12-16 15:42:38 +01:00
parent 29410708c8
commit 362894d1a5
147 changed files with 780 additions and 375 deletions

View File

@@ -0,0 +1,26 @@
import { Controller, Post, Body, HttpStatus, Res } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { Response } from 'express';
import { MediaService } from './MediaService';
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
@ApiTags('media')
@Controller('media')
export class MediaController {
constructor(private readonly mediaService: MediaService) {}
@Post('avatar/generate')
@ApiOperation({ summary: 'Request avatar generation' })
@ApiResponse({ status: 201, description: 'Avatar generation request submitted', type: RequestAvatarGenerationOutput })
async requestAvatarGeneration(
@Body() input: RequestAvatarGenerationInput,
@Res() res: Response,
): Promise<void> {
const result = await this.mediaService.requestAvatarGeneration(input);
if (result.success) {
res.status(HttpStatus.CREATED).json(result);
} else {
res.status(HttpStatus.BAD_REQUEST).json(result);
}
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { MediaService } from './MediaService';
import { MediaController } from './MediaController';
import { MediaProviders } from './MediaProviders';
@Module({
controllers: [MediaController],
providers: MediaProviders,
exports: [MediaService],
})
export class MediaModule {}

View File

@@ -0,0 +1,82 @@
import { Provider } from '@nestjs/common';
import { MediaService } from './MediaService';
// Import core interfaces
import { IAvatarGenerationRepository } from '@core/media/domain/repositories/IAvatarGenerationRepository';
import { FaceValidationPort } from '@core/media/application/ports/FaceValidationPort';
import { AvatarGenerationPort } from '@core/media/application/ports/AvatarGenerationPort';
import type { Logger } from '@core/shared/application';
// Import use cases
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
// Define injection tokens
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort';
export const AVATAR_GENERATION_PORT_TOKEN = 'AvatarGenerationPort';
export const LOGGER_TOKEN = 'Logger';
// Use case tokens
export const REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN = 'RequestAvatarGenerationUseCase';
// Mock implementations
class MockAvatarGenerationRepository implements IAvatarGenerationRepository {
async save(_request: any): Promise<void> {}
async findById(_id: string): Promise<any | null> { return null; }
async findByUserId(_userId: string): Promise<any[]> { return []; }
async findLatestByUserId(_userId: string): Promise<any | null> { return null; }
async delete(_id: string): Promise<void> {}
}
class MockFaceValidationAdapter implements FaceValidationPort {
async validateFacePhoto(data: string): Promise<any> {
return { isValid: true, hasFace: true, faceCount: 1 };
}
}
class MockAvatarGenerationAdapter implements AvatarGenerationPort {
async generateAvatars(options: any): Promise<any> {
return {
success: true,
avatars: [
{ url: 'https://cdn.example.com/avatars/mock-avatar-1.png' },
{ url: 'https://cdn.example.com/avatars/mock-avatar-2.png' },
{ url: 'https://cdn.example.com/avatars/mock-avatar-3.png' },
],
};
}
}
class MockLogger implements Logger {
debug(message: string, meta?: any): void {}
info(message: string, meta?: any): void {}
warn(message: string, meta?: any): void {}
error(message: string, error?: Error): void {}
}
export const MediaProviders: Provider[] = [
MediaService, // Provide the service itself
{
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
useClass: MockAvatarGenerationRepository,
},
{
provide: FACE_VALIDATION_PORT_TOKEN,
useClass: MockFaceValidationAdapter,
},
{
provide: AVATAR_GENERATION_PORT_TOKEN,
useClass: MockAvatarGenerationAdapter,
},
{
provide: LOGGER_TOKEN,
useClass: MockLogger,
},
// 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],
},
];

View File

@@ -0,0 +1,32 @@
import { Injectable, Inject } from '@nestjs/common';
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto';
// Use cases
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
// Presenters
import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter';
// Tokens
import { REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN, LOGGER_TOKEN } from './MediaProviders';
import type { Logger } from '@core/shared/application';
@Injectable()
export class MediaService {
constructor(
@Inject(REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN) private readonly requestAvatarGenerationUseCase: RequestAvatarGenerationUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
async requestAvatarGeneration(input: RequestAvatarGenerationInput): Promise<RequestAvatarGenerationOutput> {
this.logger.debug('[MediaService] Requesting avatar generation.');
const presenter = new RequestAvatarGenerationPresenter();
await this.requestAvatarGenerationUseCase.execute({
userId: input.userId,
facePhotoData: input.facePhotoData,
suitColor: input.suitColor as any,
}, presenter);
return presenter.viewModel;
}
}

View File

@@ -0,0 +1,40 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsBoolean } from 'class-validator';
export class RequestAvatarGenerationInput {
@ApiProperty()
@IsString()
@IsNotEmpty()
userId: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
facePhotoData: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
suitColor: string;
}
export class RequestAvatarGenerationOutput {
@ApiProperty({ type: Boolean })
@IsBoolean()
success: boolean;
@ApiProperty({ required: false })
@IsString()
requestId?: string;
@ApiProperty({ type: [String], required: false })
avatarUrls?: string[];
@ApiProperty({ required: false })
@IsString()
errorMessage?: string;
}
// Assuming FacePhotoData and SuitColor are simple string types for DTO purposes
export type FacePhotoData = string;
export type SuitColor = string;

View File

@@ -0,0 +1,28 @@
import { RequestAvatarGenerationOutput } from '../dto/MediaDto';
import type { IRequestAvatarGenerationPresenter, RequestAvatarGenerationResultDTO } from '@core/media/application/presenters/IRequestAvatarGenerationPresenter';
export class RequestAvatarGenerationPresenter implements IRequestAvatarGenerationPresenter {
private result: RequestAvatarGenerationOutput | null = null;
reset() {
this.result = null;
}
present(dto: RequestAvatarGenerationResultDTO) {
this.result = {
success: dto.status === 'completed',
requestId: dto.requestId,
avatarUrls: dto.avatarUrls,
errorMessage: dto.errorMessage,
};
}
get viewModel(): RequestAvatarGenerationOutput {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
getViewModel(): RequestAvatarGenerationOutput {
return this.viewModel;
}
}