From ae1d34c4c72a5d47b7f8bc5e9541ea842eb05c84 Mon Sep 17 00:00:00 2001 From: rcole1919 Date: Tue, 29 Oct 2024 02:11:39 +0400 Subject: [PATCH] add validation --- package-lock.json | 41 +++++++++ package.json | 1 + src/rest/base-controller.abstract.ts | 4 +- src/rest/index.ts | 3 + .../middlewares/document-exists.middleware.ts | 26 ++++++ .../middlewares/validate-dto.middleware.ts | 22 +++++ .../validate-objectid.middleware.ts | 25 ++++++ src/rest/types/document-exists.interface.ts | 3 + src/rest/types/index.ts | 2 + src/rest/types/middleware.interface.ts | 5 ++ src/rest/types/route.interface.ts | 3 +- src/shared/constants/index.ts | 1 + src/shared/constants/offer.ts | 2 + .../modules/comment/comment.controller.ts | 31 ++++++- .../modules/comment/dto/create-comment.dto.ts | 10 +++ .../comment/dto/create-comment.message.ts | 12 +++ .../modules/offer/default-offer.service.ts | 5 ++ src/shared/modules/offer/dto/coords.dto.ts | 10 +++ .../modules/offer/dto/create-offer.dto.ts | 73 +++++++++++++++- src/shared/modules/offer/dto/offer.message.ts | 67 +++++++++++++++ .../modules/offer/dto/update-offer.dto.ts | 84 ++++++++++++++++++- src/shared/modules/offer/offer.controller.ts | 74 +++++++++------- src/shared/modules/offer/offer.http | 4 +- src/shared/modules/offer/types/index.ts | 1 + .../offer/types/offer-service.interface.ts | 3 +- .../modules/offer/types/query-count.type.ts | 5 ++ .../modules/user/default-user.service.ts | 9 +- .../modules/user/dto/create-user.dto.ts | 28 ++++++- src/shared/modules/user/dto/login-user.dto.ts | 9 ++ .../modules/user/dto/update-user.dto.ts | 9 -- src/shared/modules/user/dto/user.message.ts | 23 +++++ src/shared/modules/user/index.ts | 1 - .../user/types/user-service.interface.ts | 3 +- src/shared/modules/user/user.controller.ts | 48 +++++++++-- src/shared/modules/user/user.entity.ts | 6 +- src/shared/types/city.enum.ts | 8 ++ src/shared/types/index.ts | 1 + src/shared/types/user-type.enum.ts | 4 +- 38 files changed, 592 insertions(+), 74 deletions(-) create mode 100644 src/rest/middlewares/document-exists.middleware.ts create mode 100644 src/rest/middlewares/validate-dto.middleware.ts create mode 100644 src/rest/middlewares/validate-objectid.middleware.ts create mode 100644 src/rest/types/document-exists.interface.ts create mode 100644 src/rest/types/middleware.interface.ts create mode 100644 src/shared/modules/comment/dto/create-comment.message.ts create mode 100644 src/shared/modules/offer/dto/coords.dto.ts create mode 100644 src/shared/modules/offer/dto/offer.message.ts create mode 100644 src/shared/modules/offer/types/query-count.type.ts delete mode 100644 src/shared/modules/user/dto/update-user.dto.ts create mode 100644 src/shared/modules/user/dto/user.message.ts create mode 100644 src/shared/types/city.enum.ts diff --git a/package-lock.json b/package-lock.json index c5b7dbb..baa2173 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,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", @@ -603,6 +604,11 @@ "@types/send": "*" } }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1399,6 +1405,16 @@ "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/clean-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", @@ -4396,6 +4412,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.12.tgz", + "integrity": "sha512-QkJn9/D7zZ1ucvT++TQSvZuSA2xAWeUytU+DiEQwbPKLyrDpvbul2AFs1CGbRAPpSCCk47aRAb5DX5mmcayp4g==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7719,6 +7740,11 @@ "@types/send": "*" } }, + "@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==" + }, "@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -8243,6 +8269,16 @@ "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" }, + "class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "requires": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "clean-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", @@ -10410,6 +10446,11 @@ "type-check": "~0.4.0" } }, + "libphonenumber-js": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.12.tgz", + "integrity": "sha512-QkJn9/D7zZ1ucvT++TQSvZuSA2xAWeUytU+DiEQwbPKLyrDpvbul2AFs1CGbRAPpSCCk47aRAb5DX5mmcayp4g==" + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", diff --git a/package.json b/package.json index 5c12751..22f2272 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/rest/base-controller.abstract.ts b/src/rest/base-controller.abstract.ts index fb63252..51eafac 100644 --- a/src/rest/base-controller.abstract.ts +++ b/src/rest/base-controller.abstract.ts @@ -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}`); } diff --git a/src/rest/index.ts b/src/rest/index.ts index 4a6449c..71dde96 100644 --- a/src/rest/index.ts +++ b/src/rest/index.ts @@ -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'; diff --git a/src/rest/middlewares/document-exists.middleware.ts b/src/rest/middlewares/document-exists.middleware.ts new file mode 100644 index 0000000..e5569db --- /dev/null +++ b/src/rest/middlewares/document-exists.middleware.ts @@ -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 { + 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(); + } +} diff --git a/src/rest/middlewares/validate-dto.middleware.ts b/src/rest/middlewares/validate-dto.middleware.ts new file mode 100644 index 0000000..1b5c3dd --- /dev/null +++ b/src/rest/middlewares/validate-dto.middleware.ts @@ -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) {} + + public async execute({ body }: Request, res: Response, next: NextFunction): Promise { + const dtoInstance = plainToInstance(this.dto, body); + const errors = await validate(dtoInstance); + + if (errors.length > 0) { + res.status(StatusCodes.BAD_REQUEST).send(errors); + return; + } + + next(); + } +} diff --git a/src/rest/middlewares/validate-objectid.middleware.ts b/src/rest/middlewares/validate-objectid.middleware.ts new file mode 100644 index 0000000..34b9751 --- /dev/null +++ b/src/rest/middlewares/validate-objectid.middleware.ts @@ -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', + ); + } +} diff --git a/src/rest/types/document-exists.interface.ts b/src/rest/types/document-exists.interface.ts new file mode 100644 index 0000000..2b55de5 --- /dev/null +++ b/src/rest/types/document-exists.interface.ts @@ -0,0 +1,3 @@ +export interface IDocumentExists { + exists(documentId: string): Promise; +} diff --git a/src/rest/types/index.ts b/src/rest/types/index.ts index 01393ba..2db3f82 100644 --- a/src/rest/types/index.ts +++ b/src/rest/types/index.ts @@ -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'; diff --git a/src/rest/types/middleware.interface.ts b/src/rest/types/middleware.interface.ts new file mode 100644 index 0000000..f99357d --- /dev/null +++ b/src/rest/types/middleware.interface.ts @@ -0,0 +1,5 @@ +import { NextFunction, Request, Response } from 'express'; + +export interface IMiddleware { + execute(req: Request, res: Response, next: NextFunction): void; +} diff --git a/src/rest/types/route.interface.ts b/src/rest/types/route.interface.ts index 0c898d3..e041608 100644 --- a/src/rest/types/route.interface.ts +++ b/src/rest/types/route.interface.ts @@ -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; + middlewares?: IMiddleware[]; } diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts index 0541941..4ab01f6 100644 --- a/src/shared/constants/index.ts +++ b/src/shared/constants/index.ts @@ -7,6 +7,7 @@ export { PRICE, DEFAULT_OFFER_COUNT, INC_COMMENT_COUNT_NUMBER, + PHOTOS_LENGTH, } from './offer.js'; export { diff --git a/src/shared/constants/offer.ts b/src/shared/constants/offer.ts index 0abb661..135d79c 100644 --- a/src/shared/constants/offer.ts +++ b/src/shared/constants/offer.ts @@ -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; diff --git a/src/shared/modules/comment/comment.controller.ts b/src/shared/modules/comment/comment.controller.ts index d33577e..6d86aca 100644 --- a/src/shared/modules/comment/comment.controller.ts +++ b/src/shared/modules/comment/comment.controller.ts @@ -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, res: Response): Promise { diff --git a/src/shared/modules/comment/dto/create-comment.dto.ts b/src/shared/modules/comment/dto/create-comment.dto.ts index cef2182..7720110 100644 --- a/src/shared/modules/comment/dto/create-comment.dto.ts +++ b/src/shared/modules/comment/dto/create-comment.dto.ts @@ -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; } diff --git a/src/shared/modules/comment/dto/create-comment.message.ts b/src/shared/modules/comment/dto/create-comment.message.ts new file mode 100644 index 0000000..831c2b1 --- /dev/null +++ b/src/shared/modules/comment/dto/create-comment.message.ts @@ -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', + } +}; diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index b8691d1..1799ea2 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -23,6 +23,11 @@ export class DefaultOfferService implements IOfferService { @inject(COMPONENT.OFFER_MODEL) private readonly offerModel: types.ModelType, ) {} + public async exists(documentId: string): Promise { + return await this.offerModel + .exists({_id: documentId}) !== null; + } + public async create(dto: CreateOfferDTO): Promise> { const result = await this.offerModel.create(dto); this.logger.info(`New offer created: ${dto.title}`); diff --git a/src/shared/modules/offer/dto/coords.dto.ts b/src/shared/modules/offer/dto/coords.dto.ts new file mode 100644 index 0000000..3be07c3 --- /dev/null +++ b/src/shared/modules/offer/dto/coords.dto.ts @@ -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; +} diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts index 94a24a1..4e21ea2 100644 --- a/src/shared/modules/offer/dto/create-offer.dto.ts +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -1,17 +1,86 @@ -import { EHousing, EFacilities, TCoords } from '../../../types/index.js'; +import { + IsMongoId, + IsInt, + IsString, + IsEnum, + IsBoolean, + IsArray, + IsObject, + ArrayUnique, + ArrayMinSize, + ArrayMaxSize, + MinLength, + MaxLength, + Min, + Max, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +import { EHousing, EFacilities, ECity } from '../../../types/index.js'; +import { + OFFER_TITLE_LENGTH, + OFFER_DESCRIPTION_LENGTH, + PHOTOS_LENGTH, + ROOMS_NUMBER, + VISITORS_NUMBER, + PRICE, +} from '../../../constants/index.js'; +import { OfferValidationMessage } from './offer.message.js'; +import { CoordsDTO } from './coords.dto.js'; export class CreateOfferDTO { + @IsString({ message: OfferValidationMessage.title.invalidFormat }) + @MinLength(OFFER_TITLE_LENGTH.MIN, { message: OfferValidationMessage.title.minLength }) + @MaxLength(OFFER_TITLE_LENGTH.MAX, { message: OfferValidationMessage.title.maxLength }) public title!: string; + + @IsString({ message: OfferValidationMessage.description.invalidFormat }) + @MinLength(OFFER_DESCRIPTION_LENGTH.MIN, { message: OfferValidationMessage.description.minLength }) + @MaxLength(OFFER_DESCRIPTION_LENGTH.MAX, { message: OfferValidationMessage.description.maxLength }) public description!: string; + + @IsEnum(ECity, { message: OfferValidationMessage.city.invalid }) public city!: string; + + @IsString({ message: OfferValidationMessage.previewImagePath.invalidFormat }) public previewImagePath!: string; + + @IsArray({ message: OfferValidationMessage.photos.invalidFormat }) + @ArrayMinSize(PHOTOS_LENGTH, { message: OfferValidationMessage.photos.invalidLength }) + @ArrayMaxSize(PHOTOS_LENGTH, { message: OfferValidationMessage.photos.invalidLength }) public photos!: string[]; + + @IsBoolean({ message: OfferValidationMessage.isPremium.invalidFormat }) public isPremium!: boolean; + + @IsEnum(EHousing, { message: OfferValidationMessage.housingType.invalid }) public housingType!: EHousing; + + @IsInt({ message: OfferValidationMessage.roomsNumber.invalidFormat }) + @Min(ROOMS_NUMBER.MIN, { message: OfferValidationMessage.roomsNumber.min }) + @Max(ROOMS_NUMBER.MAX, { message: OfferValidationMessage.roomsNumber.max }) public roomsNumber!: number; + + @IsInt({ message: OfferValidationMessage.visitorsNumber.invalidFormat }) + @Min(VISITORS_NUMBER.MIN, { message: OfferValidationMessage.visitorsNumber.min }) + @Max(VISITORS_NUMBER.MAX, { message: OfferValidationMessage.visitorsNumber.max }) public visitorsNumber!: number; + + @IsInt({ message: OfferValidationMessage.price.invalidFormat }) + @Min(PRICE.MIN, { message: OfferValidationMessage.price.min }) + @Max(PRICE.MAX, { message: OfferValidationMessage.price.max }) public price!: number; + + @IsArray({ message: OfferValidationMessage.facilities.invalidFormat }) + @ArrayUnique({message: OfferValidationMessage.facilities.invalid}) public facilities!: EFacilities[]; + + @IsMongoId({ message: OfferValidationMessage.authorId.invalid }) public authorId!: string; - public coords!: TCoords; + + @ValidateNested() + @IsObject({ message: OfferValidationMessage.coords.invalidFormat }) + @Type(() => CoordsDTO) + public coords!: CoordsDTO; } diff --git a/src/shared/modules/offer/dto/offer.message.ts b/src/shared/modules/offer/dto/offer.message.ts new file mode 100644 index 0000000..f0344ee --- /dev/null +++ b/src/shared/modules/offer/dto/offer.message.ts @@ -0,0 +1,67 @@ +import { + OFFER_TITLE_LENGTH, + OFFER_DESCRIPTION_LENGTH, + PHOTOS_LENGTH, + ROOMS_NUMBER, + VISITORS_NUMBER, +} from '../../../constants/index.js'; + +export const OfferValidationMessage = { + title: { + invalidFormat: 'Field title must be a string', + minLength: `Minimum title length must be ${OFFER_TITLE_LENGTH.MIN}`, + maxLength: `Maximum title length must be ${OFFER_TITLE_LENGTH.MAX}`, + }, + description: { + invalidFormat: 'Field description must be a string', + minLength: `Minimum description length must be ${OFFER_DESCRIPTION_LENGTH.MIN}`, + maxLength: `Maximum description length must be ${OFFER_DESCRIPTION_LENGTH.MAX}`, + }, + city: { + invalid: 'city must be Paris, Cologne, Brussels, Amsterdam, Hamburg or Dusseldorf', + }, + previewImagePath: { + invalidFormat: 'Field photos must be a string', + }, + photos: { + invalidFormat: 'Field photos must be an array', + invalidLength: `Photos length must be ${PHOTOS_LENGTH}`, + }, + isPremium: { + invalidFormat: 'Field isPremium must be a boolean', + }, + housingType: { + invalid: 'housingType must be apartment, house, room or hotel', + }, + roomsNumber: { + invalidFormat: 'Field roomsNumber must be an integer', + min: `Min roomsNumber must be ${ROOMS_NUMBER.MIN}`, + max: `Max roomsNumber must be ${ROOMS_NUMBER.MAX}`, + }, + visitorsNumber: { + invalidFormat: 'Field visitorsNumber must be an integer', + min: `Min visitorsNumber must be ${VISITORS_NUMBER.MIN}`, + max: `Max visitorsNumber must be ${VISITORS_NUMBER.MAX}`, + }, + price: { + invalidFormat: 'Field price must be an integer', + min: `Min price must be ${VISITORS_NUMBER.MIN}`, + max: `Max price must be ${VISITORS_NUMBER.MAX}`, + }, + facilities: { + invalidFormat: 'Field photos must be an array', + invalid: 'facilities must be an unique array of Breakfast, Air conditioning, Laptop friendly workspace, Baby seat, Washer, Towels or Fridge', + }, + authorId: { + invalid: 'Field authorId must be a valid MongoDB identifier', + }, + coords: { + invalidFormat: 'Field coords must be an object of latitude and longitude fields', + latitude: { + invalidFormat: 'Field latitude must be an integer', + }, + longitude: { + invalidFormat: 'Field longitude must be an integer', + }, + } +}; diff --git a/src/shared/modules/offer/dto/update-offer.dto.ts b/src/shared/modules/offer/dto/update-offer.dto.ts index 6a40cc2..6bac593 100644 --- a/src/shared/modules/offer/dto/update-offer.dto.ts +++ b/src/shared/modules/offer/dto/update-offer.dto.ts @@ -1,16 +1,96 @@ -import { EHousing, EFacilities, TCoords } from '../../../types/index.js'; +import { + IsInt, + IsString, + IsEnum, + IsBoolean, + IsArray, + IsObject, + ArrayUnique, + ArrayMinSize, + ArrayMaxSize, + MinLength, + MaxLength, + Min, + Max, + ValidateNested, + IsOptional, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +import { EHousing, EFacilities, ECity } from '../../../types/index.js'; +import { + OFFER_TITLE_LENGTH, + OFFER_DESCRIPTION_LENGTH, + PHOTOS_LENGTH, + ROOMS_NUMBER, + VISITORS_NUMBER, + PRICE, +} from '../../../constants/index.js'; +import { OfferValidationMessage } from './offer.message.js'; +import { CoordsDTO } from './coords.dto.js'; + export class UpdateOfferDTO { + @IsOptional() + @IsString({ message: OfferValidationMessage.title.invalidFormat }) + @MinLength(OFFER_TITLE_LENGTH.MIN, { message: OfferValidationMessage.title.minLength }) + @MaxLength(OFFER_TITLE_LENGTH.MAX, { message: OfferValidationMessage.title.maxLength }) public title?: string; + + @IsOptional() + @IsString({ message: OfferValidationMessage.description.invalidFormat }) + @MinLength(OFFER_DESCRIPTION_LENGTH.MIN, { message: OfferValidationMessage.description.minLength }) + @MaxLength(OFFER_DESCRIPTION_LENGTH.MAX, { message: OfferValidationMessage.description.maxLength }) public description?: string; + + @IsOptional() + @IsEnum(ECity, { message: OfferValidationMessage.city.invalid }) public city?: string; + + @IsOptional() + @IsString({ message: OfferValidationMessage.previewImagePath.invalidFormat }) public previewImagePath?: string; + + @IsOptional() + @IsArray({ message: OfferValidationMessage.photos.invalidFormat }) + @ArrayMinSize(PHOTOS_LENGTH, { message: OfferValidationMessage.photos.invalidLength }) + @ArrayMaxSize(PHOTOS_LENGTH, { message: OfferValidationMessage.photos.invalidLength }) public photos?: string[]; + + @IsOptional() + @IsBoolean({ message: OfferValidationMessage.isPremium.invalidFormat }) public isPremium?: boolean; + + @IsOptional() + @IsEnum(EHousing, { message: OfferValidationMessage.housingType.invalid }) public housingType?: EHousing; + + @IsOptional() + @IsInt({ message: OfferValidationMessage.roomsNumber.invalidFormat }) + @Min(ROOMS_NUMBER.MIN, { message: OfferValidationMessage.roomsNumber.min }) + @Max(ROOMS_NUMBER.MAX, { message: OfferValidationMessage.roomsNumber.max }) public roomsNumber?: number; + + @IsOptional() + @IsInt({ message: OfferValidationMessage.visitorsNumber.invalidFormat }) + @Min(VISITORS_NUMBER.MIN, { message: OfferValidationMessage.visitorsNumber.min }) + @Max(VISITORS_NUMBER.MAX, { message: OfferValidationMessage.visitorsNumber.max }) public visitorsNumber?: number; + + @IsOptional() + @IsInt({ message: OfferValidationMessage.price.invalidFormat }) + @Min(PRICE.MIN, { message: OfferValidationMessage.price.min }) + @Max(PRICE.MAX, { message: OfferValidationMessage.price.max }) public price?: number; + + @IsOptional() + @IsArray({ message: OfferValidationMessage.facilities.invalidFormat }) + @ArrayUnique({message: OfferValidationMessage.facilities.invalid}) public facilities?: EFacilities[]; - public coords?: TCoords; + + @IsOptional() + @ValidateNested() + @IsObject({ message: OfferValidationMessage.coords.invalidFormat }) + @Type(() => CoordsDTO) + public coords?: CoordsDTO; } diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index a033bc0..50b7487 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -1,12 +1,18 @@ import { inject, injectable } from 'inversify'; import { Request, Response } from 'express'; -import { StatusCodes } from 'http-status-codes'; +// import { StatusCodes } from 'http-status-codes'; -import { BaseController, HttpError } from '../../../rest/index.js'; +import { + BaseController, + // HttpError, + ValidateObjectIdMiddleware, + ValidateDTOMiddleware, + DocumentExistsMiddleware, +} from '../../../rest/index.js'; import { EHttpMethod } from '../../../rest/types/index.js'; -import { COMPONENT } from '../../constants/index.js'; +import { COMPONENT, RADIX } from '../../constants/index.js'; import { ILogger } from '../../libs/logger/types/index.js'; -import { IOfferService } from './types/index.js'; +import { IOfferService, TQueryCount } from './types/index.js'; import { CreateOfferDTO, ShortOfferRDO, UpdateOfferDTO, FullOfferRDO } from './index.js'; import { fillDTO } from '../../helpers/index.js'; @@ -21,14 +27,44 @@ export class OfferController extends BaseController { this.logger.info('Register routes for OfferController...'); this.addRoute({ path: '/', method: EHttpMethod.Get, handler: this.index }); - this.addRoute({ path: '/', method: EHttpMethod.Post, handler: this.create }); - this.addRoute({ path: '/:offerId', method: EHttpMethod.Get, handler: this.show}); - this.addRoute({ path: '/:offerId', method: EHttpMethod.Delete, handler: this.delete}); - this.addRoute({ path: '/:offerId', method: EHttpMethod.Patch, handler: this.update}); + this.addRoute({ + path: '/', + method: EHttpMethod.Post, + handler: this.create, + middlewares: [new ValidateDTOMiddleware(CreateOfferDTO)] + }); + this.addRoute({ + path: '/:offerId', + method: EHttpMethod.Get, + handler: this.show, + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); + this.addRoute({ + path: '/:offerId', + method: EHttpMethod.Delete, + handler: this.delete, + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); + this.addRoute({ + path: '/:offerId', + method: EHttpMethod.Patch, + handler: this.update, + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new ValidateDTOMiddleware(UpdateOfferDTO), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); } - public async index(_req: Request, res: Response): Promise { - const offers = await this.offerService.find(); + public async index({ query }: Request, res: Response): Promise { + const offers = await this.offerService.find(Number.parseInt(query?.count as string, RADIX)); const responseData = fillDTO(ShortOfferRDO, offers); this.ok(res, responseData); } @@ -44,21 +80,12 @@ export class OfferController extends BaseController { public async show(req: Request, res: Response): Promise { const existsOffer = await this.offerService.findById(req.params.offerId); - if (!existsOffer) { - throw new HttpError( - StatusCodes.NOT_FOUND, - `Offer with id ${req.params.offerId} not found.`, - 'OfferController', - ); - } - const responseData = fillDTO(FullOfferRDO, existsOffer); this.ok(res, responseData); } public async delete(req: Request, res: Response): Promise { const result = await this.offerService.deleteById(req.params.offerId); - this.noContent(res, result); } @@ -67,15 +94,6 @@ export class OfferController extends BaseController { res: Response, ): Promise { const result = await this.offerService.updateById(req.params.offerId, req.body as UpdateOfferDTO); - - if (!result) { - throw new HttpError( - StatusCodes.NOT_FOUND, - `Offer with id ${req.params.offerId} not found.`, - 'OfferController', - ); - } - this.ok(res, fillDTO(FullOfferRDO, result)); } } diff --git a/src/shared/modules/offer/offer.http b/src/shared/modules/offer/offer.http index 7aca9d3..9038c17 100644 --- a/src/shared/modules/offer/offer.http +++ b/src/shared/modules/offer/offer.http @@ -1,6 +1,6 @@ # Предложения ## Получить список предложений -GET http://localhost:4000/offers HTTP/1.1 +GET http://localhost:4000/offers?count=2 HTTP/1.1 Content-Type: application/json ### @@ -22,7 +22,7 @@ PATCH http://localhost:4000/offers/66f947e7e706754fb39b93a9 HTTP/1.1 Content-Type: application/json { - "title": "Измененный" + "title": "d" } ### diff --git a/src/shared/modules/offer/types/index.ts b/src/shared/modules/offer/types/index.ts index 7548169..5a1e184 100644 --- a/src/shared/modules/offer/types/index.ts +++ b/src/shared/modules/offer/types/index.ts @@ -1,2 +1,3 @@ export { IOfferService } from './offer-service.interface.js'; export { TParamOfferId } from './param-offerid.type.js'; +export { TQueryCount } from './query-count.type.js'; diff --git a/src/shared/modules/offer/types/offer-service.interface.ts b/src/shared/modules/offer/types/offer-service.interface.ts index c28a38f..a801fe9 100644 --- a/src/shared/modules/offer/types/offer-service.interface.ts +++ b/src/shared/modules/offer/types/offer-service.interface.ts @@ -1,8 +1,9 @@ import { DocumentType } from '@typegoose/typegoose'; import { CreateOfferDTO, UpdateOfferDTO, OfferEntity } from '../index.js'; +import { IDocumentExists } from '../../../../rest/types/index.js'; -export interface IOfferService { +export interface IOfferService extends IDocumentExists { create(dto: CreateOfferDTO): Promise>; findById(offerId: string): Promise | null>; find(count?: number): Promise[]>; diff --git a/src/shared/modules/offer/types/query-count.type.ts b/src/shared/modules/offer/types/query-count.type.ts new file mode 100644 index 0000000..04f1da2 --- /dev/null +++ b/src/shared/modules/offer/types/query-count.type.ts @@ -0,0 +1,5 @@ +import { ParamsDictionary } from 'express-serve-static-core'; + +export type TQueryCount = { + count?: number +} | ParamsDictionary; diff --git a/src/shared/modules/user/default-user.service.ts b/src/shared/modules/user/default-user.service.ts index ebc0d94..f10722c 100644 --- a/src/shared/modules/user/default-user.service.ts +++ b/src/shared/modules/user/default-user.service.ts @@ -3,7 +3,7 @@ import { DocumentType, types } from '@typegoose/typegoose'; import { inject, injectable } from 'inversify'; import { IUserService } from './types/index.js'; -import { UserEntity, CreateUserDTO, UpdateUserDTO, populateFavorites } from './index.js'; +import { UserEntity, CreateUserDTO, populateFavorites } from './index.js'; import { COMPONENT } from '../../constants/index.js'; import { ILogger } from '../../libs/logger/types/index.js'; import { OfferEntity } from '../offer/index.js'; @@ -45,12 +45,6 @@ export class DefaultUserService implements IUserService { return this.create(dto, salt); } - public async updateById(userId: string, dto: UpdateUserDTO): Promise | null> { - return this.userModel - .findByIdAndUpdate(userId, dto, { new: true }) - .exec(); - } - public async getFavorites(userId: string): Promise | null> { const result = await this.userModel .aggregate([ @@ -74,6 +68,7 @@ export class DefaultUserService implements IUserService { }, ]) .exec(); + // TODO aggregate offers return result[0] ? result[0].favoriteObjects : null; } diff --git a/src/shared/modules/user/dto/create-user.dto.ts b/src/shared/modules/user/dto/create-user.dto.ts index 6abf246..7d014d3 100644 --- a/src/shared/modules/user/dto/create-user.dto.ts +++ b/src/shared/modules/user/dto/create-user.dto.ts @@ -1,10 +1,34 @@ +import { + IsString, + MinLength, + MaxLength, + IsEnum, + IsEmail, + IsOptional, +} from 'class-validator'; + +import { USER_NAME_LENGTH, USER_PASSWORD_LENGTH } from '../../../constants/index.js'; import { EUserType } from '../../../types/index.js'; +import { UserValidationMessage } from './user.message.js'; export class CreateUserDTO { + @IsEmail({}, { message: UserValidationMessage.email.invalidFormat }) public email!: string; + + @IsString({ message: UserValidationMessage.name.invalidFormat }) + @MinLength(USER_NAME_LENGTH.MIN, { message: UserValidationMessage.name.minLength }) + @MaxLength(USER_NAME_LENGTH.MAX, { message: UserValidationMessage.name.maxLength }) public name!: string; - public avatarPath!: string; + + @IsOptional() + @IsString({ message: UserValidationMessage.avatarPath.invalidFormat }) + public avatarPath?: string; + + @IsEnum(EUserType, { message: UserValidationMessage.type.invalid }) public type!: EUserType; + + @IsString({ message: UserValidationMessage.password.invalidFormat }) + @MinLength(USER_PASSWORD_LENGTH.MIN, { message: UserValidationMessage.password.minLength }) + @MaxLength(USER_PASSWORD_LENGTH.MAX, { message: UserValidationMessage.password.maxLength }) public password!: string; - public favorites?: string[]; } diff --git a/src/shared/modules/user/dto/login-user.dto.ts b/src/shared/modules/user/dto/login-user.dto.ts index de21a41..e0afa9d 100644 --- a/src/shared/modules/user/dto/login-user.dto.ts +++ b/src/shared/modules/user/dto/login-user.dto.ts @@ -1,4 +1,13 @@ +import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator'; +import { UserValidationMessage } from './user.message.js'; +import { USER_PASSWORD_LENGTH } from '../../../constants/index.js'; + export class LoginUserDTO { + @IsEmail({}, { message: UserValidationMessage.email.invalidFormat }) public email!: string; + + @IsString({ message: UserValidationMessage.password.invalidFormat }) + @MinLength(USER_PASSWORD_LENGTH.MIN, { message: UserValidationMessage.password.minLength }) + @MaxLength(USER_PASSWORD_LENGTH.MAX, { message: UserValidationMessage.password.maxLength }) public password!: string; } diff --git a/src/shared/modules/user/dto/update-user.dto.ts b/src/shared/modules/user/dto/update-user.dto.ts deleted file mode 100644 index 2dd8fba..0000000 --- a/src/shared/modules/user/dto/update-user.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EUserType } from '../../../types/index.js'; - -export class UpdateUserDTO { - public email?: string; - public avatarPath?: string; - public name?: string; - public type?: EUserType; - public password?: string; -} diff --git a/src/shared/modules/user/dto/user.message.ts b/src/shared/modules/user/dto/user.message.ts new file mode 100644 index 0000000..26564e0 --- /dev/null +++ b/src/shared/modules/user/dto/user.message.ts @@ -0,0 +1,23 @@ +import { USER_NAME_LENGTH, USER_PASSWORD_LENGTH } from '../../../constants/index.js'; + +export const UserValidationMessage = { + email: { + invalidFormat: 'Email must be a valid email format', + }, + name: { + invalidFormat: 'Field name must be a string', + minLength: `Minimum name length must be ${USER_NAME_LENGTH.MIN}`, + maxLength: `Maximum name length must be ${USER_NAME_LENGTH.MAX}`, + }, + avatarPath: { + invalidFormat: 'Field name must be a string', + }, + type: { + invalid: 'Field type must be pro or обычный', + }, + password: { + invalidFormat: 'Field password must be a string', + minLength: `Minimum password length must be ${USER_PASSWORD_LENGTH.MIN}`, + maxLength: `Maximum password length must be ${USER_PASSWORD_LENGTH.MAX}`, + } +}; diff --git a/src/shared/modules/user/index.ts b/src/shared/modules/user/index.ts index 24d72ad..d18dc6c 100644 --- a/src/shared/modules/user/index.ts +++ b/src/shared/modules/user/index.ts @@ -1,5 +1,4 @@ export { CreateUserDTO } from './dto/create-user.dto.js'; -export { UpdateUserDTO } from './dto/update-user.dto.js'; export { LoginUserDTO } from './dto/login-user.dto.js'; export { UserRDO } from './rdo/user.rdo.js'; export { UserEntity, UserModel } from './user.entity.js'; diff --git a/src/shared/modules/user/types/user-service.interface.ts b/src/shared/modules/user/types/user-service.interface.ts index 8ffb345..e2f5528 100644 --- a/src/shared/modules/user/types/user-service.interface.ts +++ b/src/shared/modules/user/types/user-service.interface.ts @@ -1,13 +1,12 @@ import { DocumentType } from '@typegoose/typegoose'; -import { CreateUserDTO, UserEntity, UpdateUserDTO } from '../index.js'; +import { CreateUserDTO, UserEntity } from '../index.js'; import { OfferEntity } from '../../offer/offer.entity.js'; export interface IUserService { create(dto: CreateUserDTO, salt: string): Promise>; findByEmail(email: string): Promise | null>; findOrCreate(dto: CreateUserDTO, salt: string): Promise>; - updateById(userId: string, dto: UpdateUserDTO): Promise | null>; getFavorites(userId: string): Promise | null>; addFavorite(userId: string, offerId: string): Promise | null>; deleteFavorite(userId: string, offerId: string): Promise | null>; diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 662f9a2..55b851e 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -2,15 +2,21 @@ import { inject, injectable } from 'inversify'; import { Response, Request } from 'express'; import { StatusCodes } from 'http-status-codes'; -import { BaseController, HttpError } from '../../../rest/index.js'; +import { + BaseController, + HttpError, + ValidateObjectIdMiddleware, + ValidateDTOMiddleware, + DocumentExistsMiddleware, +} from '../../../rest/index.js'; import { EHttpMethod } from '../../../rest/types/index.js'; import { ILogger } from '../../libs/logger/types/index.js'; import { COMPONENT } from '../../constants/index.js'; import { IUserService, TCreateUserRequest, TLoginUserRequest } from './types/index.js'; import { IConfig, TRestSchema } from '../../libs/config/types/index.js'; import { fillDTO } from '../../helpers/index.js'; -import { UserRDO } from './index.js'; -import { TParamOfferId } from '../offer/types/index.js'; +import { CreateUserDTO, LoginUserDTO, UserRDO } from './index.js'; +import { IOfferService, TParamOfferId } from '../offer/types/index.js'; import { ShortOfferRDO } from '../offer/index.js'; const MOCK_USER = '66f947e7e706754fb39b93a7'; @@ -20,17 +26,44 @@ export class UserController extends BaseController { constructor( @inject(COMPONENT.LOGGER) protected readonly logger: ILogger, @inject(COMPONENT.USER_SERVICE) private readonly userService: IUserService, + @inject(COMPONENT.OFFER_SERVICE) private readonly offerService: IOfferService, @inject(COMPONENT.CONFIG) private readonly config: IConfig, ) { super(logger); this.logger.info('Register routes for UserController...'); - this.addRoute({ path: '/register', method: EHttpMethod.Post, handler: this.create }); - this.addRoute({ path: '/login', method: EHttpMethod.Post, handler: this.login }); + this.addRoute({ + path: '/register', + method: EHttpMethod.Post, + handler: this.create, + middlewares: [new ValidateDTOMiddleware(CreateUserDTO)], + }); + this.addRoute({ + path: '/login', + method: EHttpMethod.Post, + handler: this.login, + middlewares: [new ValidateDTOMiddleware(LoginUserDTO)], + }); this.addRoute({ path: '/favorites/', method: EHttpMethod.Get, handler: this.showFavorites }); - this.addRoute({ path: '/favorites/:offerId', method: EHttpMethod.Post, handler: this.addFavorite }); - this.addRoute({ path: '/favorites/:offerId', method: EHttpMethod.Delete, handler: this.deleteFavorite }); + this.addRoute({ + path: '/favorites/:offerId', + method: EHttpMethod.Post, + handler: this.addFavorite, + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); + this.addRoute({ + path: '/favorites/:offerId', + method: EHttpMethod.Delete, + handler: this.deleteFavorite, + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); } public async create( @@ -80,6 +113,7 @@ export class UserController extends BaseController { public async addFavorite({ params }: Request, res: Response): Promise { const result = await this.userService.addFavorite(MOCK_USER, params.offerId); this.ok(res, fillDTO(UserRDO, result)); + // TODO 409 } public async deleteFavorite({ params }: Request, res: Response): Promise { diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts index 63677d0..0140adf 100644 --- a/src/shared/modules/user/user.entity.ts +++ b/src/shared/modules/user/user.entity.ts @@ -4,6 +4,7 @@ import { EUserType, IUser } from '../../types/index.js'; import { createSHA256 } from '../../helpers/index.js'; import { OfferEntity } from '../offer/index.js'; import { DEFAULT_AVATAR } from '../../constants/index.js'; +import { CreateUserDTO } from './index.js'; // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export interface UserEntity extends defaultClasses.Base {} @@ -20,7 +21,7 @@ export class UserEntity extends defaultClasses.TimeStamps implements IUser { @prop({ unique: true, required: true }) public email: string; - @prop({ required: true }) + @prop() public avatarPath: string; @prop({ required: true }) @@ -33,13 +34,12 @@ export class UserEntity extends defaultClasses.TimeStamps implements IUser { private password?: string; @prop({ - required: true, ref: () => OfferEntity, default: [], }) public favorites?: Ref[]; - constructor(userData: IUser, password: string, salt: string) { + constructor(userData: CreateUserDTO, password: string, salt: string) { super(); this.email = userData.email; diff --git a/src/shared/types/city.enum.ts b/src/shared/types/city.enum.ts new file mode 100644 index 0000000..6a91b19 --- /dev/null +++ b/src/shared/types/city.enum.ts @@ -0,0 +1,8 @@ +export enum ECity { + Paris = 'Paris', + Cologne = 'Cologne', + Brussels = 'Brussels', + Amsterdam = 'Amsterdam', + Hamburg = 'Hamburg', + Dusseldorf = 'Dusseldorf', +} diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index da51dde..c0fc706 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -6,3 +6,4 @@ export { TMockServerData } from './mock-server-data.type.js'; export { IUser } from './user.interface.js'; export { IOffer } from './offer.interface.js'; export { ESortType } from './sort-type.enum.js'; +export { ECity } from './city.enum.js'; diff --git a/src/shared/types/user-type.enum.ts b/src/shared/types/user-type.enum.ts index b20203f..32468d2 100644 --- a/src/shared/types/user-type.enum.ts +++ b/src/shared/types/user-type.enum.ts @@ -1,4 +1,4 @@ export enum EUserType { - Regular = 'обычный', - Pro = 'pro', + Regular = 'обычный', + Pro = 'pro', }