From b9656245e47ab2d89e3a9cb454cdadf30c331874 Mon Sep 17 00:00:00 2001 From: Ilya Kolmakov Date: Sun, 3 Nov 2024 23:17:30 +0500 Subject: [PATCH 1/2] module7-task2: add feature upload files --- .env-example | 3 +- .gitignore | 1 + package-lock.json | 297 +++++++++++++++++- package.json | 4 + src/cli/commands/import.command.ts | 11 +- src/mocks/mock-server-data.json | 2 +- src/rest/rest.application.ts | 4 + src/shared/constants/common.ts | 2 + src/shared/entities/comment.entity.ts | 2 +- src/shared/entities/offer.entity.ts | 2 +- .../libs/config/rest.schema.interface.ts | 1 + src/shared/libs/config/rest.schema.ts | 6 + .../libs/file-reader/tsv-file-reader.ts | 4 +- .../offer-generator/tsv-offer-generator.ts | 6 +- src/shared/libs/rest/index.ts | 5 + .../document-body-exists.middleware.ts | 36 +++ .../middleware/document-exists.middleware.ts | 2 +- .../document-query-exists.middleware.ts | 36 +++ .../rest/middleware/upload-file.middleware.ts | 50 +++ .../validate-objectid-body.middleware.ts | 24 ++ .../validate-objectid-query.middleware.ts | 24 ++ .../modules/comment/comment.constant.ts | 12 + .../modules/comment/comment.controller.ts | 32 +- src/shared/modules/comment/comment.http | 6 +- .../comment/default-comment.service.ts | 8 +- .../modules/comment/dto/create-comment.dto.ts | 17 +- .../comment/dto/create-comment.messages.ts | 17 - src/shared/modules/comment/rdo/comment.rdo.ts | 5 +- .../modules/offer/default-offer.service.ts | 39 ++- .../modules/offer/dto/coordinate.dto.ts | 9 + .../modules/offer/dto/create-offer.dto.ts | 52 ++- .../offer/dto/create-offer.messages.ts | 47 --- .../modules/offer/dto/update-offer.dto.ts | 44 +-- .../offer/dto/update-offer.messages.ts | 47 --- src/shared/modules/offer/index.ts | 1 - .../modules/offer/offer-service.interface.ts | 4 +- src/shared/modules/offer/offer.aggregation.ts | 29 +- src/shared/modules/offer/offer.constant.ts | 24 ++ src/shared/modules/offer/offer.controller.ts | 17 +- src/shared/modules/offer/offer.http | 9 +- .../modules/offer/rdo/full-offer.rdo.ts | 5 +- src/shared/modules/offer/rdo/id-offer.rdo.ts | 3 +- .../modules/offer/rdo/short-offer.rdo.ts | 3 +- .../modules/user/default-user.service.ts | 16 +- .../modules/user/dto/create-user.dto.ts | 17 +- .../modules/user/dto/create-user.messages.ts | 19 -- src/shared/modules/user/dto/login-user.dto.ts | 6 +- .../modules/user/dto/login-user.messages.ts | 8 - .../modules/user/dto/update-user.dto.ts | 16 +- .../modules/user/dto/update-user.messages.ts | 19 -- .../modules/user/user-service.interface.ts | 4 +- src/shared/modules/user/user.constant.ts | 11 + src/shared/modules/user/user.controller.ts | 91 ++++-- src/shared/modules/user/user.http | 39 +++ src/shared/types/comment.interface.ts | 2 +- .../types/mock-server-data.interface.ts | 2 +- src/shared/types/offer.interface.ts | 2 +- src/shared/types/sort-type.enum.ts | 4 +- src/specification/specification.yml | 36 ++- 59 files changed, 880 insertions(+), 364 deletions(-) create mode 100644 src/shared/libs/rest/middleware/document-body-exists.middleware.ts create mode 100644 src/shared/libs/rest/middleware/document-query-exists.middleware.ts create mode 100644 src/shared/libs/rest/middleware/upload-file.middleware.ts create mode 100644 src/shared/libs/rest/middleware/validate-objectid-body.middleware.ts create mode 100644 src/shared/libs/rest/middleware/validate-objectid-query.middleware.ts delete mode 100644 src/shared/modules/comment/dto/create-comment.messages.ts create mode 100644 src/shared/modules/offer/dto/coordinate.dto.ts delete mode 100644 src/shared/modules/offer/dto/create-offer.messages.ts delete mode 100644 src/shared/modules/offer/dto/update-offer.messages.ts delete mode 100644 src/shared/modules/user/dto/create-user.messages.ts delete mode 100644 src/shared/modules/user/dto/login-user.messages.ts delete mode 100644 src/shared/modules/user/dto/update-user.messages.ts create mode 100644 src/shared/modules/user/user.constant.ts diff --git a/.env-example b/.env-example index e573f24..e292c87 100644 --- a/.env-example +++ b/.env-example @@ -4,4 +4,5 @@ SALT=<Соль> DB_USER=<Логин для базы данных> DB_PASSWORD=<Пароль для базы данных> DB_PORT=<Порт для подключения к базе данных> -DB_NAME=<Имя базы данных> \ No newline at end of file +DB_NAME=<Имя базы данных> +UPLOAD_DIRECTORY=<Путь для хранения файлов> diff --git a/.gitignore b/.gitignore index a9a05fd..d43dd27 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ coverage /src/mocks/test-data.tsv /logs/ +/upload/ .env diff --git a/package-lock.json b/package-lock.json index bd05b07..3f3302b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,9 @@ "express-async-handler": "1.2.0", "got": "^14.2.1", "inversify": "^6.0.2", + "mime-types": "2.1.35", "mongoose": "8.3.4", + "multer": "1.4.5-lts.1", "pino": "9.0.0", "reflect-metadata": "^0.2.2" }, @@ -28,6 +30,8 @@ "@types/convict": "6.1.6", "@types/convict-format-with-validator": "6.0.5", "@types/express": "4.17.21", + "@types/mime-types": "2.1.4", + "@types/multer": "1.4.11", "@types/node": "20.12.7", "@typescript-eslint/eslint-plugin": "6.7.0", "@typescript-eslint/parser": "6.7.0", @@ -560,6 +564,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", @@ -937,6 +958,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1297,6 +1324,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -1319,6 +1352,17 @@ "semver": "^7.0.0" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1643,6 +1687,51 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/connect-pause": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/connect-pause/-/connect-pause-0.1.1.tgz", @@ -1743,6 +1832,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -4941,7 +5036,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4956,6 +5050,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mongodb": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.5.0.tgz", @@ -5113,6 +5219,24 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -5247,7 +5371,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5789,6 +5912,12 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/process-warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", @@ -6289,7 +6418,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/safe-regex": { @@ -6691,6 +6819,14 @@ "graceful-fs": "^4.1.3" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -7206,6 +7342,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -7266,6 +7408,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -7549,6 +7697,15 @@ "node": ">=8" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -8032,6 +8189,21 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true + }, + "@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/node": { "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", @@ -8277,6 +8449,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -8515,6 +8692,11 @@ "ieee754": "^1.2.1" } }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -8530,6 +8712,14 @@ "semver": "^7.0.0" } }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -8764,6 +8954,46 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "connect-pause": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/connect-pause/-/connect-pause-0.1.1.tgz", @@ -8824,6 +9054,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -11015,8 +11250,7 @@ "minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "minipass": { "version": "7.0.3", @@ -11024,6 +11258,14 @@ "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "dev": true }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + }, "mongodb": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.5.0.tgz", @@ -11121,6 +11363,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -11207,8 +11463,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { "version": "1.13.2", @@ -11581,6 +11836,11 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "process-warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", @@ -11912,8 +12172,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "2.1.1", @@ -12199,6 +12458,11 @@ "graceful-fs": "^4.1.3" } }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -12542,6 +12806,11 @@ "possible-typed-array-names": "^1.0.0" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -12586,6 +12855,11 @@ "punycode": "^2.1.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -12780,6 +13054,11 @@ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index bfef39e..6774a57 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "@types/convict": "6.1.6", "@types/convict-format-with-validator": "6.0.5", "@types/express": "4.17.21", + "@types/mime-types": "2.1.4", + "@types/multer": "1.4.11", "@types/node": "20.12.7", "@typescript-eslint/eslint-plugin": "6.7.0", "@typescript-eslint/parser": "6.7.0", @@ -55,7 +57,9 @@ "express-async-handler": "1.2.0", "got": "^14.2.1", "inversify": "^6.0.2", + "mime-types": "2.1.35", "mongoose": "8.3.4", + "multer": "1.4.5-lts.1", "pino": "9.0.0", "reflect-metadata": "^0.2.2" } diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index f9107d7..2376153 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -12,11 +12,14 @@ import { OfferModel, UserModel } from '../../shared/entities/index.js'; import { IRestSchema } from '../../shared/libs/config/rest.schema.interface.js'; import { Config, RestConfig } from '../../shared/libs/config/index.js'; import { DEFAULT_USER_LOGIN, DEFAULT_USER_PASSWORD } from '../cli.constants.js'; +// import { CommentService } from '../../shared/modules/comment/comment-service.interface.js'; +// import { DefaultCommentService } from '../../shared/modules/comment/index.js'; export class ImportCommand implements Command { private userService!: UserService; private offerService!: OfferService; + // private commentService!: CommentService; private databaseClient!: DatabaseClient; private config!: Config; private logger!: Logger; @@ -28,7 +31,8 @@ export class ImportCommand implements Command { this.logger = new ConsoleLogger(); this.config = new RestConfig(this.logger); - this.userService = new DefaultUserService(this.logger, UserModel); + // this.commentService = new DefaultCommentService(CommentModel, this.offerService); + this.userService = new DefaultUserService(this.logger, UserModel, this.offerService); this.offerService = new DefaultOfferService(this.logger, OfferModel); this.databaseClient = new MongoDatabaseClient(this.logger); } @@ -49,7 +53,7 @@ export class ImportCommand implements Command { private async saveOffer(offer: Offer) { const user = await this.userService.findOrCreate({ - ...offer.author, + ...offer.user, email: DEFAULT_USER_LOGIN, password: DEFAULT_USER_PASSWORD }, this.salt); @@ -68,8 +72,7 @@ export class ImportCommand implements Command { conveniences: offer.conveniences, coordinate: offer.coordinate, - author: user.id, - commentCount: offer.commentCount || 0, + userId: user.id, }); } diff --git a/src/mocks/mock-server-data.json b/src/mocks/mock-server-data.json index 7726ca1..c68634b 100644 --- a/src/mocks/mock-server-data.json +++ b/src/mocks/mock-server-data.json @@ -30,7 +30,7 @@ "guestCounts": [1, 2, 3, 4, 5, 6, 7, 8], "costs": [300, 600, 1000, 1200, 2000, 2300, 2500, 3000], "conveniences": ["Breakfast", "Air conditioning", "Laptop friendly workspace", "Baby seat", "Washer", "Towels", "Fridge"], - "authors": ["Ilya", "Ivan", "Elena", "Vladimir", "Alexander", "Elizaveta"], + "users": ["Ilya", "Ivan", "Elena", "Vladimir", "Alexander", "Elizaveta"], "commentCounts": [0, 1, 3, 5, 8, 12], "coordinates": [ { diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 11c7daa..67f71ef 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -47,6 +47,10 @@ export class RestApplication { private async initMiddleware() { this.server.use(express.json()); + this.server.use( + '/upload', + express.static(this.config.get('UPLOAD_DIRECTORY')) + ); } private async initExceptionFilters() { diff --git a/src/shared/constants/common.ts b/src/shared/constants/common.ts index 4d31b9a..cadf2a1 100644 --- a/src/shared/constants/common.ts +++ b/src/shared/constants/common.ts @@ -6,3 +6,5 @@ export const LAST_WEEK_DAY = 7; export const DECIMAL_RADIX = 10; export const ENCODING_DEFAULT = 'utf-8'; + +export const IMAGE_EXTENSIONS = ['jpg', 'png']; diff --git a/src/shared/entities/comment.entity.ts b/src/shared/entities/comment.entity.ts index dd84656..aed5214 100644 --- a/src/shared/entities/comment.entity.ts +++ b/src/shared/entities/comment.entity.ts @@ -34,7 +34,7 @@ export class CommentEntity extends defaultClasses.TimeStamps { ref: () => UserEntity, required: true, }) - public author!: Ref; + public userId!: Ref; public get id() { return this._id.toString(); diff --git a/src/shared/entities/offer.entity.ts b/src/shared/entities/offer.entity.ts index f4c0353..e1ace05 100644 --- a/src/shared/entities/offer.entity.ts +++ b/src/shared/entities/offer.entity.ts @@ -62,7 +62,7 @@ export class OfferEntity extends defaultClasses.TimeStamps { ref: () => UserEntity, required: true }) - public author!: Ref; + public userId!: Ref; @prop({default: 0}) public commentCount!: number; diff --git a/src/shared/libs/config/rest.schema.interface.ts b/src/shared/libs/config/rest.schema.interface.ts index 857aba0..4fb1a11 100644 --- a/src/shared/libs/config/rest.schema.interface.ts +++ b/src/shared/libs/config/rest.schema.interface.ts @@ -6,4 +6,5 @@ export interface IRestSchema { DB_PASSWORD: string; DB_PORT: string; DB_NAME: string; + UPLOAD_DIRECTORY: string; } diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index 7882cf7..0ba5ff9 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -47,4 +47,10 @@ export const configRestSchema = convict({ env: 'DB_NAME', default: null }, + UPLOAD_DIRECTORY: { + doc: 'Directory for upload files', + format: String, + env: 'UPLOAD_DIRECTORY', + default: null + }, }); diff --git a/src/shared/libs/file-reader/tsv-file-reader.ts b/src/shared/libs/file-reader/tsv-file-reader.ts index d696f53..2a88356 100644 --- a/src/shared/libs/file-reader/tsv-file-reader.ts +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -31,7 +31,7 @@ export class TSVFileReader extends EventEmitter implements FileReader { guestCount, cost, conveniences, - author, + userId, commentCount, coordinate, ] = line.split('\t'); @@ -50,7 +50,7 @@ export class TSVFileReader extends EventEmitter implements FileReader { guestCount: parseInt(guestCount as string, DECIMAL_RADIX), cost: parseInt(cost as string, DECIMAL_RADIX), conveniences: this.parseStringToArray(conveniences || '', ','), - author: this.parseUser(author || ''), + user: this.parseUser(userId || ''), commentCount: parseInt(commentCount as string, DECIMAL_RADIX), coordinate: this.parseStringToCoordinate(coordinate || '') }; diff --git a/src/shared/libs/offer-generator/tsv-offer-generator.ts b/src/shared/libs/offer-generator/tsv-offer-generator.ts index f1de13c..f844fdb 100644 --- a/src/shared/libs/offer-generator/tsv-offer-generator.ts +++ b/src/shared/libs/offer-generator/tsv-offer-generator.ts @@ -25,19 +25,17 @@ export class TSVOfferGenerator implements OfferGenerator { const guestCount = getRandomItem(this.mockData.guestCounts).toString(); const cost = getRandomItem(this.mockData.costs).toString(); const conveniences = getRandomItems(this.mockData.conveniences).join(';'); - const author = getRandomItem(this.mockData.authors); + const user = getRandomItem(this.mockData.users); const commentCount = getRandomItem(this.mockData.commentCounts).toString(); const coordinate = getRandomItem(this.mockData.coordinates); - // const [firstname, lastname] = author.split(' '); - const currentCoordinate = `${coordinate.latitude};${coordinate.longitude}`; return [ title, description, publicationDate, city, previewImg, images, isPremium, rating, type, flatCount, guestCount, cost, conveniences, - author, commentCount, currentCoordinate + user, commentCount, currentCoordinate ].join('\t'); } } diff --git a/src/shared/libs/rest/index.ts b/src/shared/libs/rest/index.ts index 625d267..27f2c5e 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -12,3 +12,8 @@ export { Middleware } from './middleware/middleware.interface.js'; export { ValidateObjectIdMiddleware } from './middleware/validate-objectid.middleware.js'; export { ValidateDtoMiddleware } from './middleware/validate-dto.middleware.js'; export { DocumentExistsMiddleware } from './middleware/document-exists.middleware.js'; +export { UploadFileMiddleware } from './middleware/upload-file.middleware.js'; +export { ValidateObjectIdQueryMiddleware } from './middleware/validate-objectid-query.middleware.js'; +export { DocumentQueryExistsMiddleware } from './middleware/document-query-exists.middleware.js'; +export { DocumentBodyExistsMiddleware } from './middleware/document-body-exists.middleware.js'; +export { ValidateObjectIdBodyMiddleware } from './middleware/validate-objectid-body.middleware.js'; diff --git a/src/shared/libs/rest/middleware/document-body-exists.middleware.ts b/src/shared/libs/rest/middleware/document-body-exists.middleware.ts new file mode 100644 index 0000000..1e94ebf --- /dev/null +++ b/src/shared/libs/rest/middleware/document-body-exists.middleware.ts @@ -0,0 +1,36 @@ +import { NextFunction, Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; + +import { Middleware } from './middleware.interface.js'; +import { DocumentExists } from '../../../types/index.js'; +import { HttpError } from '../errors/index.js'; + +export class DocumentBodyExistsMiddleware implements Middleware { + constructor( + private readonly service: DocumentExists, + private readonly entityName: string, + private readonly objectFieldName: string, + ) {} + + public async execute({ body }: Request, _: Response, next: NextFunction): Promise { + const documentId = body[this.objectFieldName]; + + if (!documentId) { + throw new HttpError( + StatusCodes.BAD_REQUEST, + 'documentId with body is not valid.', + 'DocumentBodyExistsMiddleware' + ); + } + + if (! await this.service.exists(String(documentId))) { + throw new HttpError( + StatusCodes.NOT_FOUND, + `${this.entityName} with ${documentId} with body not found.`, + 'DocumentBodyExistsMiddleware' + ); + } + + next(); + } +} diff --git a/src/shared/libs/rest/middleware/document-exists.middleware.ts b/src/shared/libs/rest/middleware/document-exists.middleware.ts index edfa26c..68ddc24 100644 --- a/src/shared/libs/rest/middleware/document-exists.middleware.ts +++ b/src/shared/libs/rest/middleware/document-exists.middleware.ts @@ -12,7 +12,7 @@ export class DocumentExistsMiddleware implements Middleware { private readonly paramName: string, ) {} - public async execute({ params }: Request, _res: Response, next: NextFunction): Promise { + public async execute({ params }: Request, _: Response, next: NextFunction): Promise { const documentId = params[this.paramName]; if (!documentId) { diff --git a/src/shared/libs/rest/middleware/document-query-exists.middleware.ts b/src/shared/libs/rest/middleware/document-query-exists.middleware.ts new file mode 100644 index 0000000..cb2b750 --- /dev/null +++ b/src/shared/libs/rest/middleware/document-query-exists.middleware.ts @@ -0,0 +1,36 @@ +import { NextFunction, Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; + +import { Middleware } from './middleware.interface.js'; +import { DocumentExists } from '../../../types/index.js'; +import { HttpError } from '../errors/index.js'; + +export class DocumentQueryExistsMiddleware implements Middleware { + constructor( + private readonly service: DocumentExists, + private readonly entityName: string, + private readonly query: string, + ) {} + + public async execute({ query }: Request, _: Response, next: NextFunction): Promise { + const documentId = query[this.query]; + + if (!documentId) { + throw new HttpError( + StatusCodes.BAD_REQUEST, + 'documentId query param is not valid.', + 'DocumentQueryExistsMiddleware' + ); + } + + if (! await this.service.exists(String(documentId))) { + throw new HttpError( + StatusCodes.NOT_FOUND, + `${this.entityName} with ${documentId} query not found.`, + 'DocumentQueryExistsMiddleware' + ); + } + + next(); + } +} diff --git a/src/shared/libs/rest/middleware/upload-file.middleware.ts b/src/shared/libs/rest/middleware/upload-file.middleware.ts new file mode 100644 index 0000000..f7e4c0e --- /dev/null +++ b/src/shared/libs/rest/middleware/upload-file.middleware.ts @@ -0,0 +1,50 @@ +import { NextFunction, Request, Response } from 'express'; +import multer, { diskStorage } from 'multer'; +import { extension } from 'mime-types'; + +import * as crypto from 'node:crypto'; + +import { Middleware } from './middleware.interface.js'; +import { StatusCodes } from 'http-status-codes'; +import { HttpError } from '../index.js'; +import { IMAGE_EXTENSIONS } from '../../../constants/index.js'; + +export class UploadFileMiddleware implements Middleware { + constructor( + private uploadDirectory: string, + private fieldName: string, + ) {} + + public async execute(req: Request, res: Response, next: NextFunction): Promise { + const storage = diskStorage({ + destination: this.uploadDirectory, + filename: (_, file, callback) => { + const fileExtention = extension(file.mimetype); + const filename = crypto.randomUUID(); + callback(null, `${filename}.${fileExtention}`); + } + }); + + // TODO: Разобраться + const fileFilter = ( + _: Request, + file: Express.Multer.File, + callback: multer.FileFilterCallback, + ) => { + const fileExtention = file.originalname.split('.').pop(); + if (fileExtention && !IMAGE_EXTENSIONS.includes(fileExtention)) { + return callback(new HttpError( + StatusCodes.BAD_REQUEST, + 'Invalid file extension', + 'UploadFileMiddleware', + )); + } + return callback(null, true); + }; + + const uploadSingleFileMiddleware = multer({ storage, fileFilter }) + .single(this.fieldName); + + uploadSingleFileMiddleware(req, res, next); + } +} diff --git a/src/shared/libs/rest/middleware/validate-objectid-body.middleware.ts b/src/shared/libs/rest/middleware/validate-objectid-body.middleware.ts new file mode 100644 index 0000000..06f0596 --- /dev/null +++ b/src/shared/libs/rest/middleware/validate-objectid-body.middleware.ts @@ -0,0 +1,24 @@ +import { NextFunction, Request, Response } from 'express'; +import { Types } from 'mongoose'; +import { StatusCodes } from 'http-status-codes'; + +import { Middleware } from './middleware.interface.js'; +import { HttpError } from '../errors/index.js'; + +export class ValidateObjectIdBodyMiddleware implements Middleware { + constructor(private objectFieldName: string) {} + + public execute({ body }: Request, _res: Response, next: NextFunction): void { + const objectId = body[this.objectFieldName]; + + if (Types.ObjectId.isValid(String(objectId))) { + return next(); + } + + throw new HttpError( + StatusCodes.BAD_REQUEST, + `${objectId} is invalid body field ObjectID`, + 'ValidateObjectIdBodyMiddleware' + ); + } +} \ No newline at end of file diff --git a/src/shared/libs/rest/middleware/validate-objectid-query.middleware.ts b/src/shared/libs/rest/middleware/validate-objectid-query.middleware.ts new file mode 100644 index 0000000..a432dbd --- /dev/null +++ b/src/shared/libs/rest/middleware/validate-objectid-query.middleware.ts @@ -0,0 +1,24 @@ +import { NextFunction, Request, Response } from 'express'; +import { Types } from 'mongoose'; +import { StatusCodes } from 'http-status-codes'; + +import { Middleware } from './middleware.interface.js'; +import { HttpError } from '../errors/index.js'; + +export class ValidateObjectIdQueryMiddleware implements Middleware { + constructor(private query: string) {} + + public execute({ query }: Request, _res: Response, next: NextFunction): void { + const objectId = query[this.query]; + + if (Types.ObjectId.isValid(String(objectId))) { + return next(); + } + + throw new HttpError( + StatusCodes.BAD_REQUEST, + `${objectId} is invalid query ObjectID`, + 'ValidateObjectIdQueryMiddleware' + ); + } +} diff --git a/src/shared/modules/comment/comment.constant.ts b/src/shared/modules/comment/comment.constant.ts index 45239e9..529b1fc 100644 --- a/src/shared/modules/comment/comment.constant.ts +++ b/src/shared/modules/comment/comment.constant.ts @@ -1 +1,13 @@ export const DEFAULT_COMMENT_COUNT = 50; + +export const COMMENT_DTO_CONSTRAINTS = { + TEXT: { + MIN_LENGTH: 5, + MAX_LENGTH: 1024 + }, + RATING: { + MIN_VALUE: 1, + MAX_VALUE: 5 + }, +} as const; + \ No newline at end of file diff --git a/src/shared/modules/comment/comment.controller.ts b/src/shared/modules/comment/comment.controller.ts index f4950bb..d35450f 100644 --- a/src/shared/modules/comment/comment.controller.ts +++ b/src/shared/modules/comment/comment.controller.ts @@ -2,7 +2,7 @@ import { inject, injectable } from 'inversify'; import { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; -import { BaseController, HttpError, HttpMethod, ValidateDtoMiddleware } from '../../libs/rest/index.js'; +import { BaseController, DocumentBodyExistsMiddleware, DocumentQueryExistsMiddleware, HttpError, HttpMethod, ValidateDtoMiddleware, ValidateObjectIdQueryMiddleware } from '../../libs/rest/index.js'; import { Logger } from '../../libs/logger/index.js'; import { CommentService } from './comment-service.interface.js'; import { OfferService } from '../offer/index.js'; @@ -28,30 +28,20 @@ export default class CommentController extends BaseController { method: HttpMethod.Post, handler: this.create, middlewares: [ - new ValidateDtoMiddleware(CreateCommentDto) + new ValidateDtoMiddleware(CreateCommentDto), + // ?- Спросить необходимость инжектировать сервис для проверки в контроллере + new DocumentBodyExistsMiddleware(this.offerService, 'Offer', 'offerId') ] }); - this.addRoute({ path: '/', method: HttpMethod.Get, handler: this.index }); + this.addRoute({ path: '/', method: HttpMethod.Get, handler: this.index, middlewares: [ + new ValidateObjectIdQueryMiddleware('offerId'), + // ?- Спросить необходимость инжектировать сервис для проверки в контроллере + new DocumentQueryExistsMiddleware(this.offerService, 'Offer', 'offerId') + ] }); } public async index({ query }: Request, res: Response): Promise { - const offerId = query?.offerId; - - if (!offerId) { - throw new HttpError( - StatusCodes.BAD_REQUEST, - 'OfferId param is not correct.', - 'CommentController' - ); - } - - if (!await this.offerService.exists(offerId)) { - throw new HttpError( - StatusCodes.NOT_FOUND, - `Offer with id ${offerId} not found.`, - 'CommentController' - ); - } + const offerId = String(query.offerId); const comments = await this.commentService.findByOfferId(offerId); this.ok(res, fillDTO(CommentRdo, comments)); @@ -71,7 +61,9 @@ export default class CommentController extends BaseController { } const comment = await this.commentService.create(body); + await this.offerService.incCommentCount(body.offerId); + this.created(res, fillDTO(CommentRdo, comment)); } } diff --git a/src/shared/modules/comment/comment.http b/src/shared/modules/comment/comment.http index 2d22201..1b121ef 100644 --- a/src/shared/modules/comment/comment.http +++ b/src/shared/modules/comment/comment.http @@ -7,13 +7,13 @@ Content-Type: application/json { "text": "Всё отлично", "rating": 5, - "offerId": "67152f430ace5d6726f44747", - "author": "67152f430ace5d6726f44745" + "offerId": "6727abe4326ef99e212e42f5", + "userId": "67152f430ace5d6726f44745" } ### ## Список комментариев к объявлению -GET http://localhost:6000/comments?offerId=67152f430ace5d6726f44747 HTTP/1.1 +GET http://localhost:6000/comments?offerId=6727abe4326ef99e212e42f5 HTTP/1.1 ### \ No newline at end of file diff --git a/src/shared/modules/comment/default-comment.service.ts b/src/shared/modules/comment/default-comment.service.ts index c9bf1ae..ebcc935 100644 --- a/src/shared/modules/comment/default-comment.service.ts +++ b/src/shared/modules/comment/default-comment.service.ts @@ -22,8 +22,8 @@ export class DefaultCommentService implements CommentService { public async findByOfferId(offerId: string): Promise[]> { return this.commentModel .find({offerId}, {}, { limit: DEFAULT_COMMENT_COUNT }) - .sort({ createdAt: SortType.DOWN }) - .populate('author'); + .sort({ createdAt: SortType.DESC }) + .populate('userId'); } // TODO: Закрыть от неавторизированных пользователей @@ -31,8 +31,8 @@ export class DefaultCommentService implements CommentService { public async create(dto: CreateCommentDto): Promise> { const comment = await this.commentModel.create(dto); - await this.offerService.calculateOfferRating(String(comment.author)); - return comment.populate('author'); + await this.offerService.calculateOfferRating(String(comment.userId)); + return comment.populate('userId'); } // TODO: Ручка удаления комментариев вместе с предложениями diff --git a/src/shared/modules/comment/dto/create-comment.dto.ts b/src/shared/modules/comment/dto/create-comment.dto.ts index 3d46134..f983e88 100644 --- a/src/shared/modules/comment/dto/create-comment.dto.ts +++ b/src/shared/modules/comment/dto/create-comment.dto.ts @@ -1,20 +1,19 @@ import { IsInt, IsMongoId, IsString, Length, Max, Min } from 'class-validator'; - -import { CREATE_COMMENT_MESSAGES } from './create-comment.messages.js'; +import { COMMENT_DTO_CONSTRAINTS } from '../comment.constant.js'; export class CreateCommentDto { - @IsString({ message: CREATE_COMMENT_MESSAGES.TEXT.invalidFormat }) - @Length(5, 1024, { message: 'min is 5, max is 1024 '}) + @IsString() + @Length(COMMENT_DTO_CONSTRAINTS.TEXT.MIN_LENGTH, COMMENT_DTO_CONSTRAINTS.TEXT.MAX_LENGTH) public text!: string; - @IsInt({ message: CREATE_COMMENT_MESSAGES.RATING.invalidFormat }) - @Min(1, { message: CREATE_COMMENT_MESSAGES.RATING.minValue }) - @Max(5, { message: CREATE_COMMENT_MESSAGES.RATING.maxValue }) + @IsInt() + @Min(COMMENT_DTO_CONSTRAINTS.RATING.MIN_VALUE) + @Max(COMMENT_DTO_CONSTRAINTS.RATING.MAX_VALUE) public rating!: number; - @IsMongoId({ message: CREATE_COMMENT_MESSAGES.OFFER_ID.invalidFormat }) + @IsMongoId() public offerId!: string; - @IsMongoId({ message: CREATE_COMMENT_MESSAGES.USER_ID.invalidFormat }) + @IsMongoId() public userId!: string; } diff --git a/src/shared/modules/comment/dto/create-comment.messages.ts b/src/shared/modules/comment/dto/create-comment.messages.ts deleted file mode 100644 index 713d3f7..0000000 --- a/src/shared/modules/comment/dto/create-comment.messages.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const CREATE_COMMENT_MESSAGES = { - TEXT: { - invalidFormat: 'Text is required', - lengthField: 'Min length is 5, max is 2024' - }, - RATING: { - invalidFormat: 'Rating must be an integer', - minValue: 'Minimum rating value must be 1', - maxValue: 'Maximum rating value must be 5', - }, - OFFER_ID: { - invalidFormat: 'OfferId field must be a valid id' - }, - USER_ID: { - invalidFormat: 'UserId field must be a valid id' - }, -} as const; diff --git a/src/shared/modules/comment/rdo/comment.rdo.ts b/src/shared/modules/comment/rdo/comment.rdo.ts index b869b9e..d2de747 100644 --- a/src/shared/modules/comment/rdo/comment.rdo.ts +++ b/src/shared/modules/comment/rdo/comment.rdo.ts @@ -1,11 +1,10 @@ -import { Expose, Type } from 'class-transformer'; +import { Expose, Transform, Type } from 'class-transformer'; import { UserRdo } from '../../user/rdo/user.rdo.js'; export class CommentRdo { - // TODO: Выводить корректный id - // @Transform(t => t.toString()) @Expose() + @Transform(params => params.obj._id.toString()) public id!: string; @Expose() diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index ceed450..df12e48 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -11,12 +11,15 @@ import { SortType } from '../../types/sort-type.enum.js'; import { Types } from 'mongoose'; import { authorAggregation } from './offer.aggregation.js'; import { OfferEntity } from '../../entities/index.js'; +// import { CommentService } from '../comment/index.js'; +// import { UserService } from '../user/user-service.interface.js'; @injectable() export class DefaultOfferService implements OfferService { constructor( @inject(COMPONENT.LOGGER) private readonly logger: Logger, - @inject(COMPONENT.OFFER_MODEL) private readonly offerModel: types.ModelType + @inject(COMPONENT.OFFER_MODEL) private readonly offerModel: types.ModelType, + // @inject(COMPONENT.COMMENT_SERVICE) private readonly commentService: CommentService ) {} // TODO: Возвращать не больше 60 предложений об аренде - SUCCESS @@ -28,12 +31,12 @@ export class DefaultOfferService implements OfferService { .aggregate([ ...authorAggregation, { $limit: limit }, + { $sort: { createdAt: SortType.DESC }} ]) // .find({}, {}, { limit }) // // .limit(limit) // // expose декоратор rename - // .sort({ publicationDate: SortType.DOWN }) // // .aggregate(offerAggregation) // .populate(['author']) .exec(); @@ -51,7 +54,7 @@ export class DefaultOfferService implements OfferService { public async updateById(offerId: string, dto: UpdateOfferDto): Promise | null> { return this.offerModel .findByIdAndUpdate(offerId, dto, {new: true}) - .populate(['author']) + .populate(['userId']) .exec(); } @@ -76,14 +79,14 @@ export class DefaultOfferService implements OfferService { // .populate(['author']) } - + + // TODO: Проверить метод public async findByPremium(): Promise[]> { return this.offerModel .find({ isPremium: true }, {}, { limit: DEFAULT_PREMIUM_OFFER_COUNT }) - .sort({ publicationDate: SortType.DOWN }); + .sort({ publicationDate: SortType.DESC }); } - // INFO: икремент добавления количества комментария // - обратиться к сервису оферов public async incCommentCount(offerId: string): Promise | null> { return this.offerModel .findByIdAndUpdate(offerId, { @@ -105,19 +108,27 @@ export class DefaultOfferService implements OfferService { }).exec(); } - // TODO: проверка на существование документа - предложения public async exists(documentId: string): Promise { return this.offerModel.exists({_id: documentId}).then((r) => !!r); } - // public async findByCategoryId(categoryId: string, count?: number): Promise[]> { - // const limit = count ?? DEFAULT_OFFER_COUNT; - // return this.offerModel - // .find({categories: categoryId}, {}, {limit}) - // .populate(['userId', 'categories']) - // .exec(); - // } + public async findFavoritesByUserId(user: any): Promise[]> { + console.log("user", user); + return this.offerModel + .aggregate([ + ...authorAggregation, + ]) + .exec(); + } + + // console.log("user", user); + + // const favoritesIds = user.favorites.map((item: Types.ObjectId) => ({ _id: item })); + // const user = await this.userService.findById(userId); + // const offers = user.favorites.map(() => await this.); + // console.log("user", user); + // { $match: { 'author': new Types.ObjectId(userId) } }, // public async findNew(count: number): Promise[]> { // return this.offerModel diff --git a/src/shared/modules/offer/dto/coordinate.dto.ts b/src/shared/modules/offer/dto/coordinate.dto.ts new file mode 100644 index 0000000..a56efb3 --- /dev/null +++ b/src/shared/modules/offer/dto/coordinate.dto.ts @@ -0,0 +1,9 @@ +import { IsLatitude, IsLongitude } from 'class-validator'; + +export class CoordinateDTO { + @IsLatitude() + public latitude!: number; + + @IsLongitude() + public longitude!: number; +} \ No newline at end of file diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts index e8d7390..f9064f1 100644 --- a/src/shared/modules/offer/dto/create-offer.dto.ts +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -1,18 +1,20 @@ -import { IsArray, IsEnum, IsInt, IsObject, Max, MaxLength, Min, MinLength } from 'class-validator'; +import { IsArray, IsBoolean, IsEnum, IsInt, IsObject, Max, MaxLength, Min, MinLength, ValidateNested } from 'class-validator'; import { City, ConvenienceType, Coordinate, OfferType } from '../../../types/index.js'; -import { CREATE_OFFER_VALIDATION_MESSAGE } from './create-offer.messages.js'; +import { Type } from 'class-transformer'; +import { CoordinateDTO } from './coordinate.dto.js'; +import { OFFER_DTO_CONSTRAINTS } from '../offer.constant.js'; export class CreateOfferDto { - @MinLength(10, { message: CREATE_OFFER_VALIDATION_MESSAGE.TITLE.minLength }) - @MaxLength(100, { message: CREATE_OFFER_VALIDATION_MESSAGE.TITLE.maxLength }) + @MinLength(OFFER_DTO_CONSTRAINTS.TITLE.MIN_LENGTH) + @MaxLength(OFFER_DTO_CONSTRAINTS.TITLE.MAX_LENGTH) public title!: string; - @MinLength(20, { message: CREATE_OFFER_VALIDATION_MESSAGE.TITLE.minLength }) - @MaxLength(1024, { message: CREATE_OFFER_VALIDATION_MESSAGE.TITLE.maxLength }) + @MinLength(OFFER_DTO_CONSTRAINTS.DESCRIPTION.MIN_LENGTH) + @MaxLength(OFFER_DTO_CONSTRAINTS.DESCRIPTION.MAX_LENGTH) public description!: string; - @IsEnum(City, { message: CREATE_OFFER_VALIDATION_MESSAGE.CITY.invalid }) + @IsEnum(City) public city!: City; public previewImg!: string; @@ -20,40 +22,36 @@ export class CreateOfferDto { // TODO: 6 фотографий всегда public images!: string[]; - // TODO: булево значение + @IsBoolean() public isPremium!: boolean; - @IsEnum(OfferType, { message: CREATE_OFFER_VALIDATION_MESSAGE.OFFER_TYPE.invalid }) + @IsEnum(OfferType) public type!: OfferType; - @IsInt({ message: CREATE_OFFER_VALIDATION_MESSAGE.FLAT_COUNT.invalidFormat }) - @Min(1, { message: CREATE_OFFER_VALIDATION_MESSAGE.FLAT_COUNT.minValue }) - @Max(8, { message: CREATE_OFFER_VALIDATION_MESSAGE.FLAT_COUNT.maxValue }) + @IsInt() + @Min(OFFER_DTO_CONSTRAINTS.FLAT_COUNT.MIN_VALUE) + @Max(OFFER_DTO_CONSTRAINTS.FLAT_COUNT.MAX_VALUE) public flatCount!: number; - @IsInt({ message: CREATE_OFFER_VALIDATION_MESSAGE.GUEST_COUNT.invalidFormat }) - @Min(1, { message: CREATE_OFFER_VALIDATION_MESSAGE.GUEST_COUNT.minValue }) - @Max(10, { message: CREATE_OFFER_VALIDATION_MESSAGE.GUEST_COUNT.maxValue }) + @IsInt() + @Min(OFFER_DTO_CONSTRAINTS.GUEST_COUNT.MIN_VALUE) + @Max(OFFER_DTO_CONSTRAINTS.GUEST_COUNT.MAX_VALUE) public guestCount!: number; - @IsInt({ message: CREATE_OFFER_VALIDATION_MESSAGE.COST.invalidFormat }) - @Min(100, { message: CREATE_OFFER_VALIDATION_MESSAGE.COST.minValue }) - @Max(100000, { message: CREATE_OFFER_VALIDATION_MESSAGE.COST.maxValue }) + @IsInt() + @Min(OFFER_DTO_CONSTRAINTS.COST.MIN_VALUE) + @Max(OFFER_DTO_CONSTRAINTS.COST.MAX_VALUE) public cost!: number; - @IsArray({ message: CREATE_OFFER_VALIDATION_MESSAGE.CONVENIENCES.invalidFormat }) + @IsArray() // @IsEnum({ each: true, message: CREATE_OFFER_VALIDATION_MESSAGE.CONVENIENCES.invalid }) public conveniences!: ConvenienceType[]; - // TODO: Как указываем объекты - @IsObject({ }) + @ValidateNested() + @IsObject() + @Type(() => CoordinateDTO) public coordinate!: Coordinate; // @IsMongoId({ message: CREATE_OFFER_VALIDATION_MESSAGE.AUTHOR.invalidId }) - public author?: string; - - // // @IsDateString({}, { message: CREATE_OFFER_VALIDATION_MESSAGE.PUBLICATION_DATE.invalidFormat }) - // public publicationDate?: Date; - - public commentCount?: number; + public userId?: string; } diff --git a/src/shared/modules/offer/dto/create-offer.messages.ts b/src/shared/modules/offer/dto/create-offer.messages.ts deleted file mode 100644 index 5d84cd7..0000000 --- a/src/shared/modules/offer/dto/create-offer.messages.ts +++ /dev/null @@ -1,47 +0,0 @@ -export const CREATE_OFFER_VALIDATION_MESSAGE = { - TITLE: { - minLength: 'Minimum title length must be 10', - maxLength: 'Maximum title length must be 100', - }, - DESCRIPTION: { - minLength: 'Minimum description length must be 20', - maxLength: 'Maximum description length must be 1024', - }, - CITY: { - invalid: 'City field must be one of the six cities', - }, - OFFER_TYPE: { - invalid: 'Offer type must be one of the four types', - }, - FLAT_COUNT: { - invalidFormat: 'Flat count must be an integer', - minValue: 'Minimum flat count value must be 1', - maxValue: 'Maximum flat count value must be 8', - }, - GUEST_COUNT: { - invalidFormat: 'Guest count must be an integer', - minValue: 'Minimum guest count value must be 1', - maxValue: 'Maximum guest count value must be 10', - }, - COST: { - invalidFormat: 'Cost must be an integer', - minValue: 'Minimum cost is 100', - maxValue: 'Maximum cost is 100000', - }, - // TODO: Указываем только 6 фотографий - IMAGES: { - maxLength: 'Too short for field «image»', - }, - CONVENIENCES: { - invalidFormat: 'Field conveniences must be an array', - invalid: 'Conveniences field must be an array of valid type', - }, - - - AUTHOR: { - invalidId: 'Author field must be a valid id', - }, - PUBLICATION_DATE: { - invalidFormat: 'postDate must be a valid ISO date', - }, -} as const; diff --git a/src/shared/modules/offer/dto/update-offer.dto.ts b/src/shared/modules/offer/dto/update-offer.dto.ts index 1027c1b..d201362 100644 --- a/src/shared/modules/offer/dto/update-offer.dto.ts +++ b/src/shared/modules/offer/dto/update-offer.dto.ts @@ -1,20 +1,22 @@ -import { IsArray, IsEnum, IsInt, IsObject, IsOptional, Max, MaxLength, Min, MinLength } from 'class-validator'; +import { IsArray, IsEnum, IsInt, IsObject, IsOptional, Max, MaxLength, Min, MinLength, ValidateNested } from 'class-validator'; import { City, ConvenienceType, Coordinate, OfferType } from '../../../types/index.js'; -import { UPDATE_OFFER_VALIDATION_MESSAGE } from './update-offer.messages.js'; +import { Type } from 'class-transformer'; +import { CoordinateDTO } from './coordinate.dto.js'; +import { OFFER_DTO_CONSTRAINTS } from '../offer.constant.js'; export class UpdateOfferDto { @IsOptional() - @MinLength(10, { message: UPDATE_OFFER_VALIDATION_MESSAGE.TITLE.minLength }) - @MaxLength(100, { message: UPDATE_OFFER_VALIDATION_MESSAGE.TITLE.maxLength }) + @MinLength(OFFER_DTO_CONSTRAINTS.TITLE.MIN_LENGTH) + @MaxLength(OFFER_DTO_CONSTRAINTS.TITLE.MAX_LENGTH) public title?: string; @IsOptional() - @MinLength(20, { message: UPDATE_OFFER_VALIDATION_MESSAGE.TITLE.minLength }) - @MaxLength(1024, { message: UPDATE_OFFER_VALIDATION_MESSAGE.TITLE.maxLength }) + @MinLength(OFFER_DTO_CONSTRAINTS.DESCRIPTION.MIN_LENGTH) + @MaxLength(OFFER_DTO_CONSTRAINTS.DESCRIPTION.MAX_LENGTH) public description?: string; @IsOptional() - @IsEnum(City, { message: UPDATE_OFFER_VALIDATION_MESSAGE.CITY.invalid }) + @IsEnum(City) public city?: City; @IsOptional() @@ -27,35 +29,35 @@ export class UpdateOfferDto { public isPremium?: boolean; @IsOptional() - @IsEnum(OfferType, { message: UPDATE_OFFER_VALIDATION_MESSAGE.OFFER_TYPE.invalid }) - @Min(1, { message: UPDATE_OFFER_VALIDATION_MESSAGE.FLAT_COUNT.minValue }) - @Max(8, { message: UPDATE_OFFER_VALIDATION_MESSAGE.FLAT_COUNT.maxValue }) + @IsEnum(OfferType) public type?: OfferType; @IsOptional() - @IsInt({ message: UPDATE_OFFER_VALIDATION_MESSAGE.FLAT_COUNT.invalidFormat }) - @Min(1, { message: UPDATE_OFFER_VALIDATION_MESSAGE.FLAT_COUNT.minValue }) - @Max(8, { message: UPDATE_OFFER_VALIDATION_MESSAGE.FLAT_COUNT.maxValue }) + @IsInt() + @Min(OFFER_DTO_CONSTRAINTS.FLAT_COUNT.MIN_VALUE) + @Max(OFFER_DTO_CONSTRAINTS.FLAT_COUNT.MAX_VALUE) public flatCount?: number; @IsOptional() - @IsInt({ message: UPDATE_OFFER_VALIDATION_MESSAGE.GUEST_COUNT.invalidFormat }) - @Min(1, { message: UPDATE_OFFER_VALIDATION_MESSAGE.GUEST_COUNT.minValue }) - @Max(10, { message: UPDATE_OFFER_VALIDATION_MESSAGE.GUEST_COUNT.maxValue }) + @IsInt() + @Min(OFFER_DTO_CONSTRAINTS.GUEST_COUNT.MIN_VALUE) + @Max(OFFER_DTO_CONSTRAINTS.GUEST_COUNT.MAX_VALUE) public guestCount?: number; @IsOptional() - @IsInt({ message: UPDATE_OFFER_VALIDATION_MESSAGE.COST.invalidFormat }) - @Min(100, { message: UPDATE_OFFER_VALIDATION_MESSAGE.COST.minValue }) - @Max(100000, { message: UPDATE_OFFER_VALIDATION_MESSAGE.COST.maxValue }) + @IsInt() + @Min(OFFER_DTO_CONSTRAINTS.COST.MIN_VALUE) + @Max(OFFER_DTO_CONSTRAINTS.COST.MAX_VALUE) public cost?: number; @IsOptional() - @IsArray({ message: UPDATE_OFFER_VALIDATION_MESSAGE.CONVENIENCES.invalidFormat }) + @IsArray() // @IsEnum({ each: true, message: UPDATE_OFFER_VALIDATION_MESSAGE.CONVENIENCES.invalid }) public conveniences?: ConvenienceType[]; @IsOptional() - @IsObject({ }) + @ValidateNested() + @IsObject() + @Type(() => CoordinateDTO) public coordinate?: Coordinate; } diff --git a/src/shared/modules/offer/dto/update-offer.messages.ts b/src/shared/modules/offer/dto/update-offer.messages.ts deleted file mode 100644 index b0f1e26..0000000 --- a/src/shared/modules/offer/dto/update-offer.messages.ts +++ /dev/null @@ -1,47 +0,0 @@ -export const UPDATE_OFFER_VALIDATION_MESSAGE = { - TITLE: { - minLength: 'Minimum title length must be 10', - maxLength: 'Maximum title length must be 100', - }, - DESCRIPTION: { - minLength: 'Minimum description length must be 20', - maxLength: 'Maximum description length must be 1024', - }, - CITY: { - invalid: 'City field must be one of the six cities', - }, - OFFER_TYPE: { - invalid: 'Offer type must be one of the four types', - }, - FLAT_COUNT: { - invalidFormat: 'Flat count must be an integer', - minValue: 'Minimum flat count value must be 1', - maxValue: 'Maximum flat count value must be 8', - }, - GUEST_COUNT: { - invalidFormat: 'Guest count must be an integer', - minValue: 'Minimum guest count value must be 1', - maxValue: 'Maximum guest count value must be 10', - }, - COST: { - invalidFormat: 'Cost must be an integer', - minValue: 'Minimum cost is 100', - maxValue: 'Maximum cost is 100000', - }, - // TODO: Указываем только 6 фотографий - IMAGES: { - maxLength: 'Too short for field «image»', - }, - CONVENIENCES: { - invalidFormat: 'Field conveniences must be an array', - invalid: 'Conveniences field must be an array of valid type', - }, - - - AUTHOR: { - invalidId: 'Author field must be a valid id', - }, - PUBLICATION_DATE: { - invalidFormat: 'postDate must be a valid ISO date', - }, -} as const; diff --git a/src/shared/modules/offer/index.ts b/src/shared/modules/offer/index.ts index ec54d61..d25e48b 100644 --- a/src/shared/modules/offer/index.ts +++ b/src/shared/modules/offer/index.ts @@ -5,4 +5,3 @@ export { createOfferContainer } from './offer.container.js'; export { OfferController } from './offer.controller.js'; export { ShortOfferRdo } from './rdo/short-offer.rdo.js'; export { FullOfferRdo } from './rdo/full-offer.rdo.js'; -export { CREATE_OFFER_VALIDATION_MESSAGE } from './dto/create-offer.messages.js'; diff --git a/src/shared/modules/offer/offer-service.interface.ts b/src/shared/modules/offer/offer-service.interface.ts index 0522eb0..9aaad12 100644 --- a/src/shared/modules/offer/offer-service.interface.ts +++ b/src/shared/modules/offer/offer-service.interface.ts @@ -14,8 +14,8 @@ export interface OfferService extends DocumentExists { findByPremium(): Promise[]>; // TODO: икремент добавления количества комментария - нужен он? incCommentCount(offerId: string): Promise | null>; - // findNew(count: number): Promise[]>; - // findDiscussed(count: number): Promise[]>; + // -? как правильно типизировать + findFavoritesByUserId(user: any): Promise[]>; exists(documentId: string): Promise; calculateOfferRating(offerId: string): Promise | null>; } diff --git a/src/shared/modules/offer/offer.aggregation.ts b/src/shared/modules/offer/offer.aggregation.ts index 854b563..44b8f3b 100644 --- a/src/shared/modules/offer/offer.aggregation.ts +++ b/src/shared/modules/offer/offer.aggregation.ts @@ -1,6 +1,20 @@ +import { Types } from 'mongoose'; import { SortType } from '../../types/sort-type.enum.js'; import { DEFAULT_COMMENT_COUNT } from '../comment/comment.constant.js'; +export const favoritesAggregation = (userId: string) => ([ + { + $lookup: { + from: 'users', + pipeline: [ + { $match: { 'userId': new Types.ObjectId(userId) } }, + // { $project: { favorites: 1 } } + ], + as: 'user' + }, + } +]); + // export const offerRatingAggregation = [ // { // $lookup: { @@ -33,12 +47,21 @@ import { DEFAULT_COMMENT_COUNT } from '../comment/comment.constant.js'; export const authorAggregation = [{ $lookup: { from: 'users', - localField: 'author', + localField: 'userId', foreignField: '_id', - as: 'author', + as: 'user', }, }]; // , { $unwind: '$author' } +// export const favoriteAggregation = [{ +// $lookup: { +// from: 'users', +// localField: '_id', +// foreignField: '_id', +// as: 'author', +// }, +// }] + // export const populateAuthor = [ // { // $lookup: { @@ -83,6 +106,6 @@ export const populateComments = [ $limit: DEFAULT_COMMENT_COUNT }, { - $sort: { createdAt: SortType.DOWN } + $sort: { createdAt: SortType.DESC } } ]; diff --git a/src/shared/modules/offer/offer.constant.ts b/src/shared/modules/offer/offer.constant.ts index c49f4a9..b7e10cb 100644 --- a/src/shared/modules/offer/offer.constant.ts +++ b/src/shared/modules/offer/offer.constant.ts @@ -1,3 +1,27 @@ export const DEFAULT_OFFER_COUNT = 60; export const DEFAULT_PREMIUM_OFFER_COUNT = 3; export const MAX_OFFER_COUNT = 300; + +export const OFFER_DTO_CONSTRAINTS = { + TITLE: { + MIN_LENGTH: 10, + MAX_LENGTH: 100 + }, + DESCRIPTION: { + MIN_LENGTH: 20, + MAX_LENGTH: 1024 + }, + FLAT_COUNT: { + MIN_VALUE: 1, + MAX_VALUE: 8, + }, + GUEST_COUNT: { + MIN_VALUE: 1, + MAX_VALUE: 10, + }, + COST: { + MIN_VALUE: 100, + MAX_VALUE: 100000 + } +} as const; + diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index 61fdeb3..948f47e 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -1,13 +1,10 @@ import { inject, injectable } from 'inversify'; import { Request, Response } from 'express'; -// import { StatusCodes } from 'http-status-codes'; import { BaseController, DocumentExistsMiddleware, HttpError, HttpMethod, RequestQuery, ValidateDtoMiddleware, ValidateObjectIdMiddleware } from '../../libs/rest/index.js'; import { Logger } from '../../libs/logger/index.js'; import { COMPONENT } from '../../constants/component.constant.js'; import { OfferService } from './offer-service.interface.js'; -// import { CreateOfferDto } from './dto/create-offer.dto.js'; -import { ShortOfferRdo } from './rdo/short-offer.rdo.js'; import { fillDTO } from '../../helpers/common.js'; import { FullOfferRdo } from './rdo/full-offer.rdo.js'; import { StatusCodes } from 'http-status-codes'; @@ -24,7 +21,7 @@ export class OfferController extends BaseController { constructor( @inject(COMPONENT.LOGGER) protected readonly logger: Logger, @inject(COMPONENT.OFFER_SERVICE) private readonly offerService: OfferService, - @inject(COMPONENT.COMMENT_SERVICE) private readonly commentService: CommentService + @inject(COMPONENT.COMMENT_SERVICE) private readonly commentService: CommentService // TODO: Убрать! ) { super(logger); @@ -41,13 +38,16 @@ export class OfferController extends BaseController { path: '/:offerId', method: HttpMethod.Get, handler: this.show, - middlewares: [new ValidateObjectIdMiddleware('offerId'), new ValidateDtoMiddleware(UpdateOfferDto), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId')] + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId')] }); - this.addRoute({ path: '/:offerId', method: HttpMethod.Patch, handler: this.update, middlewares: [new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId')] }); + this.addRoute({ path: '/:offerId', method: HttpMethod.Patch, handler: this.update, middlewares: [new ValidateObjectIdMiddleware('offerId'), new ValidateDtoMiddleware(UpdateOfferDto), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId')] }); this.addRoute({ path: '/:offerId', method: HttpMethod.Delete, handler: this.delete, middlewares: [new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId')] }); } public async index({ query } : Request, res: Response): Promise { + // TODO: Опционально сделать middleware для limit (опционально) const limitNum = Number(query?.limit); const limit = query?.limit && !Number.isNaN(limitNum) && limitNum < MAX_OFFER_COUNT ? limitNum : DEFAULT_OFFER_COUNT; @@ -64,7 +64,7 @@ export class OfferController extends BaseController { const offers = await this.offerService.find(limit); - const responseData = fillDTO(ShortOfferRdo, offers); + const responseData = fillDTO(FullOfferRdo, offers); this.ok(res, responseData); } @@ -120,9 +120,10 @@ export class OfferController extends BaseController { } public async delete({ params }: Request, res: Response): Promise { + // TODO: Переопределять request (опционально) const offerId = String(params?.offerId); const deletedOffer = await this.offerService.deleteById(offerId); - + // -? Circular dependency found: Symbol(kRestApplication) --> Symbol(kOfferController) --> Symbol(kOfferService) --> Symbol(kCommentService) --> Symbol(kOfferService) await this.commentService.deleteByOfferId(offerId); const responseData = fillDTO(IdOfferRdo, deletedOffer); diff --git a/src/shared/modules/offer/offer.http b/src/shared/modules/offer/offer.http index 56f3f71..cdcf795 100644 --- a/src/shared/modules/offer/offer.http +++ b/src/shared/modules/offer/offer.http @@ -25,8 +25,9 @@ Content-Type: application/json "flatCount": 1, "guestCount": 4, "cost": 4000, + "isPremium": false, "conveniences": ["Washer", "Towels", "Fridge"], - "author": "67152f430ace5d6726f44745", + "userId": "67152f430ace5d6726f44745", "coordinate": { "latitude": 52.370216, "longitude": 4.895168 @@ -37,13 +38,13 @@ Content-Type: application/json ### ## Детальный просмотр предложения -GET http://localhost:6000/offers/67152f430ace5d6726f44747 HTTP/1.1 +GET http://localhost:6000/offers/6727b44804d5909bd0be67d9 HTTP/1.1 Content-Type: application/json ### ## Редактирование предложения -PATCH http://localhost:6000/offers/67152f430ace5d6726f44747 HTTP/1.1 +PATCH http://localhost:6000/offers/6727b44804d5909bd0be67d9 HTTP/1.1 Content-Type: application/json { @@ -53,7 +54,7 @@ Content-Type: application/json ### ## Удаление предложения -DELETE http://localhost:6000/offers/67152f430ace5d6726f44771 HTTP/1.1 +DELETE http://localhost:6000/offers/6727868448704157cb5fc46e HTTP/1.1 Content-Type: application/json ### \ No newline at end of file diff --git a/src/shared/modules/offer/rdo/full-offer.rdo.ts b/src/shared/modules/offer/rdo/full-offer.rdo.ts index 6c55653..a49f249 100644 --- a/src/shared/modules/offer/rdo/full-offer.rdo.ts +++ b/src/shared/modules/offer/rdo/full-offer.rdo.ts @@ -1,9 +1,10 @@ -import { Expose, Type } from 'class-transformer'; +import { Expose, Transform, Type } from 'class-transformer'; import { City, ConvenienceType, Coordinate, OfferType } from '../../../types/index.js'; import { UserRdo } from '../../user/rdo/user.rdo.js'; export class FullOfferRdo { @Expose() + @Transform(params => params.obj._id.toString()) public id!: string; @Expose() @@ -51,7 +52,7 @@ export class FullOfferRdo { // TODO: данные вытаскивать из токена JWT @Expose() @Type(() => UserRdo) - public author!: UserRdo; + public user!: UserRdo; @Expose() public commentCount!: number; diff --git a/src/shared/modules/offer/rdo/id-offer.rdo.ts b/src/shared/modules/offer/rdo/id-offer.rdo.ts index f618a8b..9042556 100644 --- a/src/shared/modules/offer/rdo/id-offer.rdo.ts +++ b/src/shared/modules/offer/rdo/id-offer.rdo.ts @@ -1,6 +1,7 @@ -import { Expose } from 'class-transformer'; +import { Expose, Transform } from 'class-transformer'; export class IdOfferRdo { @Expose() + @Transform(params => params.obj._id.toString()) public id!: string; } diff --git a/src/shared/modules/offer/rdo/short-offer.rdo.ts b/src/shared/modules/offer/rdo/short-offer.rdo.ts index 6be3521..c2de87b 100644 --- a/src/shared/modules/offer/rdo/short-offer.rdo.ts +++ b/src/shared/modules/offer/rdo/short-offer.rdo.ts @@ -1,8 +1,9 @@ -import { Expose } from 'class-transformer'; +import { Expose, Transform } from 'class-transformer'; import { City, OfferType } from '../../../types/index.js'; export class ShortOfferRdo { @Expose() + @Transform(params => params.obj._id.toString()) public id!: string; @Expose() diff --git a/src/shared/modules/user/default-user.service.ts b/src/shared/modules/user/default-user.service.ts index e0f3ea8..588bfdd 100644 --- a/src/shared/modules/user/default-user.service.ts +++ b/src/shared/modules/user/default-user.service.ts @@ -7,7 +7,9 @@ import { CreateUserDto } from './dto/create-user.dto.js'; import { Logger } from '../../libs/logger/index.js'; import { COMPONENT } from '../../constants/index.js'; import { UpdateUserDto } from './dto/update-user.dto.js'; -import { UserEntity } from '../../entities/index.js'; +import { OfferEntity, UserEntity } from '../../entities/index.js'; +import { OfferService } from '../offer/offer-service.interface.js'; +import { Types } from 'mongoose'; @injectable() export class DefaultUserService implements UserService { @@ -15,6 +17,7 @@ export class DefaultUserService implements UserService { constructor( @inject(COMPONENT.LOGGER) private readonly logger: Logger, @inject(COMPONENT.USER_MODEL) private readonly userModel: types.ModelType, + @inject(COMPONENT.OFFER_SERVICE) private readonly offerService: OfferService ) {} public async create(dto: CreateUserDto, salt: string): Promise> { @@ -68,18 +71,19 @@ export class DefaultUserService implements UserService { // return this.userModel.findById(userId); // offerService findMany id // } - // TODO: Как с помощью агрегации Монги найти элемент - public async findFavoritesForUser(userId: string): Promise | null> { - return this.userModel.findById(userId); + public async findFavoritesForUser(userId: string): Promise[]> { + const user = await this.userModel.findById(userId); + // -? Как прокинуть user вместо userId + return this.offerService.findFavoritesByUserId(user); } // TODO: Закрыть от неавторизированных пользователей public async addFavorite(userId: string, offerId: string): Promise | null> { - return this.userModel.findByIdAndUpdate(userId, { $addToSet: { favorites: offerId } }, { new: true }).exec(); + return this.userModel.findByIdAndUpdate(userId, { $addToSet: { favorites: new Types.ObjectId(offerId) } }, { new: true }).exec(); } // TODO: Закрыть от неавторизированных пользователей public async deleteFavorite(userId: string, offerId: string): Promise | null> { - return this.userModel.findByIdAndUpdate(userId, { $pull: { favorites: offerId } }, { new: true }).exec(); + return this.userModel.findByIdAndUpdate(userId, { $pull: { favorites: new Types.ObjectId(offerId) } }, { new: true }).exec(); } } diff --git a/src/shared/modules/user/dto/create-user.dto.ts b/src/shared/modules/user/dto/create-user.dto.ts index 3ea2cd8..1117ad4 100644 --- a/src/shared/modules/user/dto/create-user.dto.ts +++ b/src/shared/modules/user/dto/create-user.dto.ts @@ -1,25 +1,24 @@ import { IsEmail, IsEnum, IsString, Length } from 'class-validator'; -import { CREATE_USER_MESSAGES } from './create-user.messages.js'; - import { UserType } from '../../../types/user-type.enum.js'; +import { USER_DTO_CONSTRAINTS } from '../user.constant.js'; export class CreateUserDto { - @IsEmail({}, { message: CREATE_USER_MESSAGES.EMAIL.invalidFormat }) + @IsEmail() public email!: string; // TODO: Указываем дефолтное значение и поле необязательно - @IsString({ message: CREATE_USER_MESSAGES.AVATAR_PATH.invalidFormat }) + @IsString() public avatarPath!: string; - @IsString({ message: CREATE_USER_MESSAGES.USERNAME.invalidFormat }) - @Length(1, 15, { message: CREATE_USER_MESSAGES.USERNAME.lengthField }) + @IsString() + @Length(USER_DTO_CONSTRAINTS.USERNAME.MIN_LENGTH, USER_DTO_CONSTRAINTS.USERNAME.MAX_LENGTH) public userName!: string; - @IsString({ message: CREATE_USER_MESSAGES.PASSWORD.invalidFormat }) - @Length(6, 12, { message: CREATE_USER_MESSAGES.PASSWORD.lengthField }) + @IsString() + @Length(USER_DTO_CONSTRAINTS.PASSWORD.MIN_LENGTH, USER_DTO_CONSTRAINTS.PASSWORD.MAX_LENGTH) public password!: string; - @IsEnum(UserType, { message: CREATE_USER_MESSAGES.USER_TYPE.invalid }) + @IsEnum(UserType) public type!: UserType; } diff --git a/src/shared/modules/user/dto/create-user.messages.ts b/src/shared/modules/user/dto/create-user.messages.ts deleted file mode 100644 index 9ae7551..0000000 --- a/src/shared/modules/user/dto/create-user.messages.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const CREATE_USER_MESSAGES = { - EMAIL: { - invalidFormat: 'Email must be a valid address' - }, - AVATAR_PATH: { - invalidFormat: 'Avatar path is required', - }, - USERNAME: { - invalidFormat: 'Username is required', - lengthField: 'Min length is 1, max is 15', - }, - PASSWORD: { - invalidFormat: 'Password is required', - lengthField: 'Min length for password is 6, max is 12' - }, - USER_TYPE: { - invalid: 'User type must be one of the two types: "pro" or "обычный"', - } -} as const; diff --git a/src/shared/modules/user/dto/login-user.dto.ts b/src/shared/modules/user/dto/login-user.dto.ts index 6ba8e05..72b35d4 100644 --- a/src/shared/modules/user/dto/login-user.dto.ts +++ b/src/shared/modules/user/dto/login-user.dto.ts @@ -1,11 +1,9 @@ import { IsEmail, IsString } from 'class-validator'; -import { CREATE_LOGIN_USER_MESSAGES } from './login-user.messages.js'; - export class LoginUserDto { - @IsEmail({}, { message: CREATE_LOGIN_USER_MESSAGES.EMAIL.invalidFormat }) + @IsEmail() public email!: string; - @IsString({ message: CREATE_LOGIN_USER_MESSAGES.PASSWORD.invalidFormat }) + @IsString() public password!: string; } diff --git a/src/shared/modules/user/dto/login-user.messages.ts b/src/shared/modules/user/dto/login-user.messages.ts deleted file mode 100644 index e42db0c..0000000 --- a/src/shared/modules/user/dto/login-user.messages.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const CREATE_LOGIN_USER_MESSAGES = { - EMAIL: { - invalidFormat: 'Email must be a valid address', - }, - PASSWORD: { - invalidFormat: 'Password is required', - } -} as const; diff --git a/src/shared/modules/user/dto/update-user.dto.ts b/src/shared/modules/user/dto/update-user.dto.ts index 9909435..a8a8ffe 100644 --- a/src/shared/modules/user/dto/update-user.dto.ts +++ b/src/shared/modules/user/dto/update-user.dto.ts @@ -1,23 +1,23 @@ import { IsEmail, IsEnum, IsString, Length } from 'class-validator'; import { UserType } from '../../../types/index.js'; -import { UPDATE_USER_MESSAGES } from './update-user.messages.js'; +import { USER_DTO_CONSTRAINTS } from '../user.constant.js'; export class UpdateUserDto { - @IsEmail({}, { message: UPDATE_USER_MESSAGES.EMAIL.invalidFormat }) + @IsEmail() public email!: string; // TODO: Указываем дефолтное значение и поле необязательно - @IsString({ message: UPDATE_USER_MESSAGES.AVATAR_PATH.invalidFormat }) + @IsString() public avatarPath!: string; - @IsString({ message: UPDATE_USER_MESSAGES.USERNAME.invalidFormat }) - @Length(1, 15, { message: UPDATE_USER_MESSAGES.USERNAME.lengthField }) + @IsString() + @Length(USER_DTO_CONSTRAINTS.USERNAME.MIN_LENGTH, USER_DTO_CONSTRAINTS.USERNAME.MAX_LENGTH) public userName!: string; - @IsString({ message: UPDATE_USER_MESSAGES.PASSWORD.invalidFormat }) - @Length(6, 12, { message: UPDATE_USER_MESSAGES.PASSWORD.lengthField }) + @IsString() + @Length(USER_DTO_CONSTRAINTS.PASSWORD.MIN_LENGTH, USER_DTO_CONSTRAINTS.PASSWORD.MAX_LENGTH) public password!: string; - @IsEnum(UserType, { message: UPDATE_USER_MESSAGES.USER_TYPE.invalid }) + @IsEnum(UserType) public type!: UserType; } diff --git a/src/shared/modules/user/dto/update-user.messages.ts b/src/shared/modules/user/dto/update-user.messages.ts deleted file mode 100644 index c07d0d8..0000000 --- a/src/shared/modules/user/dto/update-user.messages.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const UPDATE_USER_MESSAGES = { - EMAIL: { - invalidFormat: 'Email must be a valid address' - }, - AVATAR_PATH: { - invalidFormat: 'Avatar path is required', - }, - USERNAME: { - invalidFormat: 'Username is required', - lengthField: 'Min length is 1, max is 15', - }, - PASSWORD: { - invalidFormat: 'Password is required', - lengthField: 'Min length for password is 6, max is 12' - }, - USER_TYPE: { - invalid: 'User type must be one of the two types: "pro" or "обычный"', - } -} as const; diff --git a/src/shared/modules/user/user-service.interface.ts b/src/shared/modules/user/user-service.interface.ts index 35c1b5a..9f4b6ba 100644 --- a/src/shared/modules/user/user-service.interface.ts +++ b/src/shared/modules/user/user-service.interface.ts @@ -2,7 +2,7 @@ import { DocumentType } from '@typegoose/typegoose'; import { CreateUserDto } from './dto/create-user.dto.js'; import { UpdateUserDto } from './dto/update-user.dto.js'; -import { UserEntity } from '../../entities/index.js'; +import { OfferEntity, UserEntity } from '../../entities/index.js'; import { DocumentExists } from '../../types/document-exists.interface.js'; export interface UserService extends DocumentExists { @@ -11,7 +11,7 @@ export interface UserService extends DocumentExists { findOrCreate(dto: CreateUserDto, salt: string): Promise>; findById(userId: string): Promise | null>; updateById(userId: string, dto: UpdateUserDto): Promise | null>; - findFavoritesForUser(userId: string): Promise | null>; + findFavoritesForUser(userId: string): Promise[]>; addFavorite(userId: string, offerId: string): Promise | null>; deleteFavorite(userId: string, offerId: string): Promise | null>; } diff --git a/src/shared/modules/user/user.constant.ts b/src/shared/modules/user/user.constant.ts new file mode 100644 index 0000000..1af9dbe --- /dev/null +++ b/src/shared/modules/user/user.constant.ts @@ -0,0 +1,11 @@ +export const USER_DTO_CONSTRAINTS = { + USERNAME: { + MIN_LENGTH: 1, + MAX_LENGTH: 15 + }, + PASSWORD: { + MIN_LENGTH: 6, + MAX_LENGTH: 12 + }, +} as const; + \ No newline at end of file diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 8367c93..995bc47 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -1,7 +1,7 @@ import { inject, injectable } from 'inversify'; import { Request, Response } from 'express'; -import { BaseController, DocumentExistsMiddleware, HttpError, HttpMethod, ValidateDtoMiddleware, ValidateObjectIdMiddleware } from '../../libs/rest/index.js'; +import { BaseController, DocumentBodyExistsMiddleware, DocumentExistsMiddleware, HttpError, HttpMethod, UploadFileMiddleware, ValidateDtoMiddleware, ValidateObjectIdBodyMiddleware, ValidateObjectIdMiddleware } from '../../libs/rest/index.js'; import { Logger } from '../../libs/logger/index.js'; import { CreateUserRequest } from './create-user-request.type.js'; import { COMPONENT } from '../../constants/index.js'; @@ -13,12 +13,14 @@ import { UserRdo } from './rdo/user.rdo.js'; import { LoginUserRequest } from './login-user-request.type.js'; import { CreateUserDto } from './dto/create-user.dto.js'; import { LoginUserDto } from './dto/login-user.dto.js'; +import { OfferService, ShortOfferRdo } from '../offer/index.js'; @injectable() export class UserController extends BaseController { constructor( @inject(COMPONENT.LOGGER) protected readonly logger: Logger, @inject(COMPONENT.USER_SERVICE) private readonly userService: UserService, + @inject(COMPONENT.OFFER_SERVICE) private readonly offerService: OfferService, @inject(COMPONENT.CONFIG) private readonly configService: Config, ) { super(logger); @@ -27,10 +29,35 @@ export class UserController extends BaseController { this.addRoute({ path: '/register', method: HttpMethod.Post, handler: this.create, middlewares: [new ValidateDtoMiddleware(CreateUserDto)] }); this.addRoute({ path: '/login', method: HttpMethod.Post, handler: this.login, middlewares: [new ValidateDtoMiddleware(LoginUserDto)] }); this.addRoute({ path: '/login', method: HttpMethod.Get, handler: this.showStatus }); - this.addRoute({ path: '/:userId/favorites', method: HttpMethod.Get, handler: this.showUserFavorites, middlewares: [new ValidateObjectIdMiddleware('userId'), new DocumentExistsMiddleware(this.userService, 'User', 'userId')] }); - this.addRoute({ path: '/:userId/favorites', method: HttpMethod.Post, handler: this.addFavoriteForUser, middlewares: [new ValidateObjectIdMiddleware('userId'), new DocumentExistsMiddleware(this.userService, 'User', 'userId')] }); - this.addRoute({ path: '/:userId/favorites', method: HttpMethod.Delete, handler: this.deleteFavoriteForUser, middlewares: [new ValidateObjectIdMiddleware('userId'), new DocumentExistsMiddleware(this.userService, 'User', 'userId'),] }); - + this.addRoute({ path: '/favorites', method: HttpMethod.Get, handler: this.showUserFavorites, middlewares: [] }); + this.addRoute({ + path: '/favorites', + method: HttpMethod.Post, + handler: this.addFavoriteForUser, + middlewares: [ + new ValidateObjectIdBodyMiddleware('offerId'), + new DocumentBodyExistsMiddleware(this.offerService, 'Offer', 'offerId') + ] + }); + // -? Нужно спросить необходимость инжектировать сервис для проверки + this.addRoute({ + path: '/favorites/:offerId', + method: HttpMethod.Delete, + handler: this.deleteFavoriteForUser, + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId') + ] + }); + this.addRoute({ + path: '/:userId/avatar', + method: HttpMethod.Post, + handler: this.uploadAvatar, + middlewares: [ + new ValidateObjectIdMiddleware('userId'), + new UploadFileMiddleware(this.configService.get('UPLOAD_DIRECTORY'), 'avatarPath'), + ] + }) } // -? как идет проверка на статус, вошел ли пользователь в систему @@ -81,29 +108,41 @@ export class UserController extends BaseController { ); } + // TODO: Закрыть от неавторизированных пользователей public async showUserFavorites(_: Request, res: Response) { // TODO: Токен берется из токена авторизации - const userId = '67152f430ace5d6726f44745'; + const mockUserId = '67152f430ace5d6726f44745'; - const existsUser = await this.userService.findById(String(userId)); + const currentUser = await this.userService.findById(mockUserId); - if (!existsUser) { + if (!currentUser) { throw new HttpError( StatusCodes.NOT_FOUND, - `User with id ${userId} not found.`, + `User with id ${mockUserId} not found.`, 'UserController', ); } - const user = await this.userService.findFavoritesForUser(String(userId)); - this.ok(res, fillDTO(UserRdo, user)); + const offers = await this.userService.findFavoritesForUser(mockUserId); + this.ok(res, fillDTO(ShortOfferRdo, offers)); } // TODO: Можно написать DTO для params + // TODO: Закрыть от неавторизированных пользователей public async addFavoriteForUser(req: Request, res: Response) { - const { userId } = req.params; const { offerId } = req.body; + + // TODO: Не можем добавлять одни и те же офферы в избранное + // if (favorites.map((item) => item._id.toString()).includes(params.offerId)) { + // throw new HttpError( + // StatusCodes.CONFLICT, + // `Offer ${params.offerId} is already in favorites`, + // 'UserController', + // ); + // } + + const mockUserId = "67152f430ace5d6726f44745"; if (!offerId) { throw new HttpError( @@ -113,23 +152,25 @@ export class UserController extends BaseController { ); } - const existsUser = await this.userService.findById(String(userId)); + const existsUser = await this.userService.findById(mockUserId); if (!existsUser) { throw new HttpError( StatusCodes.NOT_FOUND, - `User with id ${userId} not found.`, + `User with id ${mockUserId} not found.`, 'UserController', ); } - const updatedUser = await this.userService.addFavorite(String(userId), offerId); - this.ok(res, fillDTO(UserRdo, updatedUser)); + const updatedUser = await this.userService.addFavorite(mockUserId, offerId); + this.noContent(res, updatedUser); } + // TODO: Закрыть от неавторизированных пользователей public async deleteFavoriteForUser(req: Request, res: Response) { - const { userId } = req.params; - const { offerId } = req.body; + const { offerId } = req.params; + + const mockUserId = "67152f430ace5d6726f44745"; if (!offerId) { throw new HttpError( @@ -139,17 +180,23 @@ export class UserController extends BaseController { ); } - const existsUser = await this.userService.findById(String(userId)); + const existsUser = await this.userService.findById(mockUserId); if (!existsUser) { throw new HttpError( StatusCodes.NOT_FOUND, - `User with id ${userId} not found.`, + `User with id ${mockUserId} not found.`, 'UserController', ); } - const updatedUser = await this.userService.deleteFavorite(String(userId), offerId); - this.ok(res, fillDTO(UserRdo, updatedUser)); + const updatedUser = await this.userService.deleteFavorite(mockUserId, offerId); + this.noContent(res, updatedUser); + } + + public async uploadAvatar(req: Request, res: Response) { + this.created(res, { + filepath: req.file?.path + }); } } diff --git a/src/shared/modules/user/user.http b/src/shared/modules/user/user.http index 48c8a8f..68a0448 100644 --- a/src/shared/modules/user/user.http +++ b/src/shared/modules/user/user.http @@ -24,4 +24,43 @@ Content-Type: application/json "password": "qw12345678" } +### + +## Показать список избранных предложений + +GET http://localhost:6000/users/favorites HTTP/1.1 +Content-Type: application/json + +### + +## Добавить предложение в избранное + +POST http://localhost:6000/users/favorites HTTP/1.1 +Content-Type: application/json + +{ + "offerId": "67152f430ace5d6726f4474d" +} + +### + +## Удалить предложение из избранного + +DELETE http://localhost:6000/users/favorites/67152f430ace5d6726f44747 HTTP/1.1 +Content-Type: application/json + +### + +## Отправить изображение + +POST http://localhost:6000/users/62823cb2c5a64ce9f1b50eb6/avatar HTTP/1.1 +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW + +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="avatar"; filename="screen.png" +Content-Type: image/png + +< /Users/spider_net/Desktop/screen.png +------WebKitFormBoundary7MA4YWxkTrZu0gW-- + ### \ No newline at end of file diff --git a/src/shared/types/comment.interface.ts b/src/shared/types/comment.interface.ts index ea27e1f..9701f83 100644 --- a/src/shared/types/comment.interface.ts +++ b/src/shared/types/comment.interface.ts @@ -4,5 +4,5 @@ export interface Comment { text: string; publicationDate: Date; rating: number; - author: User; + user: User; } diff --git a/src/shared/types/mock-server-data.interface.ts b/src/shared/types/mock-server-data.interface.ts index 46093d1..9d55f50 100644 --- a/src/shared/types/mock-server-data.interface.ts +++ b/src/shared/types/mock-server-data.interface.ts @@ -17,7 +17,7 @@ export interface MockServerData { guestCounts: number[], costs: number[], conveniences: ConvenienceType[], - authors: string[], + users: string[], commentCounts: number[], coordinates: Coordinate[] } diff --git a/src/shared/types/offer.interface.ts b/src/shared/types/offer.interface.ts index 219fe0d..f5f3f2d 100644 --- a/src/shared/types/offer.interface.ts +++ b/src/shared/types/offer.interface.ts @@ -18,7 +18,7 @@ export interface Offer { guestCount: number; cost: number; conveniences: ConvenienceType[]; - author: User; + user: User; commentCount: number; coordinate: Coordinate; } diff --git a/src/shared/types/sort-type.enum.ts b/src/shared/types/sort-type.enum.ts index c3ce626..b85a24f 100644 --- a/src/shared/types/sort-type.enum.ts +++ b/src/shared/types/sort-type.enum.ts @@ -1,4 +1,4 @@ export enum SortType { - DOWN = -1, - UP = 1, + DESC = -1, + ASC = 1, } diff --git a/src/specification/specification.yml b/src/specification/specification.yml index b8d8c8c..6c8c532 100644 --- a/src/specification/specification.yml +++ b/src/specification/specification.yml @@ -441,6 +441,34 @@ paths: "404": description: Не существует предложения для удаления. + /users/{userId}/avatar: + post: + tags: + - users + summary: Загрузка аватара пользователя + description: Загружает аватар пользователя в систему. + parameters: + - in: path + name: userId + required: true + schema: + type: string + responses: + "201": + description: Аватар загружен успешно. + + "400": + description: Некорректный файл. + + "401": + description: Пользователь не авторизован для выполнения данного действия. + + "403": + description: Нет доступа для загрузки аватара. + + "404": + description: Пользователя не существует. + components: @@ -532,7 +560,7 @@ components: - "Fridge" example: Towels - author: + userId: type: string format: uuid @@ -706,7 +734,7 @@ components: - "Fridge" example: Towels - author: + userId: type: string example: 6329c3d6a04ab1061c6425ea @@ -747,7 +775,7 @@ components: maximum: 5 example: 4.4 - author: + userId: type: string example: 6329c3d6a04ab1061c6425ea @@ -777,7 +805,7 @@ components: maximum: 5 example: 4.4 - author: + userId: type: string example: 6329c3d6a04ab1061c6425ea From 4310ef76c642a7d1b681eb7641d5f17426b90f4a Mon Sep 17 00:00:00 2001 From: Ilya Kolmakov Date: Sun, 3 Nov 2024 23:20:23 +0500 Subject: [PATCH 2/2] module7-task2: fix lint --- .../rest/middleware/upload-file.middleware.ts | 30 ++++++------- .../validate-objectid-body.middleware.ts | 2 +- .../modules/comment/comment.constant.ts | 17 ++++---- src/shared/modules/comment/rdo/comment.rdo.ts | 2 +- .../modules/offer/default-offer.service.ts | 16 +++---- .../modules/offer/dto/coordinate.dto.ts | 4 +- .../modules/offer/offer-service.interface.ts | 2 +- src/shared/modules/offer/offer.constant.ts | 42 +++++++++---------- src/shared/modules/offer/offer.controller.ts | 2 +- .../modules/offer/rdo/full-offer.rdo.ts | 2 +- src/shared/modules/offer/rdo/id-offer.rdo.ts | 2 +- .../modules/offer/rdo/short-offer.rdo.ts | 2 +- .../modules/user/default-user.service.ts | 4 +- src/shared/modules/user/user.constant.ts | 17 ++++---- src/shared/modules/user/user.controller.ts | 32 +++++++------- 15 files changed, 87 insertions(+), 89 deletions(-) diff --git a/src/shared/libs/rest/middleware/upload-file.middleware.ts b/src/shared/libs/rest/middleware/upload-file.middleware.ts index f7e4c0e..b634d99 100644 --- a/src/shared/libs/rest/middleware/upload-file.middleware.ts +++ b/src/shared/libs/rest/middleware/upload-file.middleware.ts @@ -24,23 +24,23 @@ export class UploadFileMiddleware implements Middleware { callback(null, `${filename}.${fileExtention}`); } }); - + // TODO: Разобраться const fileFilter = ( - _: Request, - file: Express.Multer.File, - callback: multer.FileFilterCallback, - ) => { - const fileExtention = file.originalname.split('.').pop(); - if (fileExtention && !IMAGE_EXTENSIONS.includes(fileExtention)) { - return callback(new HttpError( - StatusCodes.BAD_REQUEST, - 'Invalid file extension', - 'UploadFileMiddleware', - )); - } - return callback(null, true); - }; + _: Request, + file: Express.Multer.File, + callback: multer.FileFilterCallback, + ) => { + const fileExtention = file.originalname.split('.').pop(); + if (fileExtention && !IMAGE_EXTENSIONS.includes(fileExtention)) { + return callback(new HttpError( + StatusCodes.BAD_REQUEST, + 'Invalid file extension', + 'UploadFileMiddleware', + )); + } + return callback(null, true); + }; const uploadSingleFileMiddleware = multer({ storage, fileFilter }) .single(this.fieldName); diff --git a/src/shared/libs/rest/middleware/validate-objectid-body.middleware.ts b/src/shared/libs/rest/middleware/validate-objectid-body.middleware.ts index 06f0596..95adac1 100644 --- a/src/shared/libs/rest/middleware/validate-objectid-body.middleware.ts +++ b/src/shared/libs/rest/middleware/validate-objectid-body.middleware.ts @@ -21,4 +21,4 @@ export class ValidateObjectIdBodyMiddleware implements Middleware { 'ValidateObjectIdBodyMiddleware' ); } -} \ No newline at end of file +} diff --git a/src/shared/modules/comment/comment.constant.ts b/src/shared/modules/comment/comment.constant.ts index 529b1fc..2e0e745 100644 --- a/src/shared/modules/comment/comment.constant.ts +++ b/src/shared/modules/comment/comment.constant.ts @@ -1,13 +1,12 @@ export const DEFAULT_COMMENT_COUNT = 50; export const COMMENT_DTO_CONSTRAINTS = { - TEXT: { - MIN_LENGTH: 5, - MAX_LENGTH: 1024 - }, - RATING: { - MIN_VALUE: 1, - MAX_VALUE: 5 - }, + TEXT: { + MIN_LENGTH: 5, + MAX_LENGTH: 1024 + }, + RATING: { + MIN_VALUE: 1, + MAX_VALUE: 5 + }, } as const; - \ No newline at end of file diff --git a/src/shared/modules/comment/rdo/comment.rdo.ts b/src/shared/modules/comment/rdo/comment.rdo.ts index d2de747..a47bdb7 100644 --- a/src/shared/modules/comment/rdo/comment.rdo.ts +++ b/src/shared/modules/comment/rdo/comment.rdo.ts @@ -4,7 +4,7 @@ import { UserRdo } from '../../user/rdo/user.rdo.js'; export class CommentRdo { @Expose() - @Transform(params => params.obj._id.toString()) + @Transform((params) => params.obj._id.toString()) public id!: string; @Expose() diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index df12e48..a54f37d 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -79,7 +79,7 @@ export class DefaultOfferService implements OfferService { // .populate(['author']) } - + // TODO: Проверить метод public async findByPremium(): Promise[]> { return this.offerModel @@ -113,8 +113,8 @@ export class DefaultOfferService implements OfferService { } - public async findFavoritesByUserId(user: any): Promise[]> { - console.log("user", user); + public async findFavoritesByUserId(userId: string): Promise[]> { + console.log('userId', userId); return this.offerModel .aggregate([ ...authorAggregation, @@ -122,12 +122,12 @@ export class DefaultOfferService implements OfferService { .exec(); } - // console.log("user", user); + // console.log("user", user); - // const favoritesIds = user.favorites.map((item: Types.ObjectId) => ({ _id: item })); - // const user = await this.userService.findById(userId); - // const offers = user.favorites.map(() => await this.); - // console.log("user", user); + // const favoritesIds = user.favorites.map((item: Types.ObjectId) => ({ _id: item })); + // const user = await this.userService.findById(userId); + // const offers = user.favorites.map(() => await this.); + // console.log("user", user); // { $match: { 'author': new Types.ObjectId(userId) } }, // public async findNew(count: number): Promise[]> { diff --git a/src/shared/modules/offer/dto/coordinate.dto.ts b/src/shared/modules/offer/dto/coordinate.dto.ts index a56efb3..ffa835c 100644 --- a/src/shared/modules/offer/dto/coordinate.dto.ts +++ b/src/shared/modules/offer/dto/coordinate.dto.ts @@ -5,5 +5,5 @@ export class CoordinateDTO { public latitude!: number; @IsLongitude() - public longitude!: number; -} \ No newline at end of file + public longitude!: number; +} diff --git a/src/shared/modules/offer/offer-service.interface.ts b/src/shared/modules/offer/offer-service.interface.ts index 9aaad12..88ead83 100644 --- a/src/shared/modules/offer/offer-service.interface.ts +++ b/src/shared/modules/offer/offer-service.interface.ts @@ -15,7 +15,7 @@ export interface OfferService extends DocumentExists { // TODO: икремент добавления количества комментария - нужен он? incCommentCount(offerId: string): Promise | null>; // -? как правильно типизировать - findFavoritesByUserId(user: any): Promise[]>; + findFavoritesByUserId(userId: string): Promise[]>; exists(documentId: string): Promise; calculateOfferRating(offerId: string): Promise | null>; } diff --git a/src/shared/modules/offer/offer.constant.ts b/src/shared/modules/offer/offer.constant.ts index b7e10cb..3a7d4fb 100644 --- a/src/shared/modules/offer/offer.constant.ts +++ b/src/shared/modules/offer/offer.constant.ts @@ -3,25 +3,25 @@ export const DEFAULT_PREMIUM_OFFER_COUNT = 3; export const MAX_OFFER_COUNT = 300; export const OFFER_DTO_CONSTRAINTS = { - TITLE: { - MIN_LENGTH: 10, - MAX_LENGTH: 100 - }, - DESCRIPTION: { - MIN_LENGTH: 20, - MAX_LENGTH: 1024 - }, - FLAT_COUNT: { - MIN_VALUE: 1, - MAX_VALUE: 8, - }, - GUEST_COUNT: { - MIN_VALUE: 1, - MAX_VALUE: 10, - }, - COST: { - MIN_VALUE: 100, - MAX_VALUE: 100000 - } + TITLE: { + MIN_LENGTH: 10, + MAX_LENGTH: 100 + }, + DESCRIPTION: { + MIN_LENGTH: 20, + MAX_LENGTH: 1024 + }, + FLAT_COUNT: { + MIN_VALUE: 1, + MAX_VALUE: 8, + }, + GUEST_COUNT: { + MIN_VALUE: 1, + MAX_VALUE: 10, + }, + COST: { + MIN_VALUE: 100, + MAX_VALUE: 100000 + } } as const; - + diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index 948f47e..6d3e887 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -39,7 +39,7 @@ export class OfferController extends BaseController { method: HttpMethod.Get, handler: this.show, middlewares: [ - new ValidateObjectIdMiddleware('offerId'), + new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId')] }); this.addRoute({ path: '/:offerId', method: HttpMethod.Patch, handler: this.update, middlewares: [new ValidateObjectIdMiddleware('offerId'), new ValidateDtoMiddleware(UpdateOfferDto), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId')] }); diff --git a/src/shared/modules/offer/rdo/full-offer.rdo.ts b/src/shared/modules/offer/rdo/full-offer.rdo.ts index a49f249..6c1319b 100644 --- a/src/shared/modules/offer/rdo/full-offer.rdo.ts +++ b/src/shared/modules/offer/rdo/full-offer.rdo.ts @@ -4,7 +4,7 @@ import { UserRdo } from '../../user/rdo/user.rdo.js'; export class FullOfferRdo { @Expose() - @Transform(params => params.obj._id.toString()) + @Transform((params) => params.obj._id.toString()) public id!: string; @Expose() diff --git a/src/shared/modules/offer/rdo/id-offer.rdo.ts b/src/shared/modules/offer/rdo/id-offer.rdo.ts index 9042556..516ba49 100644 --- a/src/shared/modules/offer/rdo/id-offer.rdo.ts +++ b/src/shared/modules/offer/rdo/id-offer.rdo.ts @@ -2,6 +2,6 @@ import { Expose, Transform } from 'class-transformer'; export class IdOfferRdo { @Expose() - @Transform(params => params.obj._id.toString()) + @Transform((params) => params.obj._id.toString()) public id!: string; } diff --git a/src/shared/modules/offer/rdo/short-offer.rdo.ts b/src/shared/modules/offer/rdo/short-offer.rdo.ts index c2de87b..77bb42e 100644 --- a/src/shared/modules/offer/rdo/short-offer.rdo.ts +++ b/src/shared/modules/offer/rdo/short-offer.rdo.ts @@ -3,7 +3,7 @@ import { City, OfferType } from '../../../types/index.js'; export class ShortOfferRdo { @Expose() - @Transform(params => params.obj._id.toString()) + @Transform((params) => params.obj._id.toString()) public id!: string; @Expose() diff --git a/src/shared/modules/user/default-user.service.ts b/src/shared/modules/user/default-user.service.ts index 588bfdd..e648ca1 100644 --- a/src/shared/modules/user/default-user.service.ts +++ b/src/shared/modules/user/default-user.service.ts @@ -72,9 +72,9 @@ export class DefaultUserService implements UserService { // offerService findMany id // } public async findFavoritesForUser(userId: string): Promise[]> { - const user = await this.userModel.findById(userId); + // const user = await this.userModel.findById(userId); // -? Как прокинуть user вместо userId - return this.offerService.findFavoritesByUserId(user); + return this.offerService.findFavoritesByUserId(userId); } // TODO: Закрыть от неавторизированных пользователей diff --git a/src/shared/modules/user/user.constant.ts b/src/shared/modules/user/user.constant.ts index 1af9dbe..26bb08d 100644 --- a/src/shared/modules/user/user.constant.ts +++ b/src/shared/modules/user/user.constant.ts @@ -1,11 +1,10 @@ export const USER_DTO_CONSTRAINTS = { - USERNAME: { - MIN_LENGTH: 1, - MAX_LENGTH: 15 - }, - PASSWORD: { - MIN_LENGTH: 6, - MAX_LENGTH: 12 - }, + USERNAME: { + MIN_LENGTH: 1, + MAX_LENGTH: 15 + }, + PASSWORD: { + MIN_LENGTH: 6, + MAX_LENGTH: 12 + }, } as const; - \ No newline at end of file diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 995bc47..f80a579 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -30,24 +30,24 @@ export class UserController extends BaseController { this.addRoute({ path: '/login', method: HttpMethod.Post, handler: this.login, middlewares: [new ValidateDtoMiddleware(LoginUserDto)] }); this.addRoute({ path: '/login', method: HttpMethod.Get, handler: this.showStatus }); this.addRoute({ path: '/favorites', method: HttpMethod.Get, handler: this.showUserFavorites, middlewares: [] }); - this.addRoute({ - path: '/favorites', - method: HttpMethod.Post, - handler: this.addFavoriteForUser, + this.addRoute({ + path: '/favorites', + method: HttpMethod.Post, + handler: this.addFavoriteForUser, middlewares: [ - new ValidateObjectIdBodyMiddleware('offerId'), + new ValidateObjectIdBodyMiddleware('offerId'), new DocumentBodyExistsMiddleware(this.offerService, 'Offer', 'offerId') - ] + ] }); // -? Нужно спросить необходимость инжектировать сервис для проверки - this.addRoute({ - path: '/favorites/:offerId', - method: HttpMethod.Delete, - handler: this.deleteFavoriteForUser, + this.addRoute({ + path: '/favorites/:offerId', + method: HttpMethod.Delete, + handler: this.deleteFavoriteForUser, middlewares: [ - new ValidateObjectIdMiddleware('offerId'), + new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId') - ] + ] }); this.addRoute({ path: '/:userId/avatar', @@ -57,7 +57,7 @@ export class UserController extends BaseController { new ValidateObjectIdMiddleware('userId'), new UploadFileMiddleware(this.configService.get('UPLOAD_DIRECTORY'), 'avatarPath'), ] - }) + }); } // -? как идет проверка на статус, вошел ли пользователь в систему @@ -132,7 +132,7 @@ export class UserController extends BaseController { // TODO: Закрыть от неавторизированных пользователей public async addFavoriteForUser(req: Request, res: Response) { const { offerId } = req.body; - + // TODO: Не можем добавлять одни и те же офферы в избранное // if (favorites.map((item) => item._id.toString()).includes(params.offerId)) { // throw new HttpError( @@ -142,7 +142,7 @@ export class UserController extends BaseController { // ); // } - const mockUserId = "67152f430ace5d6726f44745"; + const mockUserId = '67152f430ace5d6726f44745'; if (!offerId) { throw new HttpError( @@ -170,7 +170,7 @@ export class UserController extends BaseController { public async deleteFavoriteForUser(req: Request, res: Response) { const { offerId } = req.params; - const mockUserId = "67152f430ace5d6726f44745"; + const mockUserId = '67152f430ace5d6726f44745'; if (!offerId) { throw new HttpError(