
- Add Prisma schema with PostgreSQL 15 support - Create Users, Batches, Images, Payments, ApiKeys tables - Implement proper foreign key relationships and indexes - Add enum types for status fields (Plan, BatchStatus, ImageStatus, PaymentStatus) - Support for JSON fields (vision_tags, metadata) - UUID primary keys for security - Created/updated timestamps with proper defaults Database Layer Components: - Prisma service with connection management and health checks - Repository pattern for all entities with comprehensive CRUD operations - TypeScript DTOs with class-validator decorations - Swagger API documentation annotations - Helper functions for business logic (quota management, pricing, etc.) Development Support: - Environment variables template - Database seed script with realistic test data - TypeScript configuration optimized for Nest.js - Package.json with all required dependencies Resolves database requirements from issues §78-81 establishing the complete data layer foundation for the AI Bulk Image Renamer SaaS. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
349 lines
No EOL
7.8 KiB
TypeScript
349 lines
No EOL
7.8 KiB
TypeScript
import {
|
|
IsString,
|
|
IsEnum,
|
|
IsInt,
|
|
IsOptional,
|
|
IsUUID,
|
|
IsObject,
|
|
MinLength,
|
|
MaxLength,
|
|
Min,
|
|
IsDate
|
|
} from 'class-validator';
|
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
import { ImageStatus } from '@prisma/client';
|
|
import { Type } from 'class-transformer';
|
|
|
|
export interface VisionTagsInterface {
|
|
objects?: string[];
|
|
colors?: string[];
|
|
scene?: string;
|
|
description?: string;
|
|
confidence?: number;
|
|
aiModel?: string;
|
|
processingTime?: number;
|
|
}
|
|
|
|
export interface ImageDimensionsInterface {
|
|
width: number;
|
|
height: number;
|
|
aspectRatio?: string;
|
|
}
|
|
|
|
export class CreateImageDto {
|
|
@ApiProperty({
|
|
description: 'ID of the batch this image belongs to',
|
|
example: '550e8400-e29b-41d4-a716-446655440000'
|
|
})
|
|
@IsUUID()
|
|
batchId: string;
|
|
|
|
@ApiProperty({
|
|
description: 'Original filename of the image',
|
|
example: 'IMG_20240101_123456.jpg'
|
|
})
|
|
@IsString()
|
|
@MinLength(1)
|
|
@MaxLength(255)
|
|
originalName: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'File size in bytes',
|
|
example: 2048576
|
|
})
|
|
@IsOptional()
|
|
@IsInt()
|
|
@Min(0)
|
|
fileSize?: number;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'MIME type of the image',
|
|
example: 'image/jpeg'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
mimeType?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Image dimensions',
|
|
example: { width: 1920, height: 1080, aspectRatio: '16:9' }
|
|
})
|
|
@IsOptional()
|
|
@IsObject()
|
|
dimensions?: ImageDimensionsInterface;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'S3 object key for storage',
|
|
example: 'uploads/user123/batch456/original/image.jpg'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
s3Key?: string;
|
|
}
|
|
|
|
export class UpdateImageDto {
|
|
@ApiPropertyOptional({
|
|
description: 'AI-generated proposed filename',
|
|
example: 'modern-kitchen-with-stainless-steel-appliances.jpg'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(255)
|
|
proposedName?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'User-approved final filename',
|
|
example: 'kitchen-renovation-final.jpg'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(255)
|
|
finalName?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'AI vision analysis results',
|
|
example: {
|
|
objects: ['kitchen', 'refrigerator', 'countertop'],
|
|
colors: ['white', 'stainless steel', 'black'],
|
|
scene: 'modern kitchen interior',
|
|
description: 'A modern kitchen with stainless steel appliances',
|
|
confidence: 0.95,
|
|
aiModel: 'gpt-4-vision',
|
|
processingTime: 2.5
|
|
}
|
|
})
|
|
@IsOptional()
|
|
@IsObject()
|
|
visionTags?: VisionTagsInterface;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Image processing status',
|
|
enum: ImageStatus
|
|
})
|
|
@IsOptional()
|
|
@IsEnum(ImageStatus)
|
|
status?: ImageStatus;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Error message if processing failed',
|
|
example: 'Image format not supported'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
@MaxLength(500)
|
|
processingError?: string;
|
|
}
|
|
|
|
export class ImageResponseDto {
|
|
@ApiProperty({
|
|
description: 'Unique image identifier',
|
|
example: '550e8400-e29b-41d4-a716-446655440000'
|
|
})
|
|
@IsUUID()
|
|
id: string;
|
|
|
|
@ApiProperty({
|
|
description: 'ID of the batch this image belongs to',
|
|
example: '550e8400-e29b-41d4-a716-446655440000'
|
|
})
|
|
@IsUUID()
|
|
batchId: string;
|
|
|
|
@ApiProperty({
|
|
description: 'Original filename of the image',
|
|
example: 'IMG_20240101_123456.jpg'
|
|
})
|
|
@IsString()
|
|
originalName: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'AI-generated proposed filename',
|
|
example: 'modern-kitchen-with-stainless-steel-appliances.jpg'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
proposedName?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'User-approved final filename',
|
|
example: 'kitchen-renovation-final.jpg'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
finalName?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'AI vision analysis results'
|
|
})
|
|
@IsOptional()
|
|
@IsObject()
|
|
visionTags?: VisionTagsInterface;
|
|
|
|
@ApiProperty({
|
|
description: 'Current image processing status',
|
|
enum: ImageStatus
|
|
})
|
|
@IsEnum(ImageStatus)
|
|
status: ImageStatus;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'File size in bytes',
|
|
example: 2048576
|
|
})
|
|
@IsOptional()
|
|
@IsInt()
|
|
fileSize?: number;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Image dimensions'
|
|
})
|
|
@IsOptional()
|
|
@IsObject()
|
|
dimensions?: ImageDimensionsInterface;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'MIME type of the image',
|
|
example: 'image/jpeg'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
mimeType?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'S3 object key for storage',
|
|
example: 'uploads/user123/batch456/original/image.jpg'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
s3Key?: string;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Error message if processing failed'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
processingError?: string;
|
|
|
|
@ApiProperty({
|
|
description: 'Image creation timestamp'
|
|
})
|
|
@IsDate()
|
|
createdAt: Date;
|
|
|
|
@ApiProperty({
|
|
description: 'Image last update timestamp'
|
|
})
|
|
@IsDate()
|
|
updatedAt: Date;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Image processing completion timestamp'
|
|
})
|
|
@IsOptional()
|
|
@IsDate()
|
|
processedAt?: Date;
|
|
}
|
|
|
|
export class ImageProcessingResultDto {
|
|
@ApiProperty({
|
|
description: 'Image details'
|
|
})
|
|
image: ImageResponseDto;
|
|
|
|
@ApiProperty({
|
|
description: 'Processing success status'
|
|
})
|
|
success: boolean;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Processing time in seconds'
|
|
})
|
|
@IsOptional()
|
|
@Type(() => Number)
|
|
processingTime?: number;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Error details if processing failed'
|
|
})
|
|
@IsOptional()
|
|
@IsString()
|
|
error?: string;
|
|
}
|
|
|
|
export class BulkImageUpdateDto {
|
|
@ApiProperty({
|
|
description: 'Array of image IDs to update',
|
|
example: ['550e8400-e29b-41d4-a716-446655440000', '660f9511-f39c-52e5-b827-557766551111']
|
|
})
|
|
@IsUUID(undefined, { each: true })
|
|
imageIds: string[];
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Status to set for all images',
|
|
enum: ImageStatus
|
|
})
|
|
@IsOptional()
|
|
@IsEnum(ImageStatus)
|
|
status?: ImageStatus;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Apply proposed names as final names for all images'
|
|
})
|
|
@IsOptional()
|
|
applyProposedNames?: boolean;
|
|
}
|
|
|
|
// Helper function to generate SEO-friendly filename
|
|
export function generateSeoFriendlyFilename(
|
|
visionTags: VisionTagsInterface,
|
|
originalName: string
|
|
): string {
|
|
if (!visionTags.objects && !visionTags.description) {
|
|
return originalName;
|
|
}
|
|
|
|
let filename = '';
|
|
|
|
// Use description if available, otherwise use objects
|
|
if (visionTags.description) {
|
|
filename = visionTags.description
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
|
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
|
.substring(0, 100); // Limit length
|
|
} else if (visionTags.objects && visionTags.objects.length > 0) {
|
|
filename = visionTags.objects
|
|
.slice(0, 3) // Take first 3 objects
|
|
.join('-')
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9-]/g, '')
|
|
.substring(0, 100);
|
|
}
|
|
|
|
// Get file extension from original name
|
|
const extension = originalName.split('.').pop()?.toLowerCase() || 'jpg';
|
|
|
|
return filename ? `${filename}.${extension}` : originalName;
|
|
}
|
|
|
|
// Helper function to validate image file type
|
|
export function isValidImageType(mimeType: string): boolean {
|
|
const validTypes = [
|
|
'image/jpeg',
|
|
'image/jpg',
|
|
'image/png',
|
|
'image/webp',
|
|
'image/gif',
|
|
'image/bmp',
|
|
'image/tiff'
|
|
];
|
|
return validTypes.includes(mimeType.toLowerCase());
|
|
}
|
|
|
|
// Helper function to calculate aspect ratio
|
|
export function calculateAspectRatio(width: number, height: number): string {
|
|
const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b);
|
|
const divisor = gcd(width, height);
|
|
return `${width / divisor}:${height / divisor}`;
|
|
} |