diff --git a/Workflow.md b/Workflow.md index ad39480..235bbc4 100644 --- a/Workflow.md +++ b/Workflow.md @@ -97,13 +97,13 @@ npm run mock:server ### --generate: ```bash -npm run ts ./src/main.cli.ts -- --generate 100 ./mocks/test-data.tsv http://localhost:3123/api +npm run ts ./src/main.cli.ts -- --generate 100 ./src/mocks/test-data.tsv http://localhost:3123/api ``` ### --import ```bash -npm run ts ./src/main.cli.ts -- --import ./src/mocks/test-data.tsv admin test localhost six-sities secret +npm run ts ./src/main.cli.ts -- --import ./src/mocks/test-data.tsv admin test localhost six-cities secret ``` ```bash diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..d8b065b --- /dev/null +++ b/nodemon.json @@ -0,0 +1,7 @@ +{ + "watch": [ + "src" + ], + "ext": "ts, json", + "exec": "npm run ts ./src/main.rest.ts | pino-pretty --colorize --translateTime SYS:standard" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8955931..95914e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "eslint": "8.49.0", "eslint-config-htmlacademy": "9.1.1", "json-server": "0.17.3", + "nodemon": "3.1.0", "pino-pretty": "11.0.0", "rimraf": "5.0.1", "ts-node": "10.9.2", @@ -813,6 +814,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1063,6 +1078,19 @@ "node": ">= 0.8" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1273,6 +1301,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -3186,6 +3252,21 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3636,6 +3717,13 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3769,6 +3857,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -4909,6 +5010,58 @@ "node": ">= 0.6" } }, + "node_modules/nodemon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", + "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -4931,6 +5084,16 @@ "semver": "bin/semver" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", @@ -5524,6 +5687,13 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -5747,6 +5917,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -6229,6 +6412,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6691,6 +6887,16 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tr46": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", @@ -6921,6 +7127,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -7857,6 +8070,16 @@ "color-convert": "^2.0.1" } }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -8020,6 +8243,12 @@ "safe-buffer": "5.1.2" } }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true + }, "body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -8161,6 +8390,33 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -9515,6 +9771,13 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -9804,6 +10067,12 @@ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -9898,6 +10167,15 @@ "has-bigints": "^1.0.1" } }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -10649,6 +10927,41 @@ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true }, + "nodemon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", + "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -10669,6 +10982,12 @@ } } }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, "normalize-url": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", @@ -11081,6 +11400,12 @@ "ipaddr.js": "1.9.1" } }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -11233,6 +11558,15 @@ "string_decoder": "^1.3.0" } }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, "real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -11565,6 +11899,15 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true }, + "simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -11877,6 +12220,12 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true + }, "tr46": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", @@ -12025,6 +12374,12 @@ "which-boxed-primitive": "^1.0.2" } }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index 413b858..184e495 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ ], "main": "main.js", "scripts": { - "start:dev": "npm run ts ./src/main.rest.ts | pino-pretty --colorize --translateTime SYS:standard", + "start": "npm run build && node ./dist/main.rest.js", + "start:dev": "nodemon", "build": "npm run clean && npm run compile", "lint": "eslint src/ --ext .ts", "lint:fix": "eslint --fix src/ --ext .ts", @@ -27,6 +28,7 @@ "eslint": "8.49.0", "eslint-config-htmlacademy": "9.1.1", "json-server": "0.17.3", + "nodemon": "3.1.0", "pino-pretty": "11.0.0", "rimraf": "5.0.1", "ts-node": "10.9.2", diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index 4656856..41e908c 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -73,7 +73,7 @@ export class ImportCommand implements Command { cost: offer.cost, conveniences: offer.conveniences, author: user.id, // userId - commentCount: offer.commentCount, + commentCount: offer.commentCount || 0, coordinate: offer.coordinate, }); diff --git a/src/main.rest.ts b/src/main.rest.ts index 4366874..b4da8e1 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -4,6 +4,7 @@ import { Container } from 'inversify'; import { createRestApplicationContainer } from './rest/rest.container.js'; import { createUserContainer } from './shared/modules/user/index.js'; import { createOfferContainer } from './shared/modules/offer/index.js'; +import { createCommentContainer } from './shared/modules/comment/index.js'; import { RestApplication } from './rest/index.js'; import { COMPONENT } from './shared/constants/index.js'; @@ -13,6 +14,7 @@ async function bootstrap() { createRestApplicationContainer(), createUserContainer(), createOfferContainer(), + createCommentContainer(), ); const application = appContainer.get(COMPONENT.REST_APPLICATION); diff --git a/src/mocks/mock-server-data.json b/src/mocks/mock-server-data.json index f9883d3..9ac8b42 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 Kolmakov", "Ivan Ivanov", "Elena Kolosova", "Vladimir Korotkov", "Alexander Potemkin", "Elizaveta Linvinova"], + "authors": ["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 1eb6e59..b9b8f24 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -5,7 +5,6 @@ import { Config, IRestSchema } from '../shared/libs/config/index.js'; import { COMPONENT } from '../shared/constants/index.js'; import { DatabaseClient } from '../shared/libs/database-client/index.js'; import { getMongoURI } from '../shared/helpers/index.js'; -import { UserModel } from '../shared/modules/user/index.js'; @injectable() export class RestApplication { @@ -34,17 +33,5 @@ export class RestApplication { this.logger.info('Init database…'); await this.initDb(); this.logger.info('Init database completed'); - - const user = await UserModel.create({ - email: 'ilkolmakov@yandex.ru', - avatarPath: 'keks.jpg', - firstname: 'Keks', - lastname: 'Unknown' - }); - - const error = user.validateSync(); - console.log(error); - - // console.log(user); } } diff --git a/src/shared/constants/component.constant.ts b/src/shared/constants/component.constant.ts index ec68c8f..2869354 100644 --- a/src/shared/constants/component.constant.ts +++ b/src/shared/constants/component.constant.ts @@ -7,4 +7,6 @@ export const COMPONENT = { USER_MODEL: Symbol.for('kUserModel'), OFFER_SERVICE: Symbol.for('kOfferService'), OFFER_MODEL: Symbol.for('kOfferModel'), + COMMENT_SERVICE: Symbol.for('kCommentService'), + COMMENT_MODEL: Symbol.for('kCommentModel'), } as const; diff --git a/src/shared/libs/file-reader/tsv-file-reader.ts b/src/shared/libs/file-reader/tsv-file-reader.ts index ae40ed2..d696f53 100644 --- a/src/shared/libs/file-reader/tsv-file-reader.ts +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -68,11 +68,12 @@ export class TSVFileReader extends EventEmitter implements FileReader { }; } - private parseUser(fullName: string): User { - const [firstName, lastName] = fullName.split(' '); + private parseUser(userName: string): User { + // const [firstName, lastName] = fullName.split(' '); return { - firstName: firstName || '', - lastName: lastName || '', + // firstName: firstName || '', + // lastName: lastName || '', + userName, email: '', avatarPath: '', password: '', diff --git a/src/shared/modules/comment/comment-service.interface.ts b/src/shared/modules/comment/comment-service.interface.ts new file mode 100644 index 0000000..67ba87c --- /dev/null +++ b/src/shared/modules/comment/comment-service.interface.ts @@ -0,0 +1,10 @@ +import { DocumentType } from '@typegoose/typegoose'; + +import { CreateCommentDto } from './dto/create-comment.dto.js'; +import { CommentEntity } from './comment.entity.js'; + +export interface CommentService { + create(dto: CreateCommentDto): Promise>; + findByOfferId(offerId: string): Promise[]>; + deleteByOfferId(offerId: string): Promise; +} diff --git a/src/shared/modules/comment/comment.constant.ts b/src/shared/modules/comment/comment.constant.ts new file mode 100644 index 0000000..45239e9 --- /dev/null +++ b/src/shared/modules/comment/comment.constant.ts @@ -0,0 +1 @@ +export const DEFAULT_COMMENT_COUNT = 50; diff --git a/src/shared/modules/comment/comment.container.ts b/src/shared/modules/comment/comment.container.ts new file mode 100644 index 0000000..b44dd42 --- /dev/null +++ b/src/shared/modules/comment/comment.container.ts @@ -0,0 +1,20 @@ +import { Container } from 'inversify'; +import { types } from '@typegoose/typegoose'; + +import { CommentService } from './comment-service.interface.js'; +import { CommentEntity, CommentModel } from './comment.entity.js'; +import { DefaultCommentService } from './default-comment.service.js'; +import { COMPONENT } from '../../constants/index.js'; + +export function createCommentContainer() { + const commentContainer = new Container(); + + commentContainer.bind(COMPONENT.COMMENT_SERVICE) + .to(DefaultCommentService) + .inSingletonScope(); + + commentContainer.bind>(COMPONENT.COMMENT_MODEL) + .toConstantValue(CommentModel); + + return commentContainer; +} diff --git a/src/shared/modules/comment/comment.entity.ts b/src/shared/modules/comment/comment.entity.ts new file mode 100644 index 0000000..64f7d7d --- /dev/null +++ b/src/shared/modules/comment/comment.entity.ts @@ -0,0 +1,42 @@ +import { + defaultClasses, + getModelForClass, + modelOptions, + prop, + Ref +} from '@typegoose/typegoose'; + +import { OfferEntity } from '../offer/index.js'; +import { UserEntity } from '../user/index.js'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface CommentEntity extends defaultClasses.Base {} + + @modelOptions({ + schemaOptions: { + collection: 'comments', + timestamps: true, + } + }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class CommentEntity extends defaultClasses.TimeStamps { + @prop({ trim: true, required: true }) + public text!: string; + + @prop({ required: true }) + public rating!: number; + + @prop({ + ref: OfferEntity, + required: true + }) + public offerId!: Ref; + + @prop({ + ref: UserEntity, + required: true, + }) + public author!: Ref; +} + +export const CommentModel = getModelForClass(CommentEntity); diff --git a/src/shared/modules/comment/default-comment.service.ts b/src/shared/modules/comment/default-comment.service.ts new file mode 100644 index 0000000..34c83c8 --- /dev/null +++ b/src/shared/modules/comment/default-comment.service.ts @@ -0,0 +1,45 @@ +import { inject, injectable } from 'inversify'; +import { DocumentType, types } from '@typegoose/typegoose'; + +import { CommentService } from './comment-service.interface.js'; +import { CommentEntity } from './comment.entity.js'; +import { CreateCommentDto } from './dto/create-comment.dto.js'; +import { COMPONENT } from '../../constants/component.constant.js'; +import { SortType } from '../../types/sort-type.enum.js'; +import { DEFAULT_COMMENT_COUNT } from './comment.constant.js'; +import { OfferService } from '../offer/offer-service.interface.js'; + +@injectable() +export class DefaultCommentService implements CommentService { + constructor( + @inject(COMPONENT.COMMENT_MODEL) private readonly commentModel: types.ModelType, + @inject(COMPONENT.OFFER_SERVICE) private readonly offerService: OfferService + ) {} + + // TODO: Добавить вывод 50 или меньше последних комментариев - SUCCESS + // TODO: Комментарии отсортированы по дате публикации от нового к старому + public async findByOfferId(offerId: string): Promise[]> { + return this.commentModel + .find({offerId}, {}, { limit: DEFAULT_COMMENT_COUNT }) + .sort({ createdAt: SortType.DOWN }) + .populate('author'); + } + + // TODO: Закрыть от неавторизированных пользователей + // INFO: Добавить перерасчет при добавлении комментария + public async create(dto: CreateCommentDto): Promise> { + const comment = await this.commentModel.create(dto); + + await this.offerService.calculateOfferRating(String(comment.author)); + return comment.populate('author'); + } + + // TODO: Ручка удаления комментариев вместе с предложениями + public async deleteByOfferId(offerId: string): Promise { + const result = await this.commentModel + .deleteMany({offerId}) + .exec(); + + return result.deletedCount; + } +} diff --git a/src/shared/modules/comment/dto/create-comment.dto.ts b/src/shared/modules/comment/dto/create-comment.dto.ts new file mode 100644 index 0000000..6a00b20 --- /dev/null +++ b/src/shared/modules/comment/dto/create-comment.dto.ts @@ -0,0 +1,6 @@ +export class CreateCommentDto { + public text!: string; + public rating!: number; + public offerId!: string; + public userId!: string; +} diff --git a/src/shared/modules/comment/index.ts b/src/shared/modules/comment/index.ts new file mode 100644 index 0000000..3a18cae --- /dev/null +++ b/src/shared/modules/comment/index.ts @@ -0,0 +1,4 @@ +export { CommentService } from './comment-service.interface.js'; +export { createCommentContainer } from './comment.container.js'; +export { CommentEntity, CommentModel } from './comment.entity.js'; +export { DefaultCommentService } from './default-comment.service.js'; diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index dcebf66..60f4a71 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -6,6 +6,9 @@ import { Logger } from '../../libs/logger/index.js'; import { OfferEntity } from './offer.entity.js'; import { CreateOfferDto } from './dto/create-offer.dto.js'; import { COMPONENT } from '../../constants/component.constant.js'; +import { UpdateOfferDto } from './dto/update-offer.dto.js'; +import { DEFAULT_OFFER_COUNT, DEFAULT_PREMIUM_OFFER_COUNT } from './offer.constant.js'; +import { SortType } from '../../types/sort-type.enum.js'; @injectable() export class DefaultOfferService implements OfferService { @@ -14,6 +17,24 @@ export class DefaultOfferService implements OfferService { @inject(COMPONENT.OFFER_MODEL) private readonly offerModel: types.ModelType ) {} + // TODO: Возвращать не больше 60 предложений об аренде - SUCCESS + // TODO: Клиент может запросить больше указав нужное количество - SUCCESS + // TODO: Отсортированный список по дате публикации - SUCCESS + // TODO: Добавить и рассчитать динамически флаг избранного предложения + public async find(count?: number): Promise[]> { + const limit = count ?? DEFAULT_OFFER_COUNT; + return this.offerModel + .find({}, {}, { limit }) + // .limit(limit) + + // expose декоратор rename + .sort({ publicationDate: SortType.DOWN }) + // .aggregate(offerAggregation) + .populate(['author']) + .exec(); + } + + // TODO: Закрыть от неавторизированных пользователей public async create(dto: CreateOfferDto): Promise> { const result = await this.offerModel.create(dto); this.logger.info(`New offer created: ${dto.title}`); @@ -21,7 +42,88 @@ export class DefaultOfferService implements OfferService { return result; } + // TODO: Закрыть от неавторизированных пользователей + public async updateById(offerId: string, dto: UpdateOfferDto): Promise | null> { + return this.offerModel + .findByIdAndUpdate(offerId, dto, {new: true}) + .populate(['author']) + .exec(); + } + + // TODO: Удалять вместо с предложением комментарии авторматически + // TODO: Закрыть от неавторизированных пользователей + public async deleteById(offerId: string): Promise | null> { + return this.offerModel + .findByIdAndDelete(offerId) + .exec(); + } + + // TODO: Добавить и рассчитать динамически флаг избранного предложения public async findById(offerId: string): Promise | null> { - return this.offerModel.findById(offerId).exec(); + return this.offerModel.findById(offerId).populate(['author']).exec(); + } + + + // short offer rdo + // full offer rdo + public async findByPremium(): Promise[]> { + return this.offerModel + .find({ isPremium: true }, {}, { limit: DEFAULT_PREMIUM_OFFER_COUNT }) + .sort({ publicationDate: SortType.DOWN }); } + + // INFO: икремент добавления количества комментария // - обратиться к сервису оферов + public async incCommentCount(offerId: string): Promise | null> { + return this.offerModel + .findByIdAndUpdate(offerId, { + '$inc': { + commentCount: 1, + } + }).exec(); + } + + public async calculateOfferRating(offerId: string): Promise | null> { + return this.offerModel.findByIdAndUpdate(offerId, { + // '$avg': { + // rating: 1, + // } + '$group': { + _id: '$', + averageQty: { $avg: '$' }, + }, + }).exec(); + } + + // TODO: проверка на существование документа - предложения + public async exists(documentId: string): Promise { + return (await this.offerModel + .exists({_id: documentId})) !== null; + } + + + // 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 findNew(count: number): Promise[]> { + // return this.offerModel + // .find() + // .sort({ createdAt: SortType.Down }) + // .limit(count) + // .populate(['userId', 'categories']) + // .exec(); + // } + + // public async findDiscussed(count: number): Promise[]> { + // return this.offerModel + // .find() + // .sort({ commentCount: SortType.Down }) + // .limit(count) + // .populate(['userId', 'categories']) + // .exec(); + // } } diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts index cbe811e..58a9d01 100644 --- a/src/shared/modules/offer/dto/create-offer.dto.ts +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -3,7 +3,6 @@ import { City, ConvenienceType, Coordinate, OfferType } from '../../../types/ind export class CreateOfferDto { public title!: string; public description!: string; - public publicationDate!: Date; public city!: City; public previewImg!: string; public images!: string[]; @@ -14,7 +13,5 @@ export class CreateOfferDto { public guestCount!: number; public cost!: number; public conveniences!: ConvenienceType[]; - public author!: string; - public commentCount!: number; public coordinate!: Coordinate; } diff --git a/src/shared/modules/offer/dto/update-offer.dto.ts b/src/shared/modules/offer/dto/update-offer.dto.ts new file mode 100644 index 0000000..91cb26e --- /dev/null +++ b/src/shared/modules/offer/dto/update-offer.dto.ts @@ -0,0 +1,17 @@ +import { City, ConvenienceType, Coordinate, OfferType } from '../../../types/index.js'; + +export class UpdateOfferDto { + public title?: string; + public description?: string; + public city?: City; + public previewImg?: string; + public images?: string[]; + public isPremium?: boolean; + public rating?: number; + public type?: OfferType; + public flatCount?: number; + public guestCount?: number; + public cost?: number; + public conveniences?: ConvenienceType[]; + public coordinate?: Coordinate; +} diff --git a/src/shared/modules/offer/offer-service.interface.ts b/src/shared/modules/offer/offer-service.interface.ts index 04d4d80..0f2db1c 100644 --- a/src/shared/modules/offer/offer-service.interface.ts +++ b/src/shared/modules/offer/offer-service.interface.ts @@ -2,8 +2,19 @@ import { DocumentType } from '@typegoose/typegoose'; import { CreateOfferDto } from './dto/create-offer.dto.js'; import { OfferEntity } from './offer.entity.js'; +import { UpdateOfferDto } from './dto/update-offer.dto.js'; export interface OfferService { + find(): Promise[]>; create(dto: CreateOfferDto): Promise>; findById(offerId: string): Promise | null>; + deleteById(offerId: string): Promise | null>; + updateById(offerId: string, dto: UpdateOfferDto): Promise | null>; + findByPremium(): Promise[]>; + // TODO: икремент добавления количества комментария - нужен он? + incCommentCount(offerId: string): Promise | null>; + // findNew(count: number): Promise[]>; + // findDiscussed(count: number): 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 new file mode 100644 index 0000000..6e1d2e4 --- /dev/null +++ b/src/shared/modules/offer/offer.aggregation.ts @@ -0,0 +1,75 @@ +import { SortType } from '../../types/sort-type.enum.js'; +import { DEFAULT_COMMENT_COUNT } from '../comment/comment.constant.js'; + +// export const offerRatingAggregation = [ +// { +// $lookup: { +// // Коллекция к которой хотим присоединится +// from: 'comments', +// // Поле, к которому мы хотим присоединиться, в локальной коллекции (коллекции, к которой мы выполняем запрос) +// localField: '_id', +// // Поле, к которому мы хотим присоединиться, во внешней коллекции (коллекция, к которой мы хотим присоединиться) +// foreignField: 'offerId', +// let: { rating: '$_rating'}, +// pipeline: [ +// { $match: { $expr: { $eq: ['$$categoryId', '$categories'] } } }, +// { $project : { _id : 0, rating: 1 } }, +// ], +// // Имя выходного массива для результатов +// as: 'comments' +// }, +// }, +// { +// $set: { +// avgRating: { $avg: '$ratings.rating' } +// } +// }, +// { +// $unset: 'ratings' +// } +// ]; + +export const populateAuthor = { + $lookup: { + from: 'users', + localField: 'userId', + foreignField: '_id', + as: 'author', + } +}; + +export const commentCountAggregation = [{ + $lookup: { + from: 'comments', + let: { offerId: '$_id'}, + pipeline: [ + // $eq - равно + { $match: { $expr: { $eq: ['$offerId', '$$offerId'] } } }, + { $project: { _id: 1}} + ], + as: 'comments' + }, +}, +{ id: { $toString: '$_id'}, commentCount: { $size: '$comments' } }, +{ $unset: 'comments' } +]; + +export const populateComments = [ + { + $lookup: { + from: 'comments', + let: { offerId: '$_id'}, + pipeline: [ + { $match: { $expr: { $eq: ['$offerId', '$$offerId'] } } }, + { $project: { _id: 1, text: 1, rating: 1, createdAt: 1 }} + ], + as: 'comments' + }, + }, + { + $limit: DEFAULT_COMMENT_COUNT + }, + { + $sort: { createdAt: SortType.DOWN } + } +]; diff --git a/src/shared/modules/offer/offer.constant.ts b/src/shared/modules/offer/offer.constant.ts new file mode 100644 index 0000000..e02f6cb --- /dev/null +++ b/src/shared/modules/offer/offer.constant.ts @@ -0,0 +1,2 @@ +export const DEFAULT_OFFER_COUNT = 60; +export const DEFAULT_PREMIUM_OFFER_COUNT = 3; diff --git a/src/shared/modules/user/default-user.service.ts b/src/shared/modules/user/default-user.service.ts index a964469..bbe15bc 100644 --- a/src/shared/modules/user/default-user.service.ts +++ b/src/shared/modules/user/default-user.service.ts @@ -7,6 +7,8 @@ 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 { OfferEntity } from '../offer/offer.entity.js'; @injectable() export class DefaultUserService implements UserService { @@ -41,4 +43,30 @@ export class DefaultUserService implements UserService { return this.create(dto, salt); } + + public async updateById(userId: string, dto: UpdateUserDto): Promise | null> { + return this.userModel + .findByIdAndUpdate(userId, dto, { new: true }) + .exec(); + } + + // ? + // // TODO: Закрыть от неавторизированных пользователей + // public async findByFavorites(userId: string): Promise[]> { + // // 1. Найти пользователя + // // 2. Найти у него массив избранных предложений + // // 3. Распарсить массив значений в список офферов + // return this.userModel.findById(userId); + // offerService findMany id + // } + + // TODO: Закрыть от неавторизированных пользователей + public async addFavorite(userId: string, offerId: string): Promise | null> { + return this.userModel.findByIdAndUpdate(userId, { $addToSet: { favorites: 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(); + } } diff --git a/src/shared/modules/user/dto/create-user.dto.ts b/src/shared/modules/user/dto/create-user.dto.ts index d23bbab..1149879 100644 --- a/src/shared/modules/user/dto/create-user.dto.ts +++ b/src/shared/modules/user/dto/create-user.dto.ts @@ -7,8 +7,9 @@ export class CreateUserDto { public email!: string; // TODO: Указываем дефолтное значение и поле необязательно public avatarPath!: string; - public firstName!: string; - public lastName!: string; + public userName!: string; + // public firstName!: string; + // public lastName!: string; public password!: string; public type!: UserType; } diff --git a/src/shared/modules/user/dto/update-user.dto.ts b/src/shared/modules/user/dto/update-user.dto.ts new file mode 100644 index 0000000..6e9f157 --- /dev/null +++ b/src/shared/modules/user/dto/update-user.dto.ts @@ -0,0 +1,12 @@ +import { UserType } from '../../../types/index.js'; + +export class UpdateUserDto { + public email!: string; + // TODO: Указываем дефолтное значение и поле необязательно + public avatarPath!: string; + public userName!: string; + // public firstName!: string; + // public lastName!: string; + public password!: string; + public type!: UserType; +} diff --git a/src/shared/modules/user/user-service.interface.ts b/src/shared/modules/user/user-service.interface.ts index 6a2d91b..1db8b73 100644 --- a/src/shared/modules/user/user-service.interface.ts +++ b/src/shared/modules/user/user-service.interface.ts @@ -2,9 +2,13 @@ import { DocumentType } from '@typegoose/typegoose'; import { UserEntity } from './user.entity.js'; import { CreateUserDto } from './dto/create-user.dto.js'; +import { UpdateUserDto } from './dto/update-user.dto.js'; export interface UserService { create(dto: CreateUserDto, salt: string): Promise>; findByEmail(email: string): Promise | null>; findOrCreate(dto: CreateUserDto, salt: string): Promise>; + updateById(userId: string, dto: UpdateUserDto): Promise | null>; + addFavorite(userId: string, offerId: string): Promise | null>; + deleteFavorite(userId: string, offerId: string): Promise | null>; } diff --git a/src/shared/modules/user/user.aggregation.ts b/src/shared/modules/user/user.aggregation.ts new file mode 100644 index 0000000..ef05f13 --- /dev/null +++ b/src/shared/modules/user/user.aggregation.ts @@ -0,0 +1,8 @@ +export const favoritesAggregation = { + $lookup: { + from: 'offers', + localField: 'favorites', + foreignField: '_id', + as: 'favorites' + } +}; diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts index d903217..21557bc 100644 --- a/src/shared/modules/user/user.entity.ts +++ b/src/shared/modules/user/user.entity.ts @@ -1,7 +1,8 @@ -import { defaultClasses, getModelForClass, prop, modelOptions } from '@typegoose/typegoose'; +import { defaultClasses, getModelForClass, prop, modelOptions, Ref } from '@typegoose/typegoose'; import { User, UserType } from '../../types/index.js'; import { createSHA256 } from '../../helpers/index.js'; +import { OfferEntity } from '../offer/offer.entity.js'; // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export interface UserEntity extends defaultClasses.Base {} @@ -15,11 +16,14 @@ export interface UserEntity extends defaultClasses.Base {} // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class UserEntity extends defaultClasses.TimeStamps implements User { -@prop({ required: true }) - public firstName!: string; + // @prop({ required: true }) + // public firstName!: string; + + // @prop({ required: true }) + // public lastName!: string; @prop({ required: true }) -public lastName!: string; + public userName!: string; @prop({ unique: true, required: true }) public email!: string; @@ -33,13 +37,20 @@ public password!: string; @prop({ required: true, enum: UserType }) public type!: UserType; +@prop({ + ref: () => OfferEntity, + default: [], +}) +public favorites?: Ref[]; + constructor(userData: User) { super(); this.email = userData.email; this.avatarPath = userData.avatarPath; - this.firstName = userData.firstName; - this.lastName = userData.lastName; + this.userName = userData.userName; + // this.firstName = userData.firstName; + // this.lastName = userData.lastName; this.type = userData.type; } diff --git a/src/shared/modules/user/user.model.ts b/src/shared/modules/user/user.model.ts index 870dd90..7597fe0 100644 --- a/src/shared/modules/user/user.model.ts +++ b/src/shared/modules/user/user.model.ts @@ -19,12 +19,18 @@ const userSchema = new Schema({ required: true, minlength: [5, 'Min length for avatar path is 5'], }, - firstName: { + userName: { type: String, required: true, - minlength: [2, 'Min length for firstname is 2'] + minlength: [1, 'Min length for userName is 1'], + maxlength: [15, 'Max length for userName is 15'], }, - lastName: String, + // firstName: { + // type: String, + // required: true, + // minlength: [2, 'Min length for firstname is 2'] + // }, + // lastName: String, type: { enum: UserType, required: true, diff --git a/src/shared/types/sort-type.enum.ts b/src/shared/types/sort-type.enum.ts new file mode 100644 index 0000000..c3ce626 --- /dev/null +++ b/src/shared/types/sort-type.enum.ts @@ -0,0 +1,4 @@ +export enum SortType { + DOWN = -1, + UP = 1, +} diff --git a/src/shared/types/user.interface.ts b/src/shared/types/user.interface.ts index e6c58a9..4b6dc40 100644 --- a/src/shared/types/user.interface.ts +++ b/src/shared/types/user.interface.ts @@ -1,8 +1,9 @@ import { UserType } from './user-type.enum.js'; export interface User { - firstName: string; - lastName: string; + // firstName: string; + // lastName: string; + userName: string; email: string; avatarPath: string; password?: string; diff --git a/src/specification/specification.yml b/src/specification/specification.yml index a376422..d93b576 100644 --- a/src/specification/specification.yml +++ b/src/specification/specification.yml @@ -34,7 +34,6 @@ paths: content: application/json: schema: - # offers: type: array items: $ref: '#/components/schemas/publicOffer' @@ -65,8 +64,8 @@ paths: "400": description: Неверные параметры для создания предложения. - "403": - description: Нет доступа на создание предложения. + "401": + description: Пользователь не авторизован для выполнения данного действия. @@ -92,7 +91,7 @@ paths: "404": description: Не существует предложение. - put: + patch: tags: - offers summary: Редактирование предложения @@ -121,6 +120,9 @@ paths: "400": description: Неверные параметры при редактировании предложения. + + "401": + description: Пользователь не авторизован для выполнения данного действия. "403": description: Нет доступа при редактировании предложения. @@ -151,6 +153,9 @@ paths: id: type: string example: 6329c3d6a04ab1061c6425ea + + "401": + description: Пользователь не авторизован для выполнения данного действия. "403": description: Нет доступа для удаления предложения. @@ -248,7 +253,6 @@ paths: content: application/json: schema: - # offers: type: array items: $ref: '#/components/schemas/publicOffer' @@ -278,6 +282,9 @@ paths: application/json: schema: $ref: '#/components/schemas/user' + + "400": + description: Неверные параметры у запроса. "409": description: Пользователь с таким email уже существует. @@ -336,19 +343,6 @@ paths: "401": description: Пользователь не зашел в систему. - /users/logout: - get: - tags: - - users - summary: Выход из закрытой части приложения - description: Разлогин из закрытой части приложения. - responses: - "204": - description: Пользователь успешно вышел из системы. - - "401": - description: Пользователь не зашел в систему. - /users/{userId}/favorites: get: tags: @@ -378,8 +372,8 @@ paths: post: tags: - users - summary: Добавление или удаление избранных предложений - description: Добавляет или удаляет избранное предложение у пользователя. + summary: Добавление избранных предложений + description: Добавляет избранное предложение пользователю. parameters: - in: path name: userId @@ -395,54 +389,57 @@ paths: type: object properties: - isFavorite: - type: boolean - example: true + id: + type: string + example: 6329c3d6a04ab1061c6425ea responses: "204": - description: "Успешное добавление или удаление предложения из избранного. Массив с предложениями." + description: "Успешное добавление в избранное." "400": description: "Неверный формат параметров." "404": description: "Текущего пользователя не существует." - + /users/{userId}/favorites/{offerId}: - post: + delete: tags: - users - summary: Добавление или удаление избранных предложений - description: Добавляет или удаляет избранное предложение у пользователя. + summary: Удаление избранных предложений + description: Удаляет избранное предложение у пользователя. parameters: - in: path name: userId required: true schema: type: string - example: 6329c3d6a04ab1061c6425ea - in: path name: offerId required: true schema: type: string - example: 6329c3d6a04ab1061c6425ea - requestBody: - description: Информация обновления избранных предложений. - content: - application/json: - schema: - type: object - - properties: - isFavorite: - type: boolean - example: true responses: - "204": - description: "Успешное добавление или удаление предложения из избранного. Массив с предложениями." - "400": - description: "Неверный формат параметров." + "200": + description: Избранное предложение удалено. Идентификатор предложения. + content: + application/json: + schema: + type: object + + properties: + id: + type: string + example: 6329c3d6a04ab1061c6425ea + + "401": + description: Пользователь не авторизован для выполнения данного действия. + + "403": + description: Нет доступа для удаления предложения из избранного. + "404": - description: "Текущего пользователя не существует. Текущего предложения не существует." + description: Не существует предложения для удаления. + + components: schemas: