Skip to content

Commit fd3aca6

Browse files
committed
feat: add validation on dto
1 parent 6ad2882 commit fd3aca6

5 files changed

Lines changed: 106 additions & 22 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
2+
import { ObjectSchema } from 'joi';
3+
4+
@Injectable()
5+
export class JoiValidationPipe implements PipeTransform {
6+
constructor(private schema: ObjectSchema) {}
7+
8+
transform(value: any): Record<string, any> {
9+
try {
10+
const { error } = this.schema.validate(value, { abortEarly: false });
11+
if (error) {
12+
throw new BadRequestException({
13+
message: 'Validation failed',
14+
details: error.details.map((detail) => detail.message),
15+
});
16+
}
17+
return value as Record<string, any>;
18+
} catch {
19+
throw new BadRequestException({
20+
message: 'Joi validation error',
21+
level: 'error',
22+
});
23+
}
24+
}
25+
}

src/notification/domain/interfaces/notification-repository.interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ export interface INotificationRepository {
1414
* @returns A promise that resolves to the found notification entity, or null if not found.
1515
*/
1616
findById(id: string): Promise<Notification | null>;
17+
18+
findAll(skip: number, limit: number): Promise<Notification[]>;
1719
}

src/notification/infrastructure/repositories/notification.repository.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,19 @@ export class NotificationRepository implements INotificationRepository {
1111
@InjectModel('Notification')
1212
private readonly notificationModel: Model<NotificationDocument>,
1313
) {}
14+
1415
findById(id: string): Promise<Notification | null> {
1516
throw new Error(`Method not implemented. ID: ${id}`);
1617
}
1718

