@@ -13,6 +13,7 @@ import {
1313 DynamoDBServiceException ,
1414 PutItemCommand ,
1515 QueryCommand ,
16+ DeleteItemCommand ,
1617} from '@aws-sdk/client-dynamodb' ;
1718import { marshall , unmarshall } from '@aws-sdk/util-dynamodb' ;
1819import { 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+ const processingStatusFilter = 'processingStatus <> :failedStatus' ;
66+
67+ if ( ! withFailed ) {
68+ expressionAttributeValues [ ':failedStatus' ] = ProcessingStatus . FAILED ;
69+ }
70+
6471 const command = new QueryCommand ( {
6572 TableName : this . tableName ,
6673 KeyConditionExpression : 'userId = :userId' ,
67- ExpressionAttributeValues : marshall ( {
68- ':userId' : userId ,
69- } ) ,
74+ FilterExpression : ! withFailed ? processingStatusFilter : undefined ,
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+ const processingStatusFilter = 'processingStatus <> :failedStatus' ;
120+
121+ if ( ! withFailed ) {
122+ expressionAttributeValues [ ':failedStatus' ] = ProcessingStatus . FAILED ;
123+ }
124+
110125 const command = new QueryCommand ( {
111126 TableName : this . tableName ,
112- IndexName : 'userIdCreatedAtIndex' , // Use the GSI for efficient queries
127+ IndexName : 'userIdCreatedAtIndex' ,
113128 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
129+ FilterExpression : ! withFailed ? processingStatusFilter : undefined ,
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}
0 commit comments