Skip to content

Commit af2fd2f

Browse files
authored
Merge pull request #83 from ModusCreateOrg/ADE-66
[ADE-66] process reports asynchronously
2 parents 5efa492 + dacaa75 commit af2fd2f

File tree

9 files changed

+212
-143
lines changed

9 files changed

+212
-143
lines changed

backend/src/document-processor/controllers/document-processor.controller.ts

Lines changed: 115 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { RequestWithUser } from '../../auth/auth.middleware';
2525
import { Readable } from 'stream';
2626
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
2727
import { ConfigService } from '@nestjs/config';
28+
import { ProcessingStatus } from '../../reports/models/report.model';
2829

2930
@Controller('document-processor')
3031
export class DocumentProcessorController {
@@ -38,7 +39,7 @@ export class DocumentProcessorController {
3839

3940
@Post('upload')
4041
@UseInterceptors(FileInterceptor('file'))
41-
async processDocument(
42+
async uploadAndProcessReport(
4243
@UploadedFile() file: Express.Multer.File,
4344
@Body('userId') userId: string,
4445
@Body('debug') debug?: string,
@@ -114,12 +115,12 @@ export class DocumentProcessorController {
114115
}
115116

116117
@Post('process-file')
117-
async processFileFromPath(
118-
@Body('filePath') filePath: string,
118+
async processReport(
119+
@Body('reportId') reportId: string,
119120
@Req() request: RequestWithUser,
120-
): Promise<ProcessedDocumentResult | any> {
121-
if (!filePath) {
122-
throw new BadRequestException('No filePath provided');
121+
): Promise<any> {
122+
if (!reportId) {
123+
throw new BadRequestException('No reportId provided');
123124
}
124125

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

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

133134
try {
134-
// Fetch the associated report record from DynamoDB
135-
const report = await this.reportsService.findByFilePath(filePath, userId);
135+
// Fetch the associated report record from DynamoDB using findOne method
136+
const report = await this.reportsService.findOne(reportId, userId);
136137
if (!report) {
137-
throw new NotFoundException(`Report with filePath ${filePath} not found`);
138+
throw new NotFoundException(`Report with ID ${reportId} not found`);
139+
}
140+
141+
// Make sure we have a filePath to retrieve the file
142+
if (!report.filePath) {
143+
throw new BadRequestException(`Report with ID ${reportId} has no associated file`);
138144
}
139145

146+
// Update report status to IN_PROGRESS before starting async processing
147+
report.processingStatus = ProcessingStatus.IN_PROGRESS;
148+
report.updatedAt = new Date().toISOString();
149+
await this.reportsService.updateReport(report);
150+
151+
// Start async processing in background
152+
this.processReportAsync(reportId, userId, report.filePath).catch(error => {
153+
this.logger.error(`Async processing failed for report ${reportId}: ${error.message}`);
154+
});
155+
156+
return {
157+
success: true,
158+
reportId: report.id,
159+
status: ProcessingStatus.IN_PROGRESS,
160+
message: 'Document processing started. Check the report status to know when it completes.',
161+
};
162+
} catch (error: unknown) {
163+
this.logger.error(
164+
`Error queueing document for report ID ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
165+
);
166+
throw error;
167+
}
168+
}
169+
170+
/**
171+
* Processes a report file asynchronously
172+
* @param reportId - ID of the report to process
173+
* @param userId - ID of the user who owns the report
174+
* @param filePath - S3 path to the file
175+
*/
176+
private async processReportAsync(
177+
reportId: string,
178+
userId: string,
179+
filePath: string,
180+
): Promise<void> {
181+
try {
182+
this.logger.log(`Started async processing for report: ${reportId}`);
183+
140184
// Get the file from S3
141185
const fileBuffer = await this.getFileFromS3(filePath);
142186

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

190+
// Fetch the report again to ensure we have the latest version
191+
const report = await this.reportsService.findOne(reportId, userId);
192+
if (!report) {
193+
throw new Error(`Report ${reportId} not found during async processing`);
194+
}
195+
146196
// Update the report with analysis results
147197
report.title = result.analysis.title || 'Untitled Report';
148198
report.category = result.analysis.category || 'general';
149-
report.isProcessed = true;
199+
report.processingStatus = ProcessingStatus.PROCESSED;
150200

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

165-
return {
166-
success: true,
167-
reportId: report.id,
168-
};
169-
} catch (error: unknown) {
215+
this.logger.log(`Completed async processing for report: ${reportId}`);
216+
} catch (error) {
217+
// If processing fails, update the report status to indicate failure
218+
try {
219+
const report = await this.reportsService.findOne(reportId, userId);
220+
if (report) {
221+
report.processingStatus = ProcessingStatus.FAILED;
222+
report.updatedAt = new Date().toISOString();
223+
await this.reportsService.updateReport(report);
224+
}
225+
} catch (updateError: unknown) {
226+
this.logger.error(
227+
`Failed to update report status after processing error: ${
228+
updateError instanceof Error ? updateError.message : 'Unknown error'
229+
}`,
230+
);
231+
}
232+
170233
this.logger.error(
171-
`Error processing document from path ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`,
234+
`Error during async processing for report ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
172235
);
173236
throw error;
174237
}
@@ -606,4 +669,39 @@ export class DocumentProcessorController {
606669

607670
res.type('text/html').send(html);
608671
}
672+
673+
@Get('report-status/:reportId')
674+
async getReportStatus(@Req() request: RequestWithUser): Promise<any> {
675+
// Get reportId from path parameter
676+
const reportId = request.params.reportId;
677+
678+
if (!reportId) {
679+
throw new BadRequestException('No reportId provided');
680+
}
681+
682+
// Extract userId from the request (attached by auth middleware)
683+
const userId = request.user?.sub;
684+
if (!userId) {
685+
throw new UnauthorizedException('User ID not found in request');
686+
}
687+
688+
try {
689+
// Fetch the associated report record from DynamoDB
690+
const report = await this.reportsService.findOne(reportId, userId);
691+
if (!report) {
692+
throw new NotFoundException(`Report with ID ${reportId} not found`);
693+
}
694+
695+
return {
696+
reportId: report.id,
697+
status: report.processingStatus,
698+
isComplete: report.processingStatus === ProcessingStatus.PROCESSED,
699+
};
700+
} catch (error: unknown) {
701+
this.logger.error(
702+
`Error fetching report status for ${reportId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
703+
);
704+
throw error;
705+
}
706+
}
609707
}

backend/src/iac/backend-stack.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,6 @@ export class BackendStack extends cdk.Stack {
405405
const integrationOptions = {
406406
connectionType: apigateway.ConnectionType.VPC_LINK,
407407
vpcLink: vpcLink,
408-
timeout: cdk.Duration.seconds(300), // Adding 5-minute timeout (300 seconds)
409408
};
410409

411410
const getDocsIntegration = new apigateway.Integration({

backend/src/reports/models/report.model.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ export enum ReportStatus {
55
READ = 'READ',
66
}
77

8+
export enum ProcessingStatus {
9+
PROCESSED = 'processed',
10+
UNPROCESSED = 'unprocessed',
11+
IN_PROGRESS = 'in_progress',
12+
FAILED = 'failed',
13+
}
14+
815
export class Report {
916
@ApiProperty({ description: 'Unique identifier for the report' })
1017
id: string;
@@ -21,8 +28,12 @@ export class Report {
2128
@ApiProperty({ description: 'Category of the report' })
2229
category: string;
2330

24-
@ApiProperty({ description: 'Whether the report has been processed' })
25-
isProcessed: boolean;
31+
@ApiProperty({
32+
description: 'Processing status of the report',
33+
enum: ProcessingStatus,
34+
default: ProcessingStatus.UNPROCESSED,
35+
})
36+
processingStatus: ProcessingStatus;
2637

2738
@ApiProperty({ description: 'List of lab values' })
2839
labValues: Array<{

backend/src/reports/reports.service.ts

Lines changed: 2 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
QueryCommand,
1616
} from '@aws-sdk/client-dynamodb';
1717
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
18-
import { Report, ReportStatus } from './models/report.model';
18+
import { Report, ReportStatus, ProcessingStatus } from './models/report.model';
1919
import { GetReportsQueryDto } from './dto/get-reports.dto';
2020
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
2121
import { v4 as uuidv4 } from 'uuid';
@@ -299,7 +299,7 @@ export class ReportsService {
299299
title: 'New Report',
300300
bookmarked: false,
301301
category: '',
302-
isProcessed: false,
302+
processingStatus: ProcessingStatus.UNPROCESSED,
303303
labValues: [],
304304
summary: '',
305305
status: ReportStatus.UNREAD,
@@ -333,106 +333,6 @@ export class ReportsService {
333333
}
334334
}
335335

336-
/**
337-
* Find a report by its filePath
338-
* @param filePath The S3 path of the file
339-
* @param userId User ID for authorization
340-
* @returns Report record if found
341-
*/
342-
async findByFilePath(filePath: string, userId: string): Promise<Report | null> {
343-
if (!filePath) {
344-
throw new NotFoundException('File path is required');
345-
}
346-
347-
if (!userId) {
348-
throw new ForbiddenException('User ID is required');
349-
}
350-
351-
// Log the actual filePath being searched for debugging
352-
this.logger.log(`Searching for report with filePath: "${filePath}" for user ${userId}`);
353-
354-
try {
355-
const command = new QueryCommand({
356-
TableName: this.tableName,
357-
KeyConditionExpression: 'userId = :userId',
358-
FilterExpression: 'filePath = :filePath',
359-
ExpressionAttributeValues: marshall({
360-
':userId': userId,
361-
':filePath': filePath,
362-
}),
363-
Limit: 1, // We only want one record
364-
});
365-
366-
this.logger.log('Executing QueryCommand with params:', {
367-
TableName: this.tableName,
368-
KeyConditionExpression: 'userId = :userId',
369-
FilterExpression: 'filePath = :filePath',
370-
Values: {
371-
userId,
372-
filePath,
373-
},
374-
});
375-
376-
const response = await this.dynamoClient.send(command);
377-
378-
this.logger.log(`Query response received, found ${response.Items?.length || 0} items`);
379-
380-
if (!response.Items || response.Items.length === 0) {
381-
// If no exact match, try with case-insensitive comparison as a fallback
382-
this.logger.log('No exact match found, trying with case-insensitive search');
383-
384-
// Get all items for the user and filter manually for case-insensitive match
385-
const allUserItemsCommand = new QueryCommand({
386-
TableName: this.tableName,
387-
KeyConditionExpression: 'userId = :userId',
388-
ExpressionAttributeValues: marshall({
389-
':userId': userId,
390-
}),
391-
});
392-
393-
const allUserResponse = await this.dynamoClient.send(allUserItemsCommand);
394-
395-
if (!allUserResponse.Items || allUserResponse.Items.length === 0) {
396-
return null;
397-
}
398-
399-
// Convert items and find case-insensitive match
400-
const allReports = allUserResponse.Items.map(item => unmarshall(item) as Report);
401-
const matchingReport = allReports.find(
402-
report => report.filePath.toLowerCase() === filePath.toLowerCase(),
403-
);
404-
405-
if (matchingReport) {
406-
this.logger.log(
407-
`Found case-insensitive match for ${filePath}: ${matchingReport.filePath}`,
408-
);
409-
410-
return matchingReport;
411-
}
412-
413-
return null;
414-
}
415-
416-
const result = unmarshall(response.Items[0]) as Report;
417-
this.logger.log(`Found report with ID ${result.id}`);
418-
419-
return result;
420-
} catch (error: unknown) {
421-
this.logger.error(`Error finding report with filePath ${filePath}:`);
422-
this.logger.error(error);
423-
424-
if (error instanceof DynamoDBServiceException) {
425-
if (error.name === 'ResourceNotFoundException') {
426-
throw new InternalServerErrorException(
427-
`Table "${this.tableName}" not found. Please check your database configuration.`,
428-
);
429-
}
430-
}
431-
432-
throw new InternalServerErrorException(`Failed to fetch report with filePath ${filePath}`);
433-
}
434-
}
435-
436336
/**
437337
* Update a report with new data
438338
* @param report Updated report object

frontend/src/common/api/reportService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import axios, { AxiosProgressEvent } from 'axios';
2-
import { MedicalReport, ReportCategory, ReportStatus } from '../models/medicalReport';
2+
import { MedicalReport, ReportCategory, ReportStatus, ProcessingStatus } from '../models/medicalReport';
33
import { fetchAuthSession } from '@aws-amplify/auth';
44
// Get the API URL from environment variables
55
const API_URL = import.meta.env.VITE_BASE_URL_API || '';
@@ -12,7 +12,7 @@ const mockReports: MedicalReport[] = [
1212
title: 'Blood Test Report',
1313
category: ReportCategory.GENERAL,
1414
bookmarked: false,
15-
isProcessed: true,
15+
processingStatus: ProcessingStatus.PROCESSED,
1616
labValues: [],
1717
summary: 'Blood test results within normal range',
1818
status: ReportStatus.UNREAD,
@@ -26,7 +26,7 @@ const mockReports: MedicalReport[] = [
2626
title: 'Heart Checkup',
2727
category: ReportCategory.HEART,
2828
bookmarked: true,
29-
isProcessed: true,
29+
processingStatus: ProcessingStatus.PROCESSED,
3030
labValues: [],
3131
summary: 'Heart functioning normally',
3232
status: ReportStatus.READ,

frontend/src/common/components/Upload/UploadModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ const UploadModal = ({ isOpen, onClose, onUploadComplete }: UploadModalProps): J
6363
if (onUploadComplete) {
6464
onUploadComplete(result);
6565
}
66-
// Navigate to the processing tab with filePath in state
66+
// Navigate to the processing tab with reportId in state
6767
if (file) {
6868
history.push('/tabs/processing', {
69-
filePath: result.filePath,
69+
reportId: result.id,
7070
});
7171
} else {
7272
history.push('/tabs/processing');

0 commit comments

Comments
 (0)