From 5eeedea063e019b152acca772742ca8114ecfa16 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Fri, 18 Apr 2025 09:08:51 -0400 Subject: [PATCH 1/2] Add bookmark endpoin --- backend/src/iac/backend-stack.ts | 27 +++++++++ backend/src/reports/reports.controller.ts | 36 +++++++++++ backend/src/reports/reports.service.ts | 74 +++++++++++++++++++++++ frontend/src/common/api/reportService.ts | 51 +--------------- 4 files changed, 140 insertions(+), 48 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 1d43cdfd..75038140 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -397,6 +397,9 @@ export class BackendStack extends cdk.Stack { // Create the 'status' resource under ':id' const reportStatusResource = reportIdResource.addResource('status'); + // Create the 'bookmark' resource under ':id' + const reportBookmarkResource = reportIdResource.addResource('bookmark'); + // Create the 'status' resource under ':id' const documentProcessorResource = apiResource.addResource('document-processor'); const processFileResource = documentProcessorResource.addResource('process-file'); @@ -460,6 +463,18 @@ export class BackendStack extends cdk.Stack { }, }); + const patchReportBookmarkIntegration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'PATCH', + uri: `${serviceUrl}/api/reports/{id}/bookmark`, + options: { + ...integrationOptions, + requestParameters: { + 'integration.request.path.id': 'method.request.path.id', + }, + }, + }); + const processFileIntegration = new apigateway.Integration({ type: apigateway.IntegrationType.HTTP_PROXY, integrationHttpMethod: 'POST', @@ -493,6 +508,14 @@ export class BackendStack extends cdk.Stack { }, }); + // Add method for bookmark endpoint + reportBookmarkResource.addMethod('PATCH', patchReportBookmarkIntegration, { + ...methodOptions, + requestParameters: { + 'method.request.path.id': true, + }, + }); + // Add POST method to process file processFileResource.addMethod('POST', processFileIntegration, methodOptions); @@ -526,6 +549,10 @@ export class BackendStack extends cdk.Stack { ...corsOptions, allowCredentials: false, }); + reportBookmarkResource.addCorsPreflight({ + ...corsOptions, + allowCredentials: false, + }); docsResource.addCorsPreflight({ ...corsOptions, allowCredentials: false, diff --git a/backend/src/reports/reports.controller.ts b/backend/src/reports/reports.controller.ts index 00b8adea..f4132029 100644 --- a/backend/src/reports/reports.controller.ts +++ b/backend/src/reports/reports.controller.ts @@ -107,6 +107,42 @@ export class ReportsController { return this.reportsService.updateStatus(id, updateDto, userId); } + @ApiOperation({ summary: 'Toggle report bookmark status' }) + @ApiResponse({ + status: 200, + description: 'Report bookmark status toggled successfully', + type: Report, + }) + @ApiResponse({ + status: 404, + description: 'Report not found', + }) + @ApiParam({ + name: 'id', + description: 'Report ID', + }) + @ApiBody({ + schema: { + type: 'object', + properties: { + bookmarked: { + type: 'boolean', + description: 'New bookmark status', + }, + }, + required: ['bookmarked'], + }, + }) + @Patch(':id/bookmark') + async toggleBookmark( + @Param('id') id: string, + @Body('bookmarked') bookmarked: boolean, + @Req() request: RequestWithUser, + ): Promise { + const userId = this.extractUserId(request); + return this.reportsService.toggleBookmark(id, bookmarked, userId); + } + @ApiOperation({ summary: 'Create a new report from S3 file' }) @ApiResponse({ status: 201, diff --git a/backend/src/reports/reports.service.ts b/backend/src/reports/reports.service.ts index 20ad1db1..f497b6c0 100644 --- a/backend/src/reports/reports.service.ts +++ b/backend/src/reports/reports.service.ts @@ -379,4 +379,78 @@ export class ReportsService { throw new InternalServerErrorException(`Failed to update report with ID ${report.id}`); } } + + /** + * Toggle the bookmark status of a report + * @param id Report ID + * @param bookmarked New bookmark status + * @param userId User ID + * @returns The updated report + */ + async toggleBookmark(id: string, bookmarked: boolean, userId: string): Promise { + if (!id) { + throw new NotFoundException('Report ID is required'); + } + + if (bookmarked === undefined) { + throw new InternalServerErrorException('Bookmark status is required'); + } + + if (!userId) { + throw new ForbiddenException('User ID is required'); + } + + try { + // First check if the report exists and belongs to the user + const existingReport = await this.findOne(id, userId); + + const command = new UpdateItemCommand({ + TableName: this.tableName, + Key: marshall({ + userId, + id, + }), + UpdateExpression: 'SET bookmarked = :bookmarked, updatedAt = :updatedAt', + ConditionExpression: 'userId = :userId', // Ensure the report belongs to the user + ExpressionAttributeValues: marshall({ + ':bookmarked': bookmarked, + ':updatedAt': new Date().toISOString(), + ':userId': userId, + }), + ReturnValues: 'ALL_NEW', + }); + + const response = await this.dynamoClient.send(command); + + if (!response.Attributes) { + // If for some reason Attributes is undefined, return the existing report with updated bookmark status + return { + ...existingReport, + bookmarked, + updatedAt: new Date().toISOString(), + }; + } + + return unmarshall(response.Attributes) as Report; + } catch (error: unknown) { + if (error instanceof NotFoundException) { + throw error; + } + + this.logger.error(`Error toggling bookmark for report ID ${id}:`); + this.logger.error(error); + + if (error instanceof DynamoDBServiceException) { + if (error.name === 'ConditionalCheckFailedException') { + throw new ForbiddenException('You do not have permission to update this report'); + } else if (error.name === 'ResourceNotFoundException') { + throw new InternalServerErrorException( + `Table "${this.tableName}" not found. Please check your database configuration.`, + ); + } + } + + throw new InternalServerErrorException(`Failed to toggle bookmark for report ID ${id}`); + } + } } diff --git a/frontend/src/common/api/reportService.ts b/frontend/src/common/api/reportService.ts index 1c2b1d43..b98007de 100644 --- a/frontend/src/common/api/reportService.ts +++ b/frontend/src/common/api/reportService.ts @@ -1,41 +1,9 @@ import axios, { AxiosProgressEvent } from 'axios'; -import { MedicalReport, ReportCategory, ReportStatus, ProcessingStatus } from '../models/medicalReport'; +import { MedicalReport } 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 || ''; -// Mock data for testing and development -const mockReports: MedicalReport[] = [ - { - id: '1', - userId: 'user1', - title: 'Blood Test Report', - category: ReportCategory.GENERAL, - bookmarked: false, - processingStatus: ProcessingStatus.PROCESSED, - labValues: [], - summary: 'Blood test results within normal range', - status: ReportStatus.UNREAD, - filePath: '/reports/blood-test.pdf', - createdAt: '2023-04-15T12:30:00Z', - updatedAt: '2023-04-15T12:30:00Z', - }, - { - id: '2', - userId: 'user1', - title: 'Heart Checkup', - category: ReportCategory.HEART, - bookmarked: true, - processingStatus: ProcessingStatus.PROCESSED, - labValues: [], - summary: 'Heart functioning normally', - status: ReportStatus.READ, - filePath: '/reports/heart-checkup.pdf', - createdAt: '2023-04-10T10:15:00Z', - updatedAt: '2023-04-10T10:15:00Z', - }, -]; - /** * Interface for upload progress callback */ @@ -195,7 +163,7 @@ export const toggleReportBookmark = async ( isBookmarked: boolean, ): Promise => { try { - await axios.patch( + const response = await axios.patch( `${API_URL}/api/reports/${reportId}/bookmark`, { bookmarked: isBookmarked, @@ -203,20 +171,7 @@ export const toggleReportBookmark = async ( await getAuthConfig(), ); - // In a real implementation, this would return the response from the API - // return response.data; - - // For now, we'll mock the response - const report = mockReports.find((r) => r.id === reportId); - - if (!report) { - throw new Error(`Report with ID ${reportId} not found`); - } - - // Update the bookmark status - report.bookmarked = isBookmarked; - - return { ...report }; + return response.data; } catch (error) { if (axios.isAxiosError(error)) { throw new ReportError(`Failed to toggle bookmark status: ${error.message}`); From ab7f71d997532934269c05f676104ea8637098f2 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Fri, 18 Apr 2025 13:39:36 -0400 Subject: [PATCH 2/2] Remove condition expression --- backend/src/reports/reports.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/reports/reports.service.ts b/backend/src/reports/reports.service.ts index f497b6c0..9eb3f942 100644 --- a/backend/src/reports/reports.service.ts +++ b/backend/src/reports/reports.service.ts @@ -411,7 +411,6 @@ export class ReportsService { id, }), UpdateExpression: 'SET bookmarked = :bookmarked, updatedAt = :updatedAt', - ConditionExpression: 'userId = :userId', // Ensure the report belongs to the user ExpressionAttributeValues: marshall({ ':bookmarked': bookmarked, ':updatedAt': new Date().toISOString(),