Skip to content

Commit

Permalink
validator serveice added
Browse files Browse the repository at this point in the history
  • Loading branch information
sinanptm committed Sep 15, 2024
1 parent 890c4a6 commit a0a6d55
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 20 deletions.
10 changes: 10 additions & 0 deletions server/src/domain/entities/ValidationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StatusCode } from "../../types";

export class ValidationError extends Error {
public statusCode: StatusCode
constructor(message: string, statusCode: StatusCode) {
super(message);
this.name = 'ValidationError';
this.statusCode = statusCode
}
}
11 changes: 11 additions & 0 deletions server/src/domain/interface/services/IValidatorService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default interface IValidatorService {
validateRequiredFields(input: object): void;
validateEmailFormat(email: string): boolean;
validateLength(field: string, minLength: number, maxLength?: number): boolean;
validateIdFormat(id: string): boolean;
validatePhoneNumber(phoneNumber: string): boolean;
validateDateFormat(date: string): boolean;
validateEnum(field: string, enumValues: string[]): boolean;
validatePassword(password: string): boolean;
validateBoolean(value: any): boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Joi from "joi";
import IAppointment from "../../entities/IAppointment";

export interface IAppointmentValidator {
createValidate(appointment: IAppointment): Joi.ValidationResult;
updateValidate(appointment: IAppointment): Joi.ValidationResult;
}
95 changes: 95 additions & 0 deletions server/src/infrastructure/services/JoiService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Joi from "joi";
import { StatusCode } from '../../types';
import IValidatorService from "../../domain/interface/services/IValidatorService";
import { ValidationError } from "../../domain/entities/ValidationError";

export default class JoiService implements IValidatorService {

public validateRequiredFields(input: object): void {
const schema = Joi.object().keys(Object.keys(input).reduce((acc, key) => {
acc[key] = Joi.required();
return acc;
}, {} as any));

const { error } = schema.validate(input);
if (error) {
throw new ValidationError(`Missing required fields: ${error.details.map(detail => detail.message).join(", ")}`, StatusCode.BadRequest);
}
}

public validateEmailFormat(email: string): boolean {
const schema = Joi.string().email();
const { error } = schema.validate(email);
if (error) {
throw new ValidationError('Invalid email format', StatusCode.BadRequest);
}
return true;
}

public validateLength(field: string, minLength: number, maxLength: number = Infinity): boolean {
const schema = Joi.string().min(minLength).max(maxLength);
const { error } = schema.validate(field);
if (error) {
throw new ValidationError(`Invalid length for field, expected between ${minLength} and ${maxLength} characters`, StatusCode.BadRequest);
}
return true;
}

public validateIdFormat(id: string): boolean {
const schema = Joi.string().pattern(new RegExp("^[a-fA-F0-9]{24}$"));
const { error } = schema.validate(id);
if (error) {
throw new ValidationError('Invalid ID format', StatusCode.BadRequest);
}
return true;
}

public validatePhoneNumber(phoneNumber: string): boolean {
const schema = Joi.string().pattern(new RegExp("^[0-9]{10}$"));
const { error } = schema.validate(phoneNumber);
if (error) {
throw new ValidationError('Invalid phone number format', StatusCode.BadRequest);
}
return true;
}

public validateDateFormat(date: string): boolean {
const schema = Joi.date().iso();
const { error } = schema.validate(date);
if (error) {
throw new ValidationError('Invalid date format', StatusCode.BadRequest);
}
return true;
}

public validateEnum(field: string, enumValues: string[]): boolean {
const schema = Joi.string().valid(...enumValues);
const { error } = schema.validate(field);
if (error) {
throw new ValidationError(`Invalid value for field, expected one of: ${enumValues.join(", ")}`, StatusCode.BadRequest);
}
return true;
}

public validatePassword(password: string): boolean {
const schema = Joi.string().min(8).pattern(/^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/);
const { error } = schema.validate(password);
if (error) {
throw new ValidationError(
'Password must be at least 8 characters, include at least one uppercase letter, one number, and one special character',
StatusCode.BadRequest
);
}
return true;
}


public validateBoolean(value: any): boolean {
const schema = Joi.boolean();
const { error } = schema.validate(value);
if (error) {
throw new ValidationError('Invalid boolean value', StatusCode.BadRequest);
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import { NextFunction, Response } from "express";
import { CustomRequest, StatusCode } from "../../../types";
import AppointmentUseCase from "../../../use_case/appointment/AppointmentUseCase";
import AppointmentValidator from "../../validators/AppointmentValidator";

export default class AppointmentController {
constructor(
private appointmentUseCase: AppointmentUseCase,
private appointmentValidator: AppointmentValidator
) { }

async create(req: CustomRequest, res: Response, next: NextFunction) {
try {
const { appointment } = req.body;
const { error } = this.appointmentValidator.validate(appointment);
if (error) {
return res.status(StatusCode.BadRequest).json({ message: error.details[0].message });
}
const patientId = req.patient?.id;
await this.appointmentUseCase.create(appointment, patientId!);
res.status(StatusCode.Success).json({ message: "Appointment created successfully" });
} catch (error:any) {
if(error.message==='Slot already booked'){
return res.status(StatusCode.Conflict).json({message:error.message})
} catch (error: any) {
if (error.message === 'Slot already booked') {
return res.status(StatusCode.Conflict).json({ message: error.message })
}
next(error);
}
Expand Down
10 changes: 8 additions & 2 deletions server/src/presentation/middlewares/ErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { StatusCode } from "../../types/index";
import logger from "../../utils/logger";
import { ValidationError } from "../../domain/entities/ValidationError";

export default class ErrorHandler {
exec(err: any, req: Request, res: Response, next: NextFunction) {
Expand All @@ -12,8 +13,13 @@ export default class ErrorHandler {
ip: req.ip,
});


const statusCode = err.statusCode || StatusCode.InternalServerError;
const message = err.message || "Internal Server Error";

if (err instanceof ValidationError) {
return res.status(statusCode).json({ message });
}

if (err.code && err.code === 11000) {
logger.warn("Duplicate key error encountered.");
Expand Down Expand Up @@ -54,8 +60,8 @@ export default class ErrorHandler {
});
} else if (message.includes("Invalid Object Id")) {
return res.status(StatusCode.UnprocessableEntity).json({ message });
}else if (message.includes("Invalid Filter")){
return res.status(StatusCode.BadRequest).json({message})
} else if (message.includes("Invalid Filter")) {
return res.status(StatusCode.BadRequest).json({ message })
}

res.status(statusCode).json({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import AppointmentRepository from '../../../infrastructure/repositories/Appointm
import SlotRepository from '../../../infrastructure/repositories/SlotRepository';
import AppointmentUseCase from '../../../use_case/appointment/AppointmentUseCase';
import AppointmentController from '../../controllers/appointment/AppointmentControllers';
import AppointmentValidator from '../../validators/AppointmentValidator';
import PatientAuthMiddleware from '../../middlewares/PatientAuthMiddleware';
import JWTService from '../../../infrastructure/services/JWTService';
import JoiService from '../../../infrastructure/services/JoiService';

const router = express.Router();


const appointmentRepository = new AppointmentRepository();
const slotRepository = new SlotRepository();
const tokenService = new JWTService()
const validatorService = new JoiService()

const appointmentUseCase = new AppointmentUseCase(appointmentRepository, slotRepository);
const appointmentUseCase = new AppointmentUseCase(appointmentRepository, slotRepository, validatorService);

const appointmentValidator = new AppointmentValidator()
const appointmentController = new AppointmentController(appointmentUseCase, appointmentValidator);
const appointmentController = new AppointmentController(appointmentUseCase);

const authorizePatient = new PatientAuthMiddleware(tokenService);

Expand Down
30 changes: 25 additions & 5 deletions server/src/use_case/appointment/AppointmentUseCase.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
import IAppointment, { AppointmentStatus } from "../../domain/entities/IAppointment";
import IAppointment, { AppointmentStatus, AppointmentType } from "../../domain/entities/IAppointment";
import IAppointmentRepository from "../../domain/interface/repositories/IAppointmentRepository";
import ISlotRepository from "../../domain/interface/repositories/ISlotRepository";
import IValidatorService from "../../domain/interface/services/IValidatorService";

export default class AppointmentUseCase {
constructor(
private appointRepository: IAppointmentRepository,
private slotRepository: ISlotRepository
private slotRepository: ISlotRepository,
private validatorService: IValidatorService
) { }

async create({ slotId, ...appointment }: IAppointment, patientId: string): Promise<void> {
async create({ slotId, appointmentDate, appointmentType, reason, doctorId, notes }: IAppointment, patientId: string): Promise<void> {
this.validatorService.validateRequiredFields({ slotId, appointmentDate, appointmentType, reason, doctorId });
this.validatorService.validateIdFormat(doctorId!);
this.validatorService.validateIdFormat(slotId!);
this.validatorService.validateIdFormat(patientId!)
this.validatorService.validateEnum(appointmentType!, Object.values(AppointmentType));
this.validatorService.validateDateFormat(appointmentDate!);
this.validatorService.validateLength(reason!, 1, 255);
if (notes) this.validatorService.validateLength(notes, 0, 255);

const slot = await this.slotRepository.findById(slotId!);
if (!slot) throw new Error("Slot Not Found");
if (slot.status==='booked') throw new Error("Slot already booked");
if (slot.status === 'booked') throw new Error("Slot already booked");
slot!.status = 'booked';
await this.slotRepository.update(slot);
await this.appointRepository.create({ ...appointment, slotId, patientId, status: AppointmentStatus.PENDING });
await this.appointRepository.create({
slotId,
patientId,
status: AppointmentStatus.PENDING,
notes,
appointmentDate,
appointmentType,
reason,
doctorId
});
}

}

0 comments on commit a0a6d55

Please sign in to comment.