feat(api): add batches module for batch processing management
- Implement POST /api/batch endpoint for multipart file uploads
- Add GET /api/batch/{batchId}/status for real-time progress tracking
- Support batch cancellation, retry, and ZIP download generation
- Include comprehensive validation and quota checking
- Add progress broadcasting integration with WebSocket gateway
- Implement batch lifecycle management (create, process, complete)
Resolves requirements §29, §32, §73-§74 for batch processing API.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d54dd44cf9
commit
2add73a264
5 changed files with 1003 additions and 0 deletions
275
packages/api/src/batches/batches.controller.ts
Normal file
275
packages/api/src/batches/batches.controller.ts
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Param,
|
||||
Body,
|
||||
UploadedFiles,
|
||||
UseInterceptors,
|
||||
UseGuards,
|
||||
Request,
|
||||
HttpStatus,
|
||||
BadRequestException,
|
||||
PayloadTooLargeException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiConsumes, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/auth.guard';
|
||||
import { BatchesService } from './batches.service';
|
||||
import { CreateBatchDto, BatchUploadResponseDto } from './dto/create-batch.dto';
|
||||
import { BatchStatusResponseDto, BatchListResponseDto } from './dto/batch-status.dto';
|
||||
|
||||
@ApiTags('batches')
|
||||
@Controller('api/batch')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class BatchesController {
|
||||
constructor(private readonly batchesService: BatchesService) {}
|
||||
|
||||
@Post()
|
||||
@UseInterceptors(FilesInterceptor('files', 1000)) // Max 1000 files per batch
|
||||
@ApiOperation({
|
||||
summary: 'Upload batch of images for processing',
|
||||
description: 'Uploads multiple images and starts batch processing with AI analysis and SEO filename generation'
|
||||
})
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Batch created successfully',
|
||||
type: BatchUploadResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Invalid files or missing data',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.PAYLOAD_TOO_LARGE,
|
||||
description: 'File size or count exceeds limits',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.FORBIDDEN,
|
||||
description: 'Insufficient quota remaining',
|
||||
})
|
||||
async uploadBatch(
|
||||
@UploadedFiles() files: Express.Multer.File[],
|
||||
@Body() createBatchDto: CreateBatchDto,
|
||||
@Request() req: any,
|
||||
): Promise<BatchUploadResponseDto> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
// Validate files are provided
|
||||
if (!files || files.length === 0) {
|
||||
throw new BadRequestException('No files provided');
|
||||
}
|
||||
|
||||
// Check file count limits
|
||||
if (files.length > 1000) {
|
||||
throw new PayloadTooLargeException('Maximum 1000 files per batch');
|
||||
}
|
||||
|
||||
// Process the batch upload
|
||||
const result = await this.batchesService.createBatch(userId, files, createBatchDto);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException ||
|
||||
error instanceof PayloadTooLargeException ||
|
||||
error instanceof ForbiddenException) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to process batch upload');
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':batchId/status')
|
||||
@ApiOperation({
|
||||
summary: 'Get batch processing status',
|
||||
description: 'Returns current status and progress of batch processing'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Batch status retrieved successfully',
|
||||
type: BatchStatusResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Batch not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.FORBIDDEN,
|
||||
description: 'Not authorized to access this batch',
|
||||
})
|
||||
async getBatchStatus(
|
||||
@Param('batchId') batchId: string,
|
||||
@Request() req: any,
|
||||
): Promise<BatchStatusResponseDto> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const status = await this.batchesService.getBatchStatus(batchId, userId);
|
||||
return status;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException || error instanceof ForbiddenException) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to get batch status');
|
||||
}
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'List user batches',
|
||||
description: 'Returns list of all batches for the authenticated user'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Batches retrieved successfully',
|
||||
type: [BatchListResponseDto],
|
||||
})
|
||||
async getUserBatches(
|
||||
@Request() req: any,
|
||||
): Promise<BatchListResponseDto[]> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const batches = await this.batchesService.getUserBatches(userId);
|
||||
return batches;
|
||||
|
||||
} catch (error) {
|
||||
throw new BadRequestException('Failed to get user batches');
|
||||
}
|
||||
}
|
||||
|
||||
@Post(':batchId/cancel')
|
||||
@ApiOperation({
|
||||
summary: 'Cancel batch processing',
|
||||
description: 'Cancels ongoing batch processing'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Batch cancelled successfully',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Batch not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.FORBIDDEN,
|
||||
description: 'Not authorized to cancel this batch',
|
||||
})
|
||||
async cancelBatch(
|
||||
@Param('batchId') batchId: string,
|
||||
@Request() req: any,
|
||||
): Promise<{ message: string }> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
await this.batchesService.cancelBatch(batchId, userId);
|
||||
|
||||
return { message: 'Batch cancelled successfully' };
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException || error instanceof ForbiddenException) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to cancel batch');
|
||||
}
|
||||
}
|
||||
|
||||
@Post(':batchId/retry')
|
||||
@ApiOperation({
|
||||
summary: 'Retry failed batch processing',
|
||||
description: 'Retries processing for failed images in a batch'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Batch retry started successfully',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Batch not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Batch is not in a retryable state',
|
||||
})
|
||||
async retryBatch(
|
||||
@Param('batchId') batchId: string,
|
||||
@Request() req: any,
|
||||
): Promise<{ message: string; retry_count: number }> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const retryCount = await this.batchesService.retryBatch(batchId, userId);
|
||||
|
||||
return {
|
||||
message: 'Batch retry started successfully',
|
||||
retry_count: retryCount
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException || error instanceof ForbiddenException) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to retry batch');
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':batchId/download')
|
||||
@ApiOperation({
|
||||
summary: 'Download processed batch as ZIP',
|
||||
description: 'Returns a ZIP file containing all processed images with new filenames'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'ZIP file download started',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Batch not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Batch processing not completed',
|
||||
})
|
||||
async downloadBatch(
|
||||
@Param('batchId') batchId: string,
|
||||
@Request() req: any,
|
||||
): Promise<{ download_url: string; expires_at: string }> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const downloadInfo = await this.batchesService.generateBatchDownload(batchId, userId);
|
||||
|
||||
return downloadInfo;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException || error instanceof ForbiddenException) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to generate batch download');
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue