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:
DustyWalker 2025-08-05 17:02:03 +02:00
parent 90016254a9
commit e7e09d5e2c
15 changed files with 3606 additions and 0 deletions

View 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}`;
}