Skip to content

Commit

Permalink
add validation
Browse files Browse the repository at this point in the history
  • Loading branch information
rcole1919 committed Oct 28, 2024
1 parent 124e229 commit ae1d34c
Show file tree
Hide file tree
Showing 38 changed files with 592 additions and 74 deletions.
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@typegoose/typegoose": "12.4.0",
"chalk": "5.3.0",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"convict": "6.2.4",
"convict-format-with-validator": "6.2.0",
"dayjs": "1.11.10",
Expand Down
4 changes: 3 additions & 1 deletion src/rest/base-controller.abstract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export abstract class BaseController implements IController {

public addRoute(route: IRoute): void {
const wrapperAsyncHandler = asyncHandler(route.handler.bind(this));
this.router[route.method](route.path, wrapperAsyncHandler);
const middlewareHandlers = route.middlewares?.map((item) => asyncHandler(item.execute.bind(item)));
const allHandlers = middlewareHandlers ? [...middlewareHandlers, wrapperAsyncHandler] : wrapperAsyncHandler;
this.router[route.method](route.path, allHandlers);
this.logger.info(`Route registered: ${route.method.toUpperCase()} ${route.path}`);
}

Expand Down
3 changes: 3 additions & 0 deletions src/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ export { createRestApplicationContainer } from './rest.container.js';
export { BaseController } from './base-controller.abstract.js';
export { AppExceptionFilter } from './app-exception-filter.js';
export { HttpError } from './http-error.js';
export { ValidateObjectIdMiddleware } from './middlewares/validate-objectid.middleware.js';
export { ValidateDTOMiddleware } from './middlewares/validate-dto.middleware.js';
export { DocumentExistsMiddleware } from './middlewares/document-exists.middleware.js';
26 changes: 26 additions & 0 deletions src/rest/middlewares/document-exists.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextFunction, Request, Response } from 'express';
import { StatusCodes } from 'http-status-codes';

import { IDocumentExists, IMiddleware } from '../types/index.js';
import { HttpError } from '../index.js';

export class DocumentExistsMiddleware implements IMiddleware {
constructor(
private readonly service: IDocumentExists,
private readonly entityName: string,
private readonly paramName: string,
) {}

public async execute({ params }: Request, _res: Response, next: NextFunction): Promise<void> {
const documentId = params[this.paramName];
if (! await this.service.exists(documentId)) {
throw new HttpError(
StatusCodes.NOT_FOUND,
`${this.entityName} with ${documentId} not found.`,
'DocumentExistsMiddleware'
);
}

next();
}
}
22 changes: 22 additions & 0 deletions src/rest/middlewares/validate-dto.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextFunction, Request, Response } from 'express';
import { ClassConstructor, plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { StatusCodes } from 'http-status-codes';

import { IMiddleware } from '../types/index.js';

export class ValidateDTOMiddleware implements IMiddleware {
constructor(private dto: ClassConstructor<object>) {}

public async execute({ body }: Request, res: Response, next: NextFunction): Promise<void> {
const dtoInstance = plainToInstance(this.dto, body);
const errors = await validate(dtoInstance);

if (errors.length > 0) {
res.status(StatusCodes.BAD_REQUEST).send(errors);
return;
}

next();
}
}
25 changes: 25 additions & 0 deletions src/rest/middlewares/validate-objectid.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextFunction, Request, Response } from 'express';
import { Types } from 'mongoose';
import { StatusCodes } from 'http-status-codes';

import { IMiddleware } from '../types/index.js';

import { HttpError } from '../index.js';

export class ValidateObjectIdMiddleware implements IMiddleware {
constructor(private param: string) {}

public execute({ params }: Request, _res: Response, next: NextFunction): void {
const objectId = params[this.param];

if (Types.ObjectId.isValid(objectId)) {
return next();
}

throw new HttpError(
StatusCodes.BAD_REQUEST,
`${objectId} is invalid ObjectID`,
'ValidateObjecIdMiddleware',
);
}
}
3 changes: 3 additions & 0 deletions src/rest/types/document-exists.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IDocumentExists {
exists(documentId: string): Promise<boolean>;
}
2 changes: 2 additions & 0 deletions src/rest/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export { IController } from './controller.interface.js';
export { IExceptionFilter } from './exception-filter.interface.js';
export { TRequestBody } from './request-body.type.js';
export { TRequestParams } from './request-params.type.js';
export { IMiddleware } from './middleware.interface.js';
export { IDocumentExists } from './document-exists.interface.js';
5 changes: 5 additions & 0 deletions src/rest/types/middleware.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NextFunction, Request, Response } from 'express';

export interface IMiddleware {
execute(req: Request, res: Response, next: NextFunction): void;
}
3 changes: 2 additions & 1 deletion src/rest/types/route.interface.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { NextFunction, Request, Response } from 'express';

import { EHttpMethod } from './index.js';
import { EHttpMethod, IMiddleware } from './index.js';

