Skip to content

Commit 96b50fa

Browse files
committed
Add reports
1 parent 4758cab commit 96b50fa

File tree

10 files changed

+300
-3
lines changed

10 files changed

+300
-3
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Construct } from 'constructs';
2+
import { RemovalPolicy } from 'aws-cdk-lib';
3+
import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb';
4+
5+
export function createReportsTable(scope: Construct, id: string): Table {
6+
const table = new Table(scope, id, {
7+
tableName: 'reports',
8+
partitionKey: {
9+
name: 'id',
10+
type: AttributeType.STRING,
11+
},
12+
billingMode: BillingMode.PAY_PER_REQUEST,
13+
removalPolicy: RemovalPolicy.RETAIN,
14+
});
15+
16+
// Add a GSI for querying by userId
17+
table.addGlobalSecondaryIndex({
18+
indexName: 'userIdIndex',
19+
partitionKey: {
20+
name: 'userId',
21+
type: AttributeType.STRING,
22+
},
23+
sortKey: {
24+
name: 'createdAt',
25+
type: AttributeType.STRING,
26+
},
27+
});
28+
29+
return table;
30+
}

backend/package-lock.json

Lines changed: 19 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
},
2929
"dependencies": {
3030
"@aws-sdk/client-dynamodb": "^3.758.0",
31+
"@aws-sdk/util-dynamodb": "^3.758.0",
3132
"@aws-sdk/client-secrets-manager": "^3.758.0",
3233
"@nestjs/common": "^10.0.0",
3334
"@nestjs/config": "^3.1.1",

backend/src/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { PerplexityController } from './controllers/perplexity/perplexity.contro
99
import { AuthModule } from './auth/auth.module';
1010
import { UserController } from './user/user.controller';
1111
import { AuthMiddleware } from './auth/auth.middleware';
12+
import { UserModule } from './user/user.module';
13+
import { ReportsModule } from './reports/reports.module';
1214

1315
@Module({
1416
imports: [
@@ -17,6 +19,8 @@ import { AuthMiddleware } from './auth/auth.middleware';
1719
load: [configuration],
1820
}),
1921
AuthModule,
22+
UserModule,
23+
ReportsModule,
2024
],
2125
controllers: [AppController, PerplexityController, UserController],
2226
providers: [AppService, AwsSecretsService, PerplexityService],
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNumber, IsOptional, Min, Max } from 'class-validator';
3+
import { Type } from 'class-transformer';
4+
5+
export class GetReportsQueryDto {
6+
@ApiProperty({
7+
description: 'Maximum number of reports to return',
8+
required: false,
9+
default: 10
10+
})
11+
@IsOptional()
12+
@Type(() => Number)
13+
@IsNumber()
14+
@Min(1)
15+
@Max(100)
16+
limit?: number = 10;
17+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsEnum, IsNotEmpty } from 'class-validator';
3+
import { ReportStatus } from '../models/report.model';
4+
5+
export class UpdateReportStatusDto {
6+
@ApiProperty({
7+
description: 'New status for the report',
8+
enum: ReportStatus,
9+
example: ReportStatus.READ
10+
})
11+
@IsNotEmpty()
12+
@IsEnum(ReportStatus)
13+
status: ReportStatus;
14+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export enum ReportStatus {
4+
UNREAD = 'UNREAD',
5+
READ = 'READ',
6+
}
7+
8+
export class Report {
9+
@ApiProperty({ description: 'Unique identifier for the report' })
10+
id: string;
11+
12+
@ApiProperty({ description: 'Title of the report' })
13+
title: string;
14+
15+
@ApiProperty({ description: 'Content of the report' })
16+
content: string;
17+
18+
@ApiProperty({ description: 'User ID of the report owner' })
19+
userId: string;
20+
21+
@ApiProperty({ description: 'Creation timestamp' })
22+
createdAt: string;
23+
24+
@ApiProperty({ description: 'Last update timestamp' })
25+
updatedAt: string;
26+
27+
@ApiProperty({
28+
description: 'Status of the report',
29+
enum: ReportStatus,
30+
default: ReportStatus.UNREAD
31+
})
32+
status: ReportStatus;
33+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
Controller,
3+
Get,
4+
Patch,
5+
Param,
6+
Body,
7+
Query,
8+
UseGuards,
9+
ValidationPipe
10+
} from '@nestjs/common';
11+
import {
12+
ApiTags,
13+
ApiOperation,
14+
ApiResponse,
15+
ApiBearerAuth,
16+
ApiParam,
17+
ApiQuery
18+
} from '@nestjs/swagger';
19+
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
20+
import { ReportsService } from './reports.service';
21+
import { Report, ReportStatus } from './models/report.model';
22+
import { GetReportsQueryDto } from './dto/get-reports.dto';
23+
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
24+
25+
@ApiTags('reports')
26+
@Controller('reports')
27+
@UseGuards(JwtAuthGuard)
28+
@ApiBearerAuth()
29+
export class ReportsController {
30+
constructor(private readonly reportsService: ReportsService) {}
31+
32+
@ApiOperation({ summary: 'Get all reports' })
33+
@ApiResponse({
34+
status: 200,
35+
description: 'Returns all reports',
36+
type: [Report]
37+
})
38+
@Get()
39+
async findAll(): Promise<Report[]> {
40+
return this.reportsService.findAll();
41+
}
42+
43+
@ApiOperation({ summary: 'Get latest reports' })
44+
@ApiResponse({
45+
status: 200,
46+
description: 'Returns the latest reports',
47+
type: [Report]
48+
})
49+
@ApiQuery({
50+
name: 'limit',
51+
required: false,
52+
description: 'Maximum number of reports to return'
53+
})
54+
@Get('latest')
55+
async findLatest(@Query(ValidationPipe) queryDto: GetReportsQueryDto): Promise<Report[]> {
56+
return this.reportsService.findLatest(queryDto);
57+
}
58+
59+
@ApiOperation({ summary: 'Update report status' })
60+
@ApiResponse({
61+
status: 200,
62+
description: 'Report status updated successfully',
63+
type: Report
64+
})
65+
@ApiResponse({
66+
status: 404,
67+
description: 'Report not found'
68+
})
69+
@ApiParam({
70+
name: 'id',
71+
description: 'Report ID'
72+
})
73+
@Patch(':id/status')
74+
async updateStatus(
75+
@Param('id') id: string,
76+
@Body(ValidationPipe) updateDto: UpdateReportStatusDto
77+
): Promise<Report> {
78+
return this.reportsService.updateStatus(id, updateDto);
79+
}
80+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
3+
import { ReportsController } from './reports.controller';
4+
import { ReportsService } from './reports.service';
5+
6+
@Module({
7+
imports: [ConfigModule],
8+
controllers: [ReportsController],
9+
providers: [ReportsService],
10+
exports: [ReportsService],
11+
})
12+
export class ReportsModule {}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { Injectable, NotFoundException } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import {
4+
DynamoDBClient,
5+
ScanCommand,
6+
GetItemCommand,
7+
UpdateItemCommand,
8+
QueryCommand
9+
} from '@aws-sdk/client-dynamodb';
10+
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
11+
import { Report, ReportStatus } from './models/report.model';
12+
import { GetReportsQueryDto } from './dto/get-reports.dto';
13+
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
14+
15+
@Injectable()
16+
export class ReportsService {
17+
private readonly dynamoClient: DynamoDBClient;
18+
private readonly tableName: string;
19+
20+
constructor(private configService: ConfigService) {
21+
this.dynamoClient = new DynamoDBClient({
22+
region: this.configService.get<string>('AWS_REGION', 'us-east-1'),
23+
});
24+
this.tableName = this.configService.get<string>('DYNAMODB_TABLE_NAME', 'reports');
25+
}
26+
27+
async findAll(): Promise<Report[]> {
28+
const command = new ScanCommand({
29+
TableName: this.tableName,
30+
});
31+
32+
const response = await this.dynamoClient.send(command);
33+
return (response.Items || []).map(item => unmarshall(item) as Report);
34+
}
35+
36+
async findLatest(queryDto: GetReportsQueryDto): Promise<Report[]> {
37+
const limit = queryDto.limit || 10;
38+
39+
const command = new ScanCommand({
40+
TableName: this.tableName,
41+
Limit: limit,
42+
ScanIndexForward: false, // This will sort in descending order
43+
});
44+
45+
const response = await this.dynamoClient.send(command);
46+
const reports = (response.Items || []).map(item => unmarshall(item) as Report);
47+
48+
// Sort by createdAt in descending order
49+
return reports.sort((a, b) =>
50+
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
51+
).slice(0, limit);
52+
}
53+
54+
async findOne(id: string): Promise<Report> {
55+
const command = new GetItemCommand({
56+
TableName: this.tableName,
57+
Key: marshall({ id }),
58+
});
59+
60+
const response = await this.dynamoClient.send(command);
61+
62+
if (!response.Item) {
63+
throw new NotFoundException(`Report with ID ${id} not found`);
64+
}
65+
66+
return unmarshall(response.Item) as Report;
67+
}
68+
69+
async updateStatus(id: string, updateDto: UpdateReportStatusDto): Promise<Report> {
70+
// First check if the report exists
71+
await this.findOne(id);
72+
73+
const command = new UpdateItemCommand({
74+
TableName: this.tableName,
75+
Key: marshall({ id }),
76+
UpdateExpression: 'SET #status = :status, updatedAt = :updatedAt',
77+
ExpressionAttributeNames: {
78+
'#status': 'status',
79+
},
80+
ExpressionAttributeValues: marshall({
81+
':status': updateDto.status,
82+
':updatedAt': new Date().toISOString(),
83+
}),
84+
ReturnValues: 'ALL_NEW',
85+
});
86+
87+
const response = await this.dynamoClient.send(command);
88+
return unmarshall(response.Attributes) as Report;
89+
}
90+
}

0 commit comments

Comments
 (0)