Skip to content

Commit 034779b

Browse files
committed
feat: Optimize report retrieval by replacing ScanCommand with QueryCommand and adding fallback for GSI validation
1 parent ab78e8f commit 034779b

File tree

1 file changed

+87
-19
lines changed

1 file changed

+87
-19
lines changed

backend/src/reports/reports.service.ts

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import {
88
import { ConfigService } from '@nestjs/config';
99
import {
1010
DynamoDBClient,
11-
ScanCommand,
1211
GetItemCommand,
1312
UpdateItemCommand,
1413
DynamoDBServiceException,
1514
PutItemCommand,
15+
QueryCommand,
1616
} from '@aws-sdk/client-dynamodb';
1717
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
1818
import { Report, ReportStatus } from './models/report.model';
@@ -60,10 +60,10 @@ export class ReportsService {
6060
}
6161

6262
try {
63-
// If the table has a GSI for userId, use QueryCommand instead
64-
const command = new ScanCommand({
63+
// Use QueryCommand instead of ScanCommand since userId is the partition key
64+
const command = new QueryCommand({
6565
TableName: this.tableName,
66-
FilterExpression: 'userId = :userId',
66+
KeyConditionExpression: 'userId = :userId',
6767
ExpressionAttributeValues: marshall({
6868
':userId': userId,
6969
}),
@@ -105,23 +105,21 @@ export class ReportsService {
105105
typeof queryDto.limit === 'string' ? parseInt(queryDto.limit, 10) : queryDto.limit || 10;
106106

107107
try {
108-
// If the table has a GSI for userId, use QueryCommand instead
109-
const command = new ScanCommand({
108+
// Use the GSI userIdCreatedAtIndex with QueryCommand for efficient retrieval
109+
// This is much more efficient than a ScanCommand
110+
const command = new QueryCommand({
110111
TableName: this.tableName,
111-
FilterExpression: 'userId = :userId',
112+
IndexName: 'userIdCreatedAtIndex', // Use the GSI for efficient queries
113+
KeyConditionExpression: 'userId = :userId',
112114
ExpressionAttributeValues: marshall({
113115
':userId': userId,
114116
}),
115-
Limit: limit * 5, // Fetch more items since we'll filter by userId
117+
ScanIndexForward: false, // Get items in descending order (newest first)
118+
Limit: limit, // Only fetch the number of items we need
116119
});
117120

118121
const response = await this.dynamoClient.send(command);
119-
const reports = (response.Items || []).map(item => unmarshall(item) as Report);
120-
121-
// Sort by createdAt in descending order
122-
return reports
123-
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
124-
.slice(0, limit);
122+
return (response.Items || []).map(item => unmarshall(item) as Report);
125123
} catch (error: unknown) {
126124
this.logger.error(`Error fetching latest reports for user ${userId}:`);
127125
this.logger.error(error);
@@ -131,6 +129,26 @@ export class ReportsService {
131129
throw new InternalServerErrorException(
132130
`Table "${this.tableName}" not found. Please check your database configuration.`,
133131
);
132+
} else if (error.name === 'ValidationException') {
133+
// This could happen if the GSI doesn't exist
134+
this.logger.warn('GSI validation error, falling back to standard query');
135+
136+
// Fallback to standard query and sort in memory if GSI has issues
137+
const fallbackCommand = new QueryCommand({
138+
TableName: this.tableName,
139+
KeyConditionExpression: 'userId = :userId',
140+
ExpressionAttributeValues: marshall({
141+
':userId': userId,
142+
}),
143+
});
144+
145+
const fallbackResponse = await this.dynamoClient.send(fallbackCommand);
146+
const reports = (fallbackResponse.Items || []).map(item => unmarshall(item) as Report);
147+
148+
// Sort by createdAt in descending order
149+
return reports
150+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
151+
.slice(0, limit);
134152
}
135153
}
136154

@@ -330,25 +348,75 @@ export class ReportsService {
330348
throw new ForbiddenException('User ID is required');
331349
}
332350

351+
// Log the actual filePath being searched for debugging
352+
this.logger.log(`Searching for report with filePath: "${filePath}" for user ${userId}`);
353+
333354
try {
334-
// Since filePath isn't a key attribute, we need to scan with filter
335-
const command = new ScanCommand({
355+
const command = new QueryCommand({
336356
TableName: this.tableName,
337-
FilterExpression: 'filePath = :filePath AND userId = :userId',
357+
KeyConditionExpression: 'userId = :userId',
358+
FilterExpression: 'filePath = :filePath',
338359
ExpressionAttributeValues: marshall({
339-
':filePath': filePath,
340360
':userId': userId,
361+
':filePath': filePath,
341362
}),
342363
Limit: 1, // We only want one record
343364
});
344365

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+
345376
const response = await this.dynamoClient.send(command);
346377

378+
this.logger.log(`Query response received, found ${response.Items?.length || 0} items`);
379+
347380
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+
348413
return null;
349414
}
350415

351-
return unmarshall(response.Items[0]) as Report;
416+
const result = unmarshall(response.Items[0]) as Report;
417+
this.logger.log(`Found report with ID ${result.id}`);
418+
419+
return result;
352420
} catch (error: unknown) {
353421
this.logger.error(`Error finding report with filePath ${filePath}:`);
354422
this.logger.error(error);

0 commit comments

Comments
 (0)