Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { RequestWithUser } from '../../auth/auth.middleware';
import { Readable } from 'stream';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { ConfigService } from '@nestjs/config';
import { ProcessingStatus } from '../../reports/models/report.model';

@Controller('document-processor')
export class DocumentProcessorController {
Expand All @@ -38,7 +39,7 @@ export class DocumentProcessorController {

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async processDocument(
async uploadAndProcessReport(
@UploadedFile() file: Express.Multer.File,
@Body('userId') userId: string,
@Body('debug') debug?: string,
Expand Down Expand Up @@ -114,12 +115,12 @@ export class DocumentProcessorController {
}

@Post('process-file')
async processFileFromPath(
@Body('filePath') filePath: string,
async processReport(
@Body('reportId') reportId: string,
@Req() request: RequestWithUser,
): Promise<ProcessedDocumentResult | any> {
if (!filePath) {
throw new BadRequestException('No filePath provided');
): Promise<any> {
if (!reportId) {
throw new BadRequestException('No reportId provided');
}

// Extract userId from the request (attached by auth middleware)
Expand All @@ -128,25 +129,74 @@ export class DocumentProcessorController {
throw new UnauthorizedException('User ID not found in request');
}

this.logger.log(`Processing document from file path: ${filePath}`);
this.logger.log(`Queueing document for processing, report ID: ${reportId}`);

try {
// Fetch the associated report record from DynamoDB
const report = await this.reportsService.findByFilePath(filePath, userId);
// Fetch the associated report record from DynamoDB using findOne method
const report = await this.reportsService.findOne(reportId, userId);
if (!report) {
throw new NotFoundException(`Report with filePath ${filePath} not found`);
throw new NotFoundException(`Report with ID ${reportId} not found`);
}

// Make sure we have a filePath to retrieve the file
if (!report.filePath) {
throw new BadRequestException(`Report with ID ${reportId} has no associated file`);
}

// Update report status to IN_PROGRESS before starting async processing
report.processingStatus = ProcessingStatus.IN_PROGRESS;
report.updatedAt = new Date().toISOString();
await this.reportsService.updateReport(report);

// Start async processing in background
this.processReportAsync(reportId, userId, report.filePath).catch(error => {
this.logger.error(`Async processing failed for report ${reportId}: ${error.message}`);
});

return {
success: true,
reportId: report.id,
status: ProcessingStatus.IN_PROGRESS,
message: 'Document processing started. Check the report status to know when it completes.',
};
} catch (error: unknown) {
this.logger.error(
`Error queueing document for report ID ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
throw error;
}
}

/**
* Processes a report file asynchronously
* @param reportId - ID of the report to process
* @param userId - ID of the user who owns the report
* @param filePath - S3 path to the file
*/
private async processReportAsync(
reportId: string,
userId: string,
filePath: string,
): Promise<void> {
try {
this.logger.log(`Started async processing for report: ${reportId}`);

// Get the file from S3
const fileBuffer = await this.getFileFromS3(filePath);

// Process the document
const result = await this.documentProcessorService.processDocument(fileBuffer, userId);

// Fetch the report again to ensure we have the latest version
const report = await this.reportsService.findOne(reportId, userId);
if (!report) {
throw new Error(`Report ${reportId} not found during async processing`);
}

// Update the report with analysis results
report.title = result.analysis.title || 'Untitled Report';
report.category = result.analysis.category || 'general';
report.isProcessed = true;
report.processingStatus = ProcessingStatus.PROCESSED;

// Extract lab values
report.labValues = result.analysis.labValues || [];
Expand All @@ -162,13 +212,26 @@ export class DocumentProcessorController {
// Update the report in DynamoDB
await this.reportsService.updateReport(report);

return {
success: true,
reportId: report.id,
};
} catch (error: unknown) {
this.logger.log(`Completed async processing for report: ${reportId}`);
} catch (error) {
// If processing fails, update the report status to indicate failure
try {
const report = await this.reportsService.findOne(reportId, userId);
if (report) {
report.processingStatus = ProcessingStatus.FAILED;
report.updatedAt = new Date().toISOString();
await this.reportsService.updateReport(report);
}
} catch (updateError: unknown) {
this.logger.error(
`Failed to update report status after processing error: ${
updateError instanceof Error ? updateError.message : 'Unknown error'
}`,
);
}

this.logger.error(
`Error processing document from path ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`,
`Error during async processing for report ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
throw error;
}
Expand Down Expand Up @@ -606,4 +669,39 @@ export class DocumentProcessorController {

res.type('text/html').send(html);
}

@Get('report-status/:reportId')
async getReportStatus(@Req() request: RequestWithUser): Promise<any> {
// Get reportId from path parameter
const reportId = request.params.reportId;

if (!reportId) {
throw new BadRequestException('No reportId provided');
}

// Extract userId from the request (attached by auth middleware)
const userId = request.user?.sub;
if (!userId) {
throw new UnauthorizedException('User ID not found in request');
}

try {
// Fetch the associated report record from DynamoDB
const report = await this.reportsService.findOne(reportId, userId);
if (!report) {
throw new NotFoundException(`Report with ID ${reportId} not found`);
}

return {
reportId: report.id,
status: report.processingStatus,
isComplete: report.processingStatus === ProcessingStatus.PROCESSED,
};
} catch (error: unknown) {
this.logger.error(
`Error fetching report status for ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
throw error;
}
}
}
1 change: 0 additions & 1 deletion backend/src/iac/backend-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,6 @@ export class BackendStack extends cdk.Stack {
const integrationOptions = {
connectionType: apigateway.ConnectionType.VPC_LINK,
vpcLink: vpcLink,
timeout: cdk.Duration.seconds(300), // Adding 5-minute timeout (300 seconds)
};

const getDocsIntegration = new apigateway.Integration({
Expand Down
15 changes: 13 additions & 2 deletions backend/src/reports/models/report.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export enum ReportStatus {
READ = 'READ',
}

export enum ProcessingStatus {
PROCESSED = 'processed',
UNPROCESSED = 'unprocessed',
IN_PROGRESS = 'in_progress',
FAILED = 'failed',
}

export class Report {
@ApiProperty({ description: 'Unique identifier for the report' })
id: string;
Expand All @@ -21,8 +28,12 @@ export class Report {
@ApiProperty({ description: 'Category of the report' })
category: string;

@ApiProperty({ description: 'Whether the report has been processed' })
isProcessed: boolean;
@ApiProperty({
description: 'Processing status of the report',
enum: ProcessingStatus,
default: ProcessingStatus.UNPROCESSED,
})
processingStatus: ProcessingStatus;

@ApiProperty({ description: 'List of lab values' })
labValues: Array<{
Expand Down
104 changes: 2 additions & 102 deletions backend/src/reports/reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
QueryCommand,
} from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
import { Report, ReportStatus } from './models/report.model';
import { Report, ReportStatus, ProcessingStatus } from './models/report.model';
import { GetReportsQueryDto } from './dto/get-reports.dto';
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
import { v4 as uuidv4 } from 'uuid';
Expand Down Expand Up @@ -299,7 +299,7 @@ export class ReportsService {
title: 'New Report',
bookmarked: false,
category: '',
isProcessed: false,
processingStatus: ProcessingStatus.UNPROCESSED,
labValues: [],
summary: '',
status: ReportStatus.UNREAD,
Expand Down Expand Up @@ -333,106 +333,6 @@ export class ReportsService {
}
}

/**
* Find a report by its filePath
* @param filePath The S3 path of the file
* @param userId User ID for authorization
* @returns Report record if found
*/
async findByFilePath(filePath: string, userId: string): Promise<Report | null> {
if (!filePath) {
throw new NotFoundException('File path is required');
}

if (!userId) {
throw new ForbiddenException('User ID is required');
}

// Log the actual filePath being searched for debugging
this.logger.log(`Searching for report with filePath: "${filePath}" for user ${userId}`);

try {
const command = new QueryCommand({
TableName: this.tableName,
KeyConditionExpression: 'userId = :userId',
FilterExpression: 'filePath = :filePath',
ExpressionAttributeValues: marshall({
':userId': userId,
':filePath': filePath,
}),
Limit: 1, // We only want one record
});

this.logger.log('Executing QueryCommand with params:', {
TableName: this.tableName,
KeyConditionExpression: 'userId = :userId',
FilterExpression: 'filePath = :filePath',
Values: {
userId,
filePath,
},
});

const response = await this.dynamoClient.send(command);

this.logger.log(`Query response received, found ${response.Items?.length || 0} items`);

if (!response.Items || response.Items.length === 0) {
// If no exact match, try with case-insensitive comparison as a fallback
this.logger.log('No exact match found, trying with case-insensitive search');

// Get all items for the user and filter manually for case-insensitive match
const allUserItemsCommand = new QueryCommand({
TableName: this.tableName,
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: marshall({
':userId': userId,
}),
});

const allUserResponse = await this.dynamoClient.send(allUserItemsCommand);

if (!allUserResponse.Items || allUserResponse.Items.length === 0) {
return null;
}

// Convert items and find case-insensitive match
const allReports = allUserResponse.Items.map(item => unmarshall(item) as Report);
const matchingReport = allReports.find(
report => report.filePath.toLowerCase() === filePath.toLowerCase(),
);

if (matchingReport) {
this.logger.log(
`Found case-insensitive match for ${filePath}: ${matchingReport.filePath}`,
);

return matchingReport;
}

return null;
}

const result = unmarshall(response.Items[0]) as Report;
this.logger.log(`Found report with ID ${result.id}`);

return result;
} catch (error: unknown) {
this.logger.error(`Error finding report with filePath ${filePath}:`);
this.logger.error(error);

if (error instanceof DynamoDBServiceException) {
if (error.name === 'ResourceNotFoundException') {
throw new InternalServerErrorException(
`Table "${this.tableName}" not found. Please check your database configuration.`,
);
}
}

throw new InternalServerErrorException(`Failed to fetch report with filePath ${filePath}`);
}
}

/**
* Update a report with new data
* @param report Updated report object
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/common/api/reportService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios, { AxiosProgressEvent } from 'axios';
import { MedicalReport, ReportCategory, ReportStatus } from '../models/medicalReport';
import { MedicalReport, ReportCategory, ReportStatus, ProcessingStatus } from '../models/medicalReport';
import { fetchAuthSession } from '@aws-amplify/auth';
// Get the API URL from environment variables
const API_URL = import.meta.env.VITE_BASE_URL_API || '';
Expand All @@ -12,7 +12,7 @@ const mockReports: MedicalReport[] = [
title: 'Blood Test Report',
category: ReportCategory.GENERAL,
bookmarked: false,
isProcessed: true,
processingStatus: ProcessingStatus.PROCESSED,
labValues: [],
summary: 'Blood test results within normal range',
status: ReportStatus.UNREAD,
Expand All @@ -26,7 +26,7 @@ const mockReports: MedicalReport[] = [
title: 'Heart Checkup',
category: ReportCategory.HEART,
bookmarked: true,
isProcessed: true,
processingStatus: ProcessingStatus.PROCESSED,
labValues: [],
summary: 'Heart functioning normally',
status: ReportStatus.READ,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/common/components/Upload/UploadModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ const UploadModal = ({ isOpen, onClose, onUploadComplete }: UploadModalProps): J
if (onUploadComplete) {
onUploadComplete(result);
}
// Navigate to the processing tab with filePath in state
// Navigate to the processing tab with reportId in state
if (file) {
history.push('/tabs/processing', {
filePath: result.filePath,
reportId: result.id,
});
} else {
history.push('/tabs/processing');
Expand Down
Loading