Skip to content

Commit 03b6d4d

Browse files
committed
fix: Enhance report retrieval and deletion logic; update processing status checks and improve error handling
1 parent 5f5b72b commit 03b6d4d

File tree

4 files changed

+96
-27
lines changed

4 files changed

+96
-27
lines changed

backend/src/reports/reports.controller.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
ApiBody,
2121
} from '@nestjs/swagger';
2222
import { ReportsService } from './reports.service';
23-
import { Report } from './models/report.model';
23+
import { ProcessingStatus, Report } from './models/report.model';
2424
import { GetReportsQueryDto } from './dto/get-reports.dto';
2525
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
2626
import { RequestWithUser } from '../auth/auth.middleware';
@@ -80,7 +80,17 @@ export class ReportsController {
8080
@Get(':id')
8181
async getReport(@Param('id') id: string, @Req() request: RequestWithUser): Promise<Report> {
8282
const userId = this.extractUserId(request);
83-
return this.reportsService.findOne(id, userId);
83+
const report = await this.reportsService.findOne(id, userId);
84+
85+
if (!report) {
86+
throw new UnauthorizedException('Report not found');
87+
}
88+
89+
if (report.processingStatus === ProcessingStatus.FAILED) {
90+
throw new UnauthorizedException('Processing failed');
91+
}
92+
93+
return report;
8494
}
8595

8696
@ApiOperation({ summary: 'Update report status' })

backend/src/reports/reports.service.ts

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DynamoDBServiceException,
1414
PutItemCommand,
1515
QueryCommand,
16+
DeleteItemCommand,
1617
} from '@aws-sdk/client-dynamodb';
1718
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
1819
import { Report, ReportStatus, ProcessingStatus } from './models/report.model';
@@ -54,19 +55,24 @@ export class ReportsService {
5455
this.tableName = this.configService.get<string>('dynamodbReportsTable')!;
5556
}
5657