export interface IRoute {
path: string;
method: EHttpMethod;
handler: (req: Request, res: Response, next: NextFunction) => Promise<void> | void;
middlewares?: IMiddleware[];
}
1 change: 1 addition & 0 deletions src/shared/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {
PRICE,
DEFAULT_OFFER_COUNT,
INC_COMMENT_COUNT_NUMBER,
PHOTOS_LENGTH,
} from './offer.js';

export {
Expand Down
2 changes: 2 additions & 0 deletions src/shared/constants/offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export const OFFER_RATING = {
MAX_NUM_AFTER_DIGIT: 1,
};

export const PHOTOS_LENGTH = 6;

export const DEFAULT_OFFER_COUNT = 60;

export const INC_COMMENT_COUNT_NUMBER = 1;
31 changes: 27 additions & 4 deletions src/shared/modules/comment/comment.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,50 @@ import { inject, injectable } from 'inversify';
import { Request, Response } from 'express';
// import { StatusCodes } from 'http-status-codes';

import { BaseController } from '../../../rest/index.js';
import {
BaseController,
ValidateObjectIdMiddleware,
ValidateDTOMiddleware,
DocumentExistsMiddleware,
} from '../../../rest/index.js';
import { EHttpMethod } from '../../../rest/types/index.js';
import { COMPONENT } from '../../constants/index.js';
import { ILogger } from '../../libs/logger/types/index.js';
import { ICommentService } from './types/index.js';
import { CreateCommentDTO, CommentRDO } from './index.js';
import { fillDTO } from '../../helpers/index.js';
import { TParamOfferId } from '../offer/types/index.js';
import { IOfferService, TParamOfferId } from '../offer/types/index.js';

@injectable()
export class CommentController extends BaseController {
constructor(
@inject(COMPONENT.LOGGER) protected readonly logger: ILogger,
@inject(COMPONENT.COMMENT_SERVICE) private readonly commentService: ICommentService,
@inject(COMPONENT.OFFER_SERVICE) private readonly offerService: IOfferService,
) {
super(logger);

this.logger.info('Register routes for CommentController...');

this.addRoute({ path: '/:offerId', method: EHttpMethod.Get, handler: this.index});
this.addRoute({ path: '/:offerId', method: EHttpMethod.Post, handler: this.create});
this.addRoute({
path: '/:offerId',
method: EHttpMethod.Get,
handler: this.index,
middlewares: [
new ValidateObjectIdMiddleware('offerId'),
new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'),
],
});
this.addRoute({
path: '/:offerId',
method: EHttpMethod.Post,
handler: this.create,
middlewares: [
new ValidateObjectIdMiddleware('offerId'),
new ValidateDTOMiddleware(CreateCommentDTO),
new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'),
],
});
}

public async index({ params }: Request<TParamOfferId>, res: Response): Promise<void> {
Expand Down
10 changes: 10 additions & 0 deletions src/shared/modules/comment/dto/create-comment.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { IsMongoId, MinLength, MaxLength, IsString } from 'class-validator';

import { COMMENT_TEXT_LENGTH } from '../../../constants/index.js';
import { CreateCommentValidationMessage } from './create-comment.message.js';

export class CreateCommentDTO {
@IsString({ message: CreateCommentValidationMessage.text.invalidFormat })
@MinLength(COMMENT_TEXT_LENGTH.MIN, { message: CreateCommentValidationMessage.text.minLength })
@MaxLength(COMMENT_TEXT_LENGTH.MAX, { message: CreateCommentValidationMessage.text.maxLength })
public text!: string;

@IsMongoId({ message: CreateCommentValidationMessage.authorId.invalid })
public authorId!: string;
}
12 changes: 12 additions & 0 deletions src/shared/modules/comment/dto/create-comment.message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { COMMENT_TEXT_LENGTH } from '../../../constants/index.js';

export const CreateCommentValidationMessage = {
text: {
invalidFormat: 'Field text must be a string',
minLength: `Minimum text length must be ${COMMENT_TEXT_LENGTH.MIN}`,
maxLength: `Maximum text length must be ${COMMENT_TEXT_LENGTH.MAX}`,
},
authorId: {
invalid: 'Field authorId must be a valid MongoDB identifier',
}
};
5 changes: 5 additions & 0 deletions src/shared/modules/offer/default-offer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export class DefaultOfferService implements IOfferService {
@inject(COMPONENT.OFFER_MODEL) private readonly offerModel: types.ModelType<OfferEntity>,
) {}

public async exists(documentId: string): Promise<boolean> {
return await this.offerModel
.exists({_id: documentId}) !== null;
}

public async create(dto: CreateOfferDTO): Promise<DocumentType<OfferEntity>> {
const result = await this.offerModel.create(dto);
this.logger.info(`New offer created: ${dto.title}`);
Expand Down
10 changes: 10 additions & 0 deletions src/shared/modules/offer/dto/coords.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IsInt } from 'class-validator';
import { OfferValidationMessage } from './offer.message.js';

export class CoordsDTO {
@IsInt({ message: OfferValidationMessage.coords.latitude.invalidFormat })
public latitude!: number;

@IsInt({ message: OfferValidationMessage.coords.longitude.invalidFormat })
public longitude!: number;
}
Loading

0 comments on commit ae1d34c

Please sign in to comment.