19+
async findAll(skip: number, limit: number): Promise<Notification[]> {
20+
const docs = await this.notificationModel
21+
.find()
22+
.skip(skip)
23+
.limit(limit)
24+
.exec();
25+
return docs.map((doc) => this.toEntity(doc));
26+
}
1827
async save(notification: Notification): Promise<Notification> {
1928
const doc = new this.notificationModel(notification);
2029
const saved = await doc.save();

src/notification/presentation/controllers/notification.controller.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,20 @@ import {
1616
ApiQuery,
1717
} from '@nestjs/swagger';
1818
import { NotificationFactory } from '../../application/factories/notification.factory';
19-
import { NotificationValidator } from '../../application/validators/notification.validator';
20-
import { SendNotificationDto } from '../dtos/send-notification.dto';
19+
import {
20+
SendNotificationDto,
21+
SendNotificationSchema,
22+
} from '../dtos/send-notification.dto';
2123
import { Notification } from '../../domain/entities/notification.entity';
2224
import { INotificationStrategy } from '../../domain/interfaces/notification-strategy.interface';
2325
import { INotificationRepository } from '../../domain/interfaces/notification-repository.interface';
2426
import { LoggerServiceFile } from '../../../logger/services/logger.service.file';
2527
import { LoggerServiceDb } from '../../../logger/services/logger.service.db';
26-
import { number } from 'joi';
28+
import { JoiValidationPipe } from '../../../config/validation/joi-validation.pipe';
2729

2830
@ApiTags('Notifications')
2931
@Controller('api/notifications')
3032
export class NotificationController {
31-
notificationModel: import('mongoose').Model<Notification>;
3233
constructor(
3334
private readonly notificationFactory: NotificationFactory,
3435
@Inject('INotificationRepository')
@@ -52,14 +53,10 @@ export class NotificationController {
5253
status: 400,
5354
description: 'Validation failed',
5455
})
55-
async send(@Body() dto: SendNotificationDto): Promise<void> {
56-
// Validate input
57-
const { error } = NotificationValidator.validateCreateNotification(dto);
58-
if (error) {
59-
this.logger.error(`Validation failed: ${error.message}`);
60-
throw new Error(`Validation failed: ${error.message}`);
61-
}
62-
56+
async send(
57+
@Body(new JoiValidationPipe(SendNotificationSchema))
58+
dto: SendNotificationDto,
59+
): Promise<void> {
6360
// Create notification entity
6461
const notification = new Notification(
6562
dto.recipient,
@@ -87,11 +84,15 @@ export class NotificationController {
8784

8885
await this.loggerDb.error({
8986
level: 'info',
90-
message: 'Exception occurred while sending notification',
87+
message: `Notification (${dto.notificationType}) sent to ${dto.recipient} via ${dto.mediaType}`,
9188
timestamp: new Date(),
9289
});
9390
}
91+
9492
@Get('history')
93+
@ApiOperation({
94+
summary: 'Retrieve notification history',
95+
})
9596
@ApiResponse({
9697
status: HttpStatus.OK,
9798
description: 'Notification history retrieved',
@@ -113,17 +114,13 @@ export class NotificationController {
113114
@Query('limit') limit = 10,
114115
) {
115116
const skip = (page - 1) * limit;
116-
const notifications = await this.notificationModel
117-
.find()
118-
.sort({ createdAt: -1 })
119-
.skip(skip)
120-
.limit(limit)
121-
.exec();
122-
const total = await this.notificationModel.countDocuments().exec();
117+
const notifications: Notification[] =
118+
await this.notificationRepository.findAll(skip, limit);
119+
const total = notifications.length;
123120
return {
124121
data: notifications,
125-
total: number,
126-
page: number,
122+
total,
123+
page,
127124
totalPages: Math.ceil(total / limit),
128125
};
129126
}

src/notification/presentation/dtos/send-notification.dto.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ApiProperty } from '@nestjs/swagger';
2+
import * as Joi from 'joi';
23
import { NotificationChannel } from '../../../config/notification.config';
34

45
export enum NotificationType {
@@ -46,3 +47,53 @@ export class SendNotificationDto {
4647
})
4748
notificationType: NotificationType;
4849
}
50+
51+
export const SendNotificationSchema = Joi.object({
52+
recipient: Joi.string().required().messages({
53+
'string.empty': 'Recipient is required',
54+
}),
55+
subject: Joi.string().min(3).max(100).required().messages({
56+
'string.min': 'Subject must be at least 3 characters',
57+
'string.max': 'Subject must not exceed 100 characters',
58+
'string.empty': 'Subject is required',
59+
}),
60+
body: Joi.string().min(10).max(1000).required().messages({
61+
'string.min': 'Body must be at least 10 characters',
62+
'string.max': 'Body must not exceed 1000 characters',
63+
'string.empty': 'Body is required',
64+
}),
65+
mediaType: Joi.string()
66+
.valid(...Object.values(NotificationChannel))
67+
.required()
68+
.messages({
69+
'any.only': 'Invalid mediaType, must be one of: EMAIL, SMS',
70+
'string.empty': 'mediaType is required',
71+
}),
72+
notificationType: Joi.string()
73+
.valid(...Object.values(NotificationType))
74+
.required()
75+
.messages({
76+
'any.only': 'Invalid notificationType',
77+
'string.empty': 'notificationType is required',
78+
}),
79+
}).custom((value: Record<string, any>, helpers) => {
80+
if (
81+
value.mediaType === NotificationChannel.EMAIL &&
82+
!Joi.string().email().validate(value.recipient).value
83+
) {
84+
return helpers.message({
85+
custom: 'Recipient must be a valid email for EMAIL channel',
86+
});
87+
}
88+
if (
89+
value.mediaType === NotificationChannel.SMS &&
90+
!Joi.string()
91+
.pattern(/^\+?[1-9]\d{1,14}$/)
92+
.validate(value.recipient).value
93+
) {
94+
return helpers.message({
95+
custom: 'Recipient must be a valid phone number for SMS channel',
96+
});
97+
}
98+
return value;
99+
});

0 commit comments

Comments
 (0)