57-
async findAll(userId: string): Promise<Report[]> {
58+
async findAll(userId: string, withFailed = false): Promise<Report[]> {
5859
if (!userId) {
5960
throw new ForbiddenException('User ID is required');
6061
}
6162

6263
try {
63-
// Use QueryCommand instead of ScanCommand since userId is the partition key
64+
const expressionAttributeValues: any = { ':userId': userId };
65+
let filterExpression = '';
66+
67+
if (!withFailed) {
68+
filterExpression = ' AND processingStatus <> :failedStatus';
69+
expressionAttributeValues[':failedStatus'] = ProcessingStatus.FAILED;
70+
}
71+
6472
const command = new QueryCommand({
6573
TableName: this.tableName,
66-
KeyConditionExpression: 'userId = :userId',
67-
ExpressionAttributeValues: marshall({
68-
':userId': userId,
69-
}),
74+
KeyConditionExpression: 'userId = :userId' + filterExpression,
75+
ExpressionAttributeValues: marshall(expressionAttributeValues),
7076
});
7177

7278
const response = await this.dynamoClient.send(command);
@@ -91,7 +97,11 @@ export class ReportsService {
9197
}
9298
}
9399

94-
async findLatest(queryDto: GetReportsQueryDto, userId: string): Promise<Report[]> {
100+
async findLatest(
101+
queryDto: GetReportsQueryDto,
102+
userId: string,
103+
withFailed = false,
104+
): Promise<Report[]> {
95105
this.logger.log(
96106
`Running findLatest with params: ${JSON.stringify(queryDto)} for user ${userId}`,
97107
);
@@ -100,22 +110,26 @@ export class ReportsService {
100110
throw new ForbiddenException('User ID is required');
101111
}
102112

103-
// Convert limit to a number to avoid serialization errors
104113
const limit =
105114
typeof queryDto.limit === 'string' ? parseInt(queryDto.limit, 10) : queryDto.limit || 10;
106115

116+
const expressionAttributeValues: any = { ':userId': userId };
117+
107118
try {
108-
// Use the GSI userIdCreatedAtIndex with QueryCommand for efficient retrieval
109-
// This is much more efficient than a ScanCommand
119+
let filterExpression = '';
120+
121+
if (!withFailed) {
122+
filterExpression = ' AND processingStatus <> :failedStatus';
123+
expressionAttributeValues[':failedStatus'] = ProcessingStatus.FAILED;
124+
}
125+
110126
const command = new QueryCommand({
111127
TableName: this.tableName,
112-
IndexName: 'userIdCreatedAtIndex', // Use the GSI for efficient queries
113-
KeyConditionExpression: 'userId = :userId',
114-
ExpressionAttributeValues: marshall({
115-
':userId': userId,
116-
}),
117-
ScanIndexForward: false, // Get items in descending order (newest first)
118-
Limit: limit, // Only fetch the number of items we need
128+
IndexName: 'userIdCreatedAtIndex',
129+
KeyConditionExpression: 'userId = :userId' + filterExpression,
130+
ExpressionAttributeValues: marshall(expressionAttributeValues),
131+
ScanIndexForward: false,
132+
Limit: limit,
119133
});
120134

121135
const response = await this.dynamoClient.send(command);
@@ -130,22 +144,17 @@ export class ReportsService {
130144
`Table "${this.tableName}" not found. Please check your database configuration.`,
131145
);
132146
} else if (error.name === 'ValidationException') {
133-
// This could happen if the GSI doesn't exist
134147
this.logger.warn('GSI validation error, falling back to standard query');
135148

136-
// Fallback to standard query and sort in memory if GSI has issues
137149
const fallbackCommand = new QueryCommand({
138150
TableName: this.tableName,
139151
KeyConditionExpression: 'userId = :userId',
140-
ExpressionAttributeValues: marshall({
141-
':userId': userId,
142-
}),
152+
ExpressionAttributeValues: marshall(expressionAttributeValues),
143153
});
144154

145155
const fallbackResponse = await this.dynamoClient.send(fallbackCommand);
146156
const reports = (fallbackResponse.Items || []).map(item => unmarshall(item) as Report);
147157

148-
// Sort by createdAt in descending order
149158
return reports
150159
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
151160
.slice(0, limit);
@@ -469,4 +478,54 @@ export class ReportsService {
469478
throw new InternalServerErrorException(`Failed to toggle bookmark for report ID ${id}`);
470479
}
471480
}
481+
482+
/**
483+
* Delete a report by ID
484+
* @param reportId Report ID
485+
* @param userId User ID
486+
* @returns A confirmation message
487+
*/
488+
async deleteReport(reportId: string, userId: string): Promise<string> {
489+
if (!reportId) {
490+
throw new NotFoundException('Report ID is required');
491+
}
492+
493+
if (!userId) {
494+
throw new ForbiddenException('User ID is required');
495+
}
496+
497+
try {
498+
const command = new DeleteItemCommand({
499+
TableName: this.tableName,
500+
Key: marshall({
501+
userId,
502+
id: reportId,
503+
}),
504+
ConditionExpression: 'userId = :userId',
505+
ExpressionAttributeValues: marshall({
506+
':userId': userId,
507+
}),
508+
});
509+
510+
await this.dynamoClient.send(command);
511+
this.logger.log(`Successfully deleted report with ID ${reportId} for user ${userId}`);
512+
513+
return `Report with ID ${reportId} successfully deleted`;
514+
} catch (error: unknown) {
515+
this.logger.error(`Error deleting report with ID ${reportId}:`);
516+
this.logger.error(error);
517+
518+
if (error instanceof DynamoDBServiceException) {
519+
if (error.name === 'ConditionalCheckFailedException') {
520+
throw new ForbiddenException('You do not have permission to delete this report');
521+
} else if (error.name === 'ResourceNotFoundException') {
522+
throw new InternalServerErrorException(
523+
`Table "${this.tableName}" not found. Please check your database configuration.`,
524+
);
525+
}
526+
}
527+
528+
throw new InternalServerErrorException(`Failed to delete report with ID ${reportId}`);
529+
}
530+
}
472531
}

backend/src/services/perplexity.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export class PerplexityService {
158158
'Your goal is to help patients understand their medical reports by translating medical jargon into plain language.\n' +
159159
'You must be accurate, concise, comprehensive, and easy to understand. Use everyday analogies when helpful.\n';
160160

161-
const userPrompt = `Please explain the following medical text in simple terms, in a single paragraph that's between 100 to 500 characters:\n\n${medicalText}`;
161+
const userPrompt = `Please explain the following medical text in simple terms, in a single paragraph that's between 10 to 200 words, all in normal text NOT .md style, the more concise the better:\n\n${medicalText}`;
162162

163163
const messages: PerplexityMessage[] = [
164164
{ role: 'system', content: systemPrompt },

frontend/src/pages/Reports/components/ReportHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const ReportHeader: React.FC<ReportHeaderProps> = ({ reportData, onClose }) => {
2525
{/* Category & Title */}
2626
<div className="report-detail-page__category-wrapper">
2727
<span className="report-detail-page__category">
28-
{reportData.category && t(`list.${reportData.category}Category`, { ns: 'report' })}
28+
{reportData.category && t(`list.${reportData.category}Category`, { ns: 'reportDetail' })}
2929
</span>
3030
<button className="report-detail-page__bookmark-button">
3131
{reportData.bookmarked ? (

0 commit comments

Comments
 (0)