feat(db): implement complete database schema and models
- 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>
This commit is contained in:
parent
90016254a9
commit
e7e09d5e2c
15 changed files with 3606 additions and 0 deletions
349
packages/api/src/images/image.entity.ts
Normal file
349
packages/api/src/images/image.entity.ts
Normal file
|
@ -0,0 +1,349 @@
|
|||
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}`;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue