diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..bddbd78 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,7 @@ +{ + "watch": [ + "src" + ], + "ext": "ts, json", + "exec": "npm run ts ./src/main.rest.ts | pino-pretty --colorize --translateTime SYS:standard" +} diff --git a/package-lock.json b/package-lock.json index 18c74f9..cc9eee4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "eslint": "8.49.0", "eslint-config-htmlacademy": "9.1.1", "lodash": "^4.17.21", + "nodemon": "^3.1.7", "prettier": "^3.3.3", "rimraf": "5.0.1", "ts-node": "10.9.2", @@ -1010,6 +1011,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1209,6 +1223,18 @@ } ] }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2611,6 +2637,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2984,6 +3024,12 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3119,6 +3165,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -3963,6 +4021,104 @@ "node": ">= 0.6" } }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -3984,6 +4140,15 @@ "semver": "bin/semver" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", @@ -4409,6 +4574,12 @@ "react-is": "^16.13.1" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -4970,6 +5141,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -5337,6 +5520,15 @@ "node": ">=6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tr46": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", @@ -5533,6 +5725,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index 03e2cd3..f980418 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ ], "main": "main.js", "scripts": { - "start:dev": "npm run ts ./src/main.rest.ts | pino-pretty --colorize --translateTime SYS:standard", + "start": "npm run build && node ./dist/main.rest.js", + "start:dev": "nodemon", "build": "npm run clean && npm run compile", "lint": "eslint src/ --ext .ts", "compile": "tsc -p tsconfig.json", @@ -28,6 +29,7 @@ "eslint": "8.49.0", "eslint-config-htmlacademy": "9.1.1", "lodash": "^4.17.21", + "nodemon": "^3.1.7", "prettier": "^3.3.3", "rimraf": "5.0.1", "ts-node": "10.9.2", diff --git a/specification/project.spec.yml b/specification/project.spec.yml new file mode 100644 index 0000000..047f5c0 --- /dev/null +++ b/specification/project.spec.yml @@ -0,0 +1,64 @@ +openapi: 3.1.0 +info: + title: API Server for Demo Project "Six Cities" + description: |- + * List of resources and routes for the "Six Cities" server. + license: + name: MIT + url: https://opensource.org/licenses/MIT + version: 2.0.0 +tags: + - name: offers + description: Actions related to offers. + - name: comments + description: Actions related to comments. + - name: users + description: Actions related to the user. +paths: + /users/register: + post: + tags: + - users + summary: User registration + description: Registers a new user. + + requestBody: + description: Information for creating a new user. + content: + application/json: + schema: + $ref: '#/components/schemas/createUser' + required: true + + responses: + "201": + description: User registered. User object. + content: + application/json: + schema: + $ref: '#/components/schemas/user' + + "409": + description: A user with this email already exists. + + + /users/login: + post: + tags: + - users + summary: User authentication + description: Authenticates a user based on login and password. + + get: + tags: + - users + summary: User status check + description: Returns information about the authenticated user. + + /users/{userId}/avatar: + post: + tags: + - users + summary: Upload avatar image + description: Uploads the user's avatar image. The avatar image must be in + `png` or `jpg` format. diff --git a/src/main.rest.ts b/src/main.rest.ts index 363e115..f529ba2 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -6,12 +6,14 @@ import { Component } from './shared/types/index.js'; import { createRestApplicationContainer } from './rest/rest.container.js'; import { createUserContainer } from './shared/modules/user/index.js'; import { createOfferContainer } from './shared/modules/offer/index.js'; +import { createCommentContainer } from './shared/modules/comment/index.js'; async function bootstrap() { const appContainer = Container.merge( createRestApplicationContainer(), createUserContainer(), createOfferContainer(), + createCommentContainer(), ); const application = appContainer.get(Component.RestApplication); diff --git a/src/shared/constants.ts b/src/shared/constants.ts new file mode 100644 index 0000000..1ff9c58 --- /dev/null +++ b/src/shared/constants.ts @@ -0,0 +1 @@ +export const TSV_SEPARATOR = '|'; diff --git a/src/shared/libs/file-reader/tsv-file-reader.ts b/src/shared/libs/file-reader/tsv-file-reader.ts index 93de95b..81f3542 100644 --- a/src/shared/libs/file-reader/tsv-file-reader.ts +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -3,6 +3,7 @@ import { createReadStream } from 'node:fs'; import { FileReader } from './file-reader.interface.js'; import { Offer} from '../../types/index.js'; +import { TSV_SEPARATOR } from '../../constants.js'; export class TSVFileReader extends EventEmitter implements FileReader { private CHUNK_SIZE = 16384; // 16KB @@ -13,6 +14,7 @@ export class TSVFileReader extends EventEmitter implements FileReader { super(); } + separator: string = TSV_SEPARATOR; private parseLineToOffer(line: string): Offer { const [ @@ -38,12 +40,12 @@ export class TSVFileReader extends EventEmitter implements FileReader { createdData: new Date(createdDate), city, previewImage, - images: images.split('|'), + images: images.split(this.separator), premium: premium.toLocaleLowerCase() === 'true', rating: Number(rating), bedrooms: Number(bedrooms), guests: Number(guests), - amenities: amenities.split('|'), + amenities: amenities.split(this.separator), autor: { name: parseAutor[0], avatarPath: parseAutor[1], @@ -51,7 +53,7 @@ export class TSVFileReader extends EventEmitter implements FileReader { typeUser: parseAutor[3], }, price: Number(price), - coordinates: {latatude: Number(coordinates.split('|')[0]), longitude: Number(coordinates.split('|')[1])} + coordinates: {latatude: Number(coordinates.split(this.separator)[0]), longitude: Number(coordinates.split(this.separator)[1])} }; } diff --git a/src/shared/libs/offer-generator/tsv-offer-generator.ts b/src/shared/libs/offer-generator/tsv-offer-generator.ts index 616bd8c..b20676c 100644 --- a/src/shared/libs/offer-generator/tsv-offer-generator.ts +++ b/src/shared/libs/offer-generator/tsv-offer-generator.ts @@ -8,28 +8,29 @@ import { getRandomItem, getRandomItems, } from '../../helpers/index.js'; +import { TSV_SEPARATOR } from '../../constants.js'; const FIRST_WEEK_DAY = 1; const LAST_WEEK_DAY = 7; export class TSVOfferGenerator implements OfferGenerator { constructor(private readonly mockData: MockServerData) {} - + separator: string = TSV_SEPARATOR; public generate(): string { const name = getRandomItem(this.mockData.name); const describe = getRandomItem(this.mockData.describe); const city = getRandomItem(this.mockData.city); const previewImage = getRandomItem(this.mockData.previewImage); - const images = getRandomItems(this.mockData.images).join('|'); + const images = getRandomItems(this.mockData.images).join(this.separator); const premium = !!generateRandomValue(0, 1); const price = generateRandomValue(0, 5); const rating = generateRandomValue(0, 5); const bedrooms = generateRandomValue(0, 5); const guests = generateRandomValue(0, 5); - const amenities = getRandomItems(this.mockData.amenities).join('|'); + const amenities = getRandomItems(this.mockData.amenities).join(this.separator); const autorRandom = getRandomItem(this.mockData.autor); const autor = `${autorRandom.name}|${autorRandom.avatarPath}|${autorRandom.email}|${autorRandom.typeUser}`; - const coordinates = `${generateRandomValue(1000, 3000).toString()}|${generateRandomValue(1000, 3000).toString()}`; + const coordinates = `${generateRandomValue(1000, 3000).toString()}${this.separator}${generateRandomValue(1000, 3000).toString()}`; const createData = dayjs() .subtract(generateRandomValue(FIRST_WEEK_DAY, LAST_WEEK_DAY), 'day') .toISOString(); diff --git a/src/shared/modules/comment/comment-service.interface.ts b/src/shared/modules/comment/comment-service.interface.ts new file mode 100644 index 0000000..67ba87c --- /dev/null +++ b/src/shared/modules/comment/comment-service.interface.ts @@ -0,0 +1,10 @@ +import { DocumentType } from '@typegoose/typegoose'; + +import { CreateCommentDto } from './dto/create-comment.dto.js'; +import { CommentEntity } from './comment.entity.js'; + +export interface CommentService { + create(dto: CreateCommentDto): Promise>; + findByOfferId(offerId: string): Promise[]>; + deleteByOfferId(offerId: string): Promise; +} diff --git a/src/shared/modules/comment/comment.container.ts b/src/shared/modules/comment/comment.container.ts new file mode 100644 index 0000000..cde6ea3 --- /dev/null +++ b/src/shared/modules/comment/comment.container.ts @@ -0,0 +1,20 @@ +import { Container } from 'inversify'; +import { types } from '@typegoose/typegoose'; + +import { CommentService } from './comment-service.interface.js'; +import { Component } from '../../types/index.js'; +import { CommentEntity, CommentModel } from './comment.entity.js'; +import { DefaultCommentService } from './default-comment.service.js'; + +export function createCommentContainer() { + const commentContainer = new Container(); + + commentContainer.bind(Component.CommentService) + .to(DefaultCommentService) + .inSingletonScope(); + + commentContainer.bind>(Component.CommentModel) + .toConstantValue(CommentModel); + + return commentContainer; +} diff --git a/src/shared/modules/comment/comment.entity.ts b/src/shared/modules/comment/comment.entity.ts new file mode 100644 index 0000000..1232b87 --- /dev/null +++ b/src/shared/modules/comment/comment.entity.ts @@ -0,0 +1,38 @@ +import { + defaultClasses, + getModelForClass, + modelOptions, + prop, + Ref +} from '@typegoose/typegoose'; + +import { OfferEntity } from '../offer/index.js'; +import { UserEntity } from '../user/index.js'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface CommentEntity extends defaultClasses.Base {} + +@modelOptions({ + schemaOptions: { + collection: 'comments' + } +}) +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class CommentEntity extends defaultClasses.TimeStamps { + @prop({ trim: true, required: true }) + public text: string; + + @prop({ + ref: OfferEntity, + required: true + }) + public offerId: Ref; + + @prop({ + ref: UserEntity, + required: true, + }) + public userId: Ref; +} + +export const CommentModel = getModelForClass(CommentEntity); diff --git a/src/shared/modules/comment/default-comment.service.ts b/src/shared/modules/comment/default-comment.service.ts new file mode 100644 index 0000000..4158842 --- /dev/null +++ b/src/shared/modules/comment/default-comment.service.ts @@ -0,0 +1,33 @@ +import { inject, injectable } from 'inversify'; +import { DocumentType, types } from '@typegoose/typegoose'; + +import { CommentService } from './comment-service.interface.js'; +import { Component } from '../../types/index.js'; +import { CommentEntity } from './comment.entity.js'; +import { CreateCommentDto } from './dto/create-comment.dto.js'; + +@injectable() +export class DefaultCommentService implements CommentService { + constructor( + @inject(Component.CommentModel) private readonly commentModel: types.ModelType + ) {} + + public async create(dto: CreateCommentDto): Promise> { + const comment = await this.commentModel.create(dto); + return comment.populate('userId'); + } + + public async findByOfferId(offerId: string): Promise[]> { + return this.commentModel + .find({offerId}) + .populate('userId'); + } + + public async deleteByOfferId(offerId: string): Promise { + const result = await this.commentModel + .deleteMany({offerId}) + .exec(); + + return result.deletedCount; + } +} diff --git a/src/shared/modules/comment/dto/create-comment.dto.ts b/src/shared/modules/comment/dto/create-comment.dto.ts new file mode 100644 index 0000000..8193860 --- /dev/null +++ b/src/shared/modules/comment/dto/create-comment.dto.ts @@ -0,0 +1,5 @@ +export class CreateCommentDto { + public text: string; + public offerId: string; + public userId: string; +} diff --git a/src/shared/modules/comment/index.ts b/src/shared/modules/comment/index.ts new file mode 100644 index 0000000..3a18cae --- /dev/null +++ b/src/shared/modules/comment/index.ts @@ -0,0 +1,4 @@ +export { CommentService } from './comment-service.interface.js'; +export { createCommentContainer } from './comment.container.js'; +export { CommentEntity, CommentModel } from './comment.entity.js'; +export { DefaultCommentService } from './default-comment.service.js'; diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index 9d68160..4f5b91b 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -6,6 +6,9 @@ import { Component } from '../../types/index.js'; import { Logger } from '../../libs/logger/index.js'; import { OfferEntity } from './offer.entity.js'; import { CreateOfferDto } from './dto/create-offer.dto.js'; +import { SortType } from '../../types/sort-type.enum.js'; +import { UpdateOfferDto } from './dto/update-offer.dto.js'; +import { DEFAULT_OFFER_COUNT } from './offer.constant.js'; @injectable() export class DefaultOfferService implements OfferService { @@ -21,7 +24,84 @@ export class DefaultOfferService implements OfferService { return result; } + public async find(): Promise[]> { + return this.offerModel + .find({DEFAULT_OFFER_COUNT}) + .populate(['userId']) + .exec(); + } + public async findById(offerId: string): Promise | null> { - return this.offerModel.findById(offerId).exec(); + return this.offerModel + .findById(offerId) + .populate(['userId']) + .exec(); + } + + public async deleteById(offerId: string): Promise | null> { + return this.offerModel + .findByIdAndDelete(offerId) + .exec(); + } + + public async updateById(offerId: string, dto: UpdateOfferDto): Promise | null> { + return this.offerModel + .findByIdAndUpdate(offerId, dto, {new: true}) + .populate(['userId']) + .exec(); + } + + public async exists(documentId: string): Promise { + return (await this.offerModel + .exists({_id: documentId})) !== null; + } + + public async incCommentCount(offerId: string): Promise | null> { + return this.offerModel + .findByIdAndUpdate(offerId, {'$inc': { + commentCount: 1, + }}).exec(); + } + + public async findFavorite(count: number): Promise[]> { + return this.offerModel + .find({ favorite: true }) + .sort({ createdAt: SortType.Down }) + .limit(count) + .populate(['userId']) + .exec(); + } + + public async findPremium( + count: number + ): Promise[]> { + return this.offerModel + .find({ premium: true }) + .sort({ commentCount: SortType.Down }) + .limit(count) + .populate(['userId']) + .exec(); + } + + public async calculateRating(offerId: string, newRating: number): Promise | null> { + const offer = await this.offerModel.findById(offerId).exec(); + if (!offer) { + return null; + } + + const totalRatings = offer.rating * offer.commentCount; + const newTotalRatings = totalRatings + newRating; + const newCommentCount = offer.commentCount + 1; + + const updatedRating = newTotalRatings / newCommentCount; + + return this.offerModel.findByIdAndUpdate( + offerId, + { + rating: updatedRating, + commentCount: newCommentCount, + }, + { new: true } + ).exec(); } } diff --git a/src/shared/modules/offer/dto/update-offer.dto.ts b/src/shared/modules/offer/dto/update-offer.dto.ts new file mode 100644 index 0000000..7e8a205 --- /dev/null +++ b/src/shared/modules/offer/dto/update-offer.dto.ts @@ -0,0 +1,17 @@ +import { Coordinates } from '../../../types/coordinates.type.js'; + +export class UpdateOfferDto { + public name?: string; + public description?: string; + public udateddData?: Date; + public city?: string; + public previewImage?: string; + public images?: string[]; + public premium?: boolean; + public rating?: number; + public bedrooms?: number; + public guests?: number; + public amenities?: string[]; + public price?: number; + public coordinates?: Coordinates; +} diff --git a/src/shared/modules/offer/index.ts b/src/shared/modules/offer/index.ts index 86e042a..22f7652 100644 --- a/src/shared/modules/offer/index.ts +++ b/src/shared/modules/offer/index.ts @@ -3,3 +3,4 @@ export { CreateOfferDto } from './dto/create-offer.dto.js'; export { OfferService } from './offer-service.interface.js'; export { DefaultOfferService } from './default-offer.service.js'; export { createOfferContainer } from './offer.container.js'; +export { UpdateOfferDto } from './dto/update-offer.dto.js'; diff --git a/src/shared/modules/offer/offer-service.interface.ts b/src/shared/modules/offer/offer-service.interface.ts index 04d4d80..0394c16 100644 --- a/src/shared/modules/offer/offer-service.interface.ts +++ b/src/shared/modules/offer/offer-service.interface.ts @@ -2,8 +2,16 @@ import { DocumentType } from '@typegoose/typegoose'; import { CreateOfferDto } from './dto/create-offer.dto.js'; import { OfferEntity } from './offer.entity.js'; +import { UpdateOfferDto } from './dto/update-offer.dto.js'; export interface OfferService { create(dto: CreateOfferDto): Promise>; findById(offerId: string): Promise | null>; + find(): Promise[]>; + deleteById(offerId: string): Promise | null>; + updateById(offerId: string, dto: UpdateOfferDto): Promise | null>; + incCommentCount(offerId: string): Promise | null>; + exists(documentId: string): Promise; + findFavorite(count: number): Promise[]>; + findPremium(count: number): Promise[]> } diff --git a/src/shared/modules/offer/offer.constant.ts b/src/shared/modules/offer/offer.constant.ts new file mode 100644 index 0000000..b4e098d --- /dev/null +++ b/src/shared/modules/offer/offer.constant.ts @@ -0,0 +1 @@ +export const DEFAULT_OFFER_COUNT = 60; diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index 483d19e..886cf79 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -59,9 +59,6 @@ export class OfferEntity extends defaultClasses.TimeStamps { @prop({ type: () => [String], required: true }) public amenities!: string[]; - // @prop() - // public autor!: string; - @prop() public price!: number; @@ -73,6 +70,9 @@ export class OfferEntity extends defaultClasses.TimeStamps { required: true }) public userId!: Ref; + + @prop({default: 0}) + public commentCount!: number; } export const OfferModel = getModelForClass(OfferEntity); diff --git a/src/shared/modules/user/default-user.service.ts b/src/shared/modules/user/default-user.service.ts index 35a9c29..2dc723a 100644 --- a/src/shared/modules/user/default-user.service.ts +++ b/src/shared/modules/user/default-user.service.ts @@ -6,6 +6,7 @@ import { UserEntity } from './user.entity.js'; import { CreateUserDto } from './dto/create-user.dto.js'; import { Component } from '../../types/index.js'; import { Logger } from '../../libs/logger/index.js'; +import { UpdateUserDto } from './dto/update-user.dto.js'; @injectable() export class DefaultUserService implements UserService { @@ -37,4 +38,10 @@ export class DefaultUserService implements UserService { return this.create(dto, salt); } + + public async updateById(userId: string, dto: UpdateUserDto): Promise | null> { + return this.userModel + .findByIdAndUpdate(userId, dto, { new: true }) + .exec(); + } } diff --git a/src/shared/modules/user/dto/update-user.dto.ts b/src/shared/modules/user/dto/update-user.dto.ts new file mode 100644 index 0000000..ccbe6e9 --- /dev/null +++ b/src/shared/modules/user/dto/update-user.dto.ts @@ -0,0 +1,5 @@ +export class UpdateUserDto { + public avatarPath?: string; + public typeUser?: string; + public name?: string; +} diff --git a/src/shared/modules/user/user-service.interface.ts b/src/shared/modules/user/user-service.interface.ts index 6a2d91b..4cf8dd9 100644 --- a/src/shared/modules/user/user-service.interface.ts +++ b/src/shared/modules/user/user-service.interface.ts @@ -2,9 +2,11 @@ import { DocumentType } from '@typegoose/typegoose'; import { UserEntity } from './user.entity.js'; import { CreateUserDto } from './dto/create-user.dto.js'; +import { UpdateUserDto } from './dto/update-user.dto.js'; export interface UserService { create(dto: CreateUserDto, salt: string): Promise>; findByEmail(email: string): Promise | null>; findOrCreate(dto: CreateUserDto, salt: string): Promise>; + updateById(userId: string, dto: UpdateUserDto): Promise | null>; } diff --git a/src/shared/types/component.enum.ts b/src/shared/types/component.enum.ts index fa27dfd..09d6c3b 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -7,4 +7,6 @@ export const Component = { UserModel: Symbol.for('UserModel'), OfferService: Symbol.for('OfferService'), OfferModel: Symbol.for('OfferModel'), + CommentService: Symbol.for('CommentService'), + CommentModel: Symbol.for('CommentModel'), } as const; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index ded8c20..0002b1c 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -5,3 +5,4 @@ export { User } from './user.type.js'; export { Offer } from './offer.type.js'; export { MockServerData } from './mock-server-data.type.js'; export { Component } from './component.enum.js'; +export { SortType } from './sort-type.enum.js'; diff --git a/src/shared/types/sort-type.enum.ts b/src/shared/types/sort-type.enum.ts new file mode 100644 index 0000000..727ba7c --- /dev/null +++ b/src/shared/types/sort-type.enum.ts @@ -0,0 +1,4 @@ +export enum SortType { + Down = -1, + Up = 1, +}