All files / services imageService.ts

80% Statements 28/35
85.71% Branches 6/7
100% Functions 4/4
80% Lines 28/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 1261x 1x     1x   1x 1x   1x 1x 1x             1x               1x 1x                           1x           1x       1x     1x             1x                   1x   1x   1x                       1x   1x 1x 1x                               1x   1x 1x 1x 1x                          
import { v4 as uuidv4 } from 'uuid';
import { ImageStorageService } from './imageStorageService';
import { IImageStorageService } from './imageStorageService.interface';
import { IImageRepository } from '../repositories/imageRepository.interface';
import { ImageRepository } from '../repositories/imageRepository';
import { ImageMetadata } from '../models/imageMetadata.model';
import { getLogger } from '../common/logger';
import { ValidationError } from '../common/errors';
 
const logger = getLogger('ImageService');
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const PRESIGNED_URL_EXPIRATION = 1800;
 
export interface PresignUploadResult {
  imageId: string;
  uploadUrl: string;
}
 
export class ImageService {
  private storageService: IImageStorageService;
  private imageRepository: IImageRepository;
 
  constructor(
    storageService?: IImageStorageService,
    imageRepository?: IImageRepository
  ) {
    this.storageService = storageService || new ImageStorageService();
    this.imageRepository = imageRepository || new ImageRepository();
  }
 
  /**
   * Generate a presigned S3 upload URL for image upload
   * @param originalName - Original filename provided by user
   * @param contentType - MIME type of the image (e.g., image/jpeg)
   * @returns Object with imageId and presigned upload URL
   * @throws ValidationError if contentType is not supported
   */
  async generatePresignedUpload(
    originalName: string,
    contentType: string
  ): Promise<PresignUploadResult> {
    logger.info('generatePresignedUpload called', {
      fileName: originalName,
      fileType: contentType,
    });
 
    // Validate content type
    Iif (!ALLOWED_IMAGE_TYPES.includes(contentType)) {
      throw new ValidationError(`Unsupported content type: ${contentType}`);
    }
 
    const imageId = uuidv4();
 
    // Generate presigned URL (5 min expiry)
    const uploadUrl = await this.storageService.getPresignedUploadUrl(
      imageId,
      contentType,
      PRESIGNED_URL_EXPIRATION
    );
 
    // Save initial metadata (status: pending upload)
    const metadata: ImageMetadata = {
      imageId,
      originalName,
      contentType,
      size: 0, // Unknown until upload completes
      url: '', // Public S3 URL will be set after upload
      uploadedAt: new Date().toISOString(),
      status: 'pending',
    };
 
    await this.imageRepository.save(metadata);
 
    logger.info(`Presigned upload URL generated for: ${imageId}`);
 
    return {
      imageId,
      uploadUrl,
    };
  }
 
  /**
   * Retrieve image metadata by ID
   * @param imageId - Unique image identifier
   * @returns Image metadata if found, null otherwise
   */
  async getImageMetaData(imageId: string): Promise<ImageMetadata | null> {
    logger.info('getImageMetaData called', { imageId });
    // Check metadata exists
    const metadata = await this.imageRepository.findById(imageId);
    if (!metadata) {
      return null;
    }
 
    // Retrieve from S3
    // const image = await this.storageService.getImage(imageId);
 
    logger.info(`getImageMetaData retrieved successfully: ${imageId}`);
    return metadata;
  }
 
  /**
   * Delete an image from S3 and its metadata from DynamoDB
   * @param imageId - Unique image identifier
   * @returns true if image was found and deleted, false if not found
   */
  async deleteImage(imageId: string): Promise<boolean> {
    logger.info('deleteImage called', { imageId });
    // Check metadata exists
    const metadata = await this.imageRepository.findById(imageId);
    if (!metadata) {
      logger.warn('deleteImage - image not found', { imageId });
      return false;
    }
 
    // Delete from S3
    await this.storageService.deleteImage(imageId);
 
    // Delete metadata from DynamoDB
    await this.imageRepository.delete(imageId);
 
    logger.info(`Image deleted successfully: ${imageId}`);
    return true;
  }
}