diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3da9031 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + db: + image: mongo:4.2 + restart: always + container_name: six-cities_mongodb + environment: + MONGO_INITDB_ROOT_USERNAME: ${DB_USER} + MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD} + ports: + - ${DB_PORT}:27017 + volumes: + - six-cities_data:/data/db + + db_ui: + image: mongo-express:latest + restart: always + container_name: six-cities_mongo_express + ports: + - 8081:8081 + environment: + ME_CONFIG_BASICAUTH_USERNAME: ${DB_USER} + ME_CONFIG_BASICAUTH_PASSWORD: ${DB_PASSWORD} + ME_CONFIG_MONGODB_ADMINUSERNAME: ${DB_USER} + ME_CONFIG_MONGODB_ADMINPASSWORD: ${DB_PASSWORD} + ME_CONFIG_MONGODB_URL: mongodb://${DB_USER}:${DB_PASSWORD}@db:${DB_PORT}/ + +volumes: + six-cities_data: diff --git a/package-lock.json b/package-lock.json index 2b379f3..d5b81af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "six-cities", "version": "8.0.0", "dependencies": { + "@typegoose/typegoose": "12.8.0", "chalk": "5.3.0", "convict": "6.2.4", "convict-format-with-validator": "6.2.0", @@ -15,6 +16,8 @@ "dotenv": "16.4.5", "got": "14.4.2", "inversify": "6.0.2", + "mkdirp": "3.0.1", + "mongoose": "8.7.0", "pino": "9.4.0", "reflect-metadata": "0.2.2" }, @@ -323,6 +326,14 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -425,6 +436,24 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "node_modules/@typegoose/typegoose": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-12.8.0.tgz", + "integrity": "sha512-YCeYYH0joT4n48WRUfofPq3KBg6OQw1zR6wB4WKflkFYf9SC4P29hf0PlmsiA+hAbubd3Qn51KmkjiUJetJmFQ==", + "dependencies": { + "lodash": "^4.17.20", + "loglevel": "^1.9.2", + "reflect-metadata": "^0.2.2", + "semver": "^7.6.3", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "mongoose": "~8.7.0" + } + }, "node_modules/@types/convict": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.6.tgz", @@ -481,6 +510,19 @@ "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", "dev": true }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz", @@ -1062,6 +1104,14 @@ "node": ">=8" } }, + "node_modules/bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1548,7 +1598,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -4118,6 +4167,14 @@ "node": ">=4.0" } }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4210,8 +4267,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash-id": { "version": "0.14.1", @@ -4233,6 +4289,18 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4272,18 +4340,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -4299,6 +4355,11 @@ "node": ">= 0.6" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -4461,6 +4522,100 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mongodb": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", + "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.0.tgz", + "integrity": "sha512-rUCSF1mMYQXjXYdqEQLLlMD3xbcj2j1/hRn+9VnVj7ipzru/UoUZxlj/hWmteKMAh4EFnDZ+BIrmma9l/0Hi1g==", + "dependencies": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.9.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -4504,11 +4659,29 @@ "node": ">= 0.8" } }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -5142,7 +5315,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, "engines": { "node": ">=6" } @@ -5615,13 +5787,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -5787,6 +5955,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -5816,6 +5989,14 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -6234,6 +6415,17 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -6304,8 +6496,7 @@ "node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "dev": true + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/type-check": { "version": "0.4.0", @@ -6520,6 +6711,26 @@ "node": ">=0.10.48" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6732,12 +6943,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -7021,6 +7226,14 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7102,6 +7315,18 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "@typegoose/typegoose": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-12.8.0.tgz", + "integrity": "sha512-YCeYYH0joT4n48WRUfofPq3KBg6OQw1zR6wB4WKflkFYf9SC4P29hf0PlmsiA+hAbubd3Qn51KmkjiUJetJmFQ==", + "requires": { + "lodash": "^4.17.20", + "loglevel": "^1.9.2", + "reflect-metadata": "^0.2.2", + "semver": "^7.6.3", + "tslib": "^2.7.0" + } + }, "@types/convict": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.6.tgz", @@ -7158,6 +7383,19 @@ "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", "dev": true }, + "@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "requires": { + "@types/webidl-conversions": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz", @@ -7541,6 +7779,11 @@ "fill-range": "^7.0.1" } }, + "bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==" + }, "buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -7892,7 +8135,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -9740,6 +9982,11 @@ "object.assign": "^4.1.3" } }, + "kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==" + }, "keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9813,8 +10060,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash-id": { "version": "0.14.1", @@ -9833,6 +10079,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9860,15 +10111,6 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==" }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -9881,6 +10123,11 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -9999,6 +10246,51 @@ "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "dev": true }, + "mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" + }, + "mongodb": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", + "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "requires": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + } + }, + "mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "requires": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "mongoose": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.0.tgz", + "integrity": "sha512-rUCSF1mMYQXjXYdqEQLLlMD3xbcj2j1/hRn+9VnVj7ipzru/UoUZxlj/hWmteKMAh4EFnDZ+BIrmma9l/0Hi1g==", + "requires": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.9.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -10038,11 +10330,23 @@ } } }, + "mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==" + }, + "mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "requires": { + "debug": "4.x" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { "version": "3.3.7", @@ -10507,8 +10811,7 @@ "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" }, "qs": { "version": "6.13.0", @@ -10829,13 +11132,9 @@ "dev": true }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" }, "semver-compare": { "version": "1.0.0", @@ -10972,6 +11271,11 @@ "object-inspect": "^1.13.1" } }, + "sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -10992,6 +11296,14 @@ "atomic-sleep": "^1.0.0" } }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "requires": { + "memory-pager": "^1.0.2" + } + }, "spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -11282,6 +11594,14 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "requires": { + "punycode": "^2.3.0" + } + }, "ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -11325,8 +11645,7 @@ "tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "dev": true + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "type-check": { "version": "0.4.0", @@ -11483,6 +11802,20 @@ "integrity": "sha512-JwPr6erhX53EWH/HCSzfy1tTFrtPXUe927wdM1jqBBeYp1OM+qPHjWbsvv6pIBduqdgxxS+ScfG7S28pzyr2DQ==", "dev": true }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "requires": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11632,12 +11965,6 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index b9ac2c0..3790384 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,16 @@ ], "main": "main.js", "scripts": { - "start": "npm run build && node ./dist/main.cli.js", - "start:dev": "npm run ts ./src/main.rest.ts | pino-pretty --colorize --translateTime SYS:standard", + "start": "npm run build && node ./dist/src/main.cli.js", + "dev": "npm run ts ./src/main.rest.ts | pino-pretty --colorize --translateTime SYS:standard", "build": "npm run clean && npm run compile", "lint": "eslint src/ --ext .ts", + "lint:fix": "eslint src/ --ext .ts --fix", "compile": "tsc -p tsconfig.json", - "clean": "rimraf dist", + "clean": "rimraf dist && mkdirp dist/logs", "ts": "tsc --noEmit && node --no-warnings=ExperimentalWarning --loader ts-node/esm", + "docker:up": "docker compose --env-file ./.env --project-name six-cities up -d", + "docker:down": "docker compose --env-file ./.env --project-name six-cities down -v", "mock:server": "json-server ./src/mocks/mock-server-data.json --port 3123 --host localhost" }, "devDependencies": { @@ -39,6 +42,7 @@ "npm": ">=10" }, "dependencies": { + "@typegoose/typegoose": "12.8.0", "chalk": "5.3.0", "convict": "6.2.4", "convict-format-with-validator": "6.2.0", @@ -46,13 +50,9 @@ "dotenv": "16.4.5", "got": "14.4.2", "inversify": "6.0.2", + "mkdirp": "3.0.1", + "mongoose": "8.7.0", "pino": "9.4.0", "reflect-metadata": "0.2.2" - }, - "imports": { - "#helpers/": "./src/shared/helpers/", - "#libs/*": "./src/shared/libs/*", - "#types/": "./src/shared/types/", - "#shared/*": "./src/shared/*" } } diff --git a/src/cli/commands/command.constant.ts b/src/cli/commands/command.constant.ts new file mode 100644 index 0000000..3503d91 --- /dev/null +++ b/src/cli/commands/command.constant.ts @@ -0,0 +1,2 @@ +export const DEFAULT_DB_PORT = '27017'; +export const DEFAULT_USER_PASSWORD = '123456'; diff --git a/src/cli/commands/generate.command.ts b/src/cli/commands/generate.command.ts index 5b44c97..ed94033 100644 --- a/src/cli/commands/generate.command.ts +++ b/src/cli/commands/generate.command.ts @@ -1,8 +1,8 @@ import got from 'got'; import { Command } from './command.interface.js'; -import { MockServerData } from '#types/index.js'; -import { TSVOfferGenerator } from '#libs/offer-generator/tsv.offer-generator.js'; -import { TSVFileWriter } from '#libs/file-writer/tsv.file-writer.js'; +import { TSVOfferGenerator } from '../../shared/libs/offer-generator/tsv.offer-generator.js'; +import { TSVFileWriter } from '../../shared/libs/file-writer/tsv.file-writer.js'; +import { MockServerData } from '../../shared/types/index.js'; export class GenerateCommand implements Command { private initialData: MockServerData; diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index 65ec63d..ffffae8 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -1,25 +1,89 @@ +import { getMongoURI } from '../../shared/helpers/database.js'; +import { DatabaseClient } from '../../shared/libs/database-client/database-client.interface.js'; +import { MongoDatabaseClient } from '../../shared/libs/database-client/mongo.database-client.js'; +import { TSVFileReader } from '../../shared/libs/file-reader/tsv.file-reader.js'; +import { ConsoleLogger } from '../../shared/libs/logger/console.logger.js'; +import { Logger } from '../../shared/libs/logger/logger.interface.js'; +import { DefaultFacilityService, FacilityModel, FacilityService } from '../../shared/modules/facility/index.js'; +import { DefaultOfferService, OfferModel, OfferService } from '../../shared/modules/offer/index.js'; +import { DefaultUserService, UserModel, UserService } from '../../shared/modules/user/index.js'; +import { Offer } from '../../shared/types/index.js'; +import { DEFAULT_DB_PORT, DEFAULT_USER_PASSWORD } from './command.constant.js'; import { Command } from './command.interface.js'; -import { TSVFileReader } from '#libs/file-reader/tsv.file-reader.js'; -import { Offer } from '#types/index.js'; export class ImportCommand implements Command { - private onOfferImport(offer: Offer): void { - console.info(offer); + private userService: UserService; + private facilityService: FacilityService; + private offerService: OfferService; + private databaseClient: DatabaseClient; + private logger: Logger; + private salt: string; + + constructor() { + this.onImportedOffer = this.onImportedOffer.bind(this); + this.onCompleteImport = this.onCompleteImport.bind(this); + + this.logger = new ConsoleLogger(); + this.offerService = new DefaultOfferService(this.logger, OfferModel); + this.facilityService = new DefaultFacilityService(this.logger, FacilityModel); + this.userService = new DefaultUserService(this.logger, UserModel); + this.databaseClient = new MongoDatabaseClient(this.logger); } - private onCompleteImport(count: number): void { - console.info(`${count} offers imported.`); + private async onImportedOffer(offer: Offer, resolve: () => void) { + await this.saveOffer(offer); + resolve(); + } + + private async saveOffer(offer: Offer) { + const facilities: string[] = []; + const user = await this.userService.findOrCreate({ + ...offer.user, + password: DEFAULT_USER_PASSWORD + }, this.salt); + + for (const { name } of offer.facilities) { + const existFacility = await this.facilityService.findByFacilityNameOrCreate(name, { name }); + facilities.push(existFacility.id); + } + + await this.offerService.create({ + facilities, + userId: user.id, + title: offer.title, + description: offer.description, + preview: offer.preview, + images: offer.images, + postDate: offer.postDate, + price: offer.price, + type: offer.type, + city: offer.city, + isPremium: offer.isPremium, + isFavorite: offer.isFavorite, + rooms: offer.rooms, + guests: offer.guests, + location: offer.location + }); + + } + + private onCompleteImport(count: number) { + console.info(`${count} rows imported.`); + this.databaseClient.disconnect(); } public getName(): string { return '--import'; } - public async execute(...params: string[]): Promise { - const [fileName] = params; - const tsvFileReader = new TSVFileReader(fileName.trim()); + public async execute(fileName: string, login: string, password: string, host: string, dbname: string, salt: string): Promise { + const uri = getMongoURI(login, password, host, DEFAULT_DB_PORT, dbname); + this.salt = salt; + + await this.databaseClient.connect(uri); - tsvFileReader.on('line', this.onOfferImport); + const tsvFileReader = new TSVFileReader(fileName.trim()); + tsvFileReader.on('line', this.onImportedOffer); tsvFileReader.on('end', this.onCompleteImport); try { diff --git a/src/main.cli.ts b/src/main.cli.ts index ae07b2d..07a1cee 100644 --- a/src/main.cli.ts +++ b/src/main.cli.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import 'reflect-metadata'; import { CLIApplication, HelpCommand, VersionCommand, ImportCommand, GenerateCommand } from './cli/index.js'; function bootstrap() { diff --git a/src/main.rest.ts b/src/main.rest.ts index 9477c8e..c7b1af8 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -1,17 +1,20 @@ #!/usr/bin/env node import 'reflect-metadata'; import { Container } from 'inversify'; -import { PinoLogger } from '#libs/logger/pino.logger.js'; +import { createRestApplicationContainer } from './rest/rest.container.js'; +import { Component } from './shared/const.js'; import { RESTApplication } from './rest/rest.application.js'; -import { Config, RestConfig, RestSchema } from '#libs/config/index.js'; -import { Component } from '#shared/const.js'; +import { createFacilityContainer } from './shared/modules/facility/index.js'; +import { createUserContainer } from './shared/modules/user/index.js'; +import { createOfferContainer } from './shared/modules/offer/index.js'; async function bootstrap() { - const container = new Container(); - - container.bind(Component.RestApplication).to(RESTApplication).inSingletonScope(); - container.bind(Component.Logger).to(PinoLogger).inSingletonScope(); - container.bind>(Component.Config).to(RestConfig).inSingletonScope(); + const container = Container.merge( + createRestApplicationContainer(), + createUserContainer(), + createFacilityContainer(), + createOfferContainer(), + ); const application = container.get(Component.RestApplication); await application.init(); diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index f9d61a9..149cc34 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -1,18 +1,36 @@ import { injectable, inject } from 'inversify'; -import { PinoLogger } from '#libs/logger/pino.logger.js'; -import { Config } from '#libs/config/config.interface.js'; -import { RestSchema } from '#libs/config/rest.schema.js'; -import { Component } from '#shared/const.js'; +import { Config } from 'convict'; +import { PinoLogger } from '../shared/libs/logger/pino.logger.js'; +import { RestSchema } from '../shared/libs/config/rest.schema.js'; +import { Component } from '../shared/const.js'; +import { DatabaseClient } from '../shared/libs/database-client/database-client.interface.js'; +import { getMongoURI } from '../shared/helpers/database.js'; @injectable() export class RESTApplication { constructor( @inject(Component.Logger) private readonly logger: PinoLogger, @inject(Component.Config) private readonly config: Config, + @inject(Component.DatabaseClient) private readonly databaseClient: DatabaseClient, ) { } + private async initDb() { + const mongoUri = getMongoURI( + this.config.get('DB_USER'), + this.config.get('DB_PASSWORD'), + this.config.get('DB_HOST'), + this.config.get('DB_PORT'), + this.config.get('DB_NAME'), + ); + + return this.databaseClient.connect(mongoUri); + } + public async init() { this.logger.info('Application initialization'); this.logger.info(`Get value from env $PORT: ${this.config.get('PORT')}`); + this.logger.info('Init database...'); + await this.initDb(); + this.logger.info('Init database completed'); } } diff --git a/src/rest/rest.container.ts b/src/rest/rest.container.ts new file mode 100644 index 0000000..399cf5d --- /dev/null +++ b/src/rest/rest.container.ts @@ -0,0 +1,19 @@ +import { Container } from 'inversify'; +import { Logger } from '../shared/libs/logger/logger.interface.js'; +import { Component } from '../shared/const.js'; +import { PinoLogger } from '../shared/libs/logger/pino.logger.js'; +import { Config, RestConfig, RestSchema } from '../shared/libs/config/index.js'; +import { DatabaseClient } from '../shared/libs/database-client/database-client.interface.js'; +import { MongoDatabaseClient } from '../shared/libs/database-client/mongo.database-client.js'; +import { RESTApplication } from './rest.application.js'; + +export function createRestApplicationContainer() { + const restApplicationContainer = new Container(); + + restApplicationContainer.bind(Component.RestApplication).to(RESTApplication).inSingletonScope(); + restApplicationContainer.bind(Component.Logger).to(PinoLogger).inSingletonScope(); + restApplicationContainer.bind>(Component.Config).to(RestConfig).inSingletonScope(); + restApplicationContainer.bind(Component.DatabaseClient).to(MongoDatabaseClient).inSingletonScope(); + + return restApplicationContainer; +} diff --git a/src/shared/const.ts b/src/shared/const.ts index 0cdce6c..bc28c20 100644 --- a/src/shared/const.ts +++ b/src/shared/const.ts @@ -53,4 +53,11 @@ export const Component = { RestApplication: Symbol.for('RestApplication'), Logger: Symbol.for('Logger'), Config: Symbol.for('Config'), + DatabaseClient: Symbol.for('DatabaseClient'), + UserService: Symbol.for('UserService'), + UserModel: Symbol.for('UserModel'), + FacilityService: Symbol.for('FacilityService'), + FacilityModel: Symbol.for('FacilityModel'), + OfferService: Symbol.for('OfferService'), + OfferModel: Symbol.for('OfferModel'), } as const; diff --git a/src/shared/helpers/database.ts b/src/shared/helpers/database.ts new file mode 100644 index 0000000..e16113f --- /dev/null +++ b/src/shared/helpers/database.ts @@ -0,0 +1,3 @@ +export function getMongoURI(username: string, password: string, host: string, port: string, databaseName: string): string { + return `mongodb://${username}:${password}@${host}:${port}/${databaseName}?authSource=admin`; +} diff --git a/src/shared/helpers/hash.ts b/src/shared/helpers/hash.ts new file mode 100644 index 0000000..856ad58 --- /dev/null +++ b/src/shared/helpers/hash.ts @@ -0,0 +1,6 @@ +import * as crypto from 'node:crypto'; + +export const createSHA256 = (line: string, salt: string): string => { + const shaHasher = crypto.createHmac('sha256', salt); + return shaHasher.update(line).digest('hex'); +}; diff --git a/src/shared/libs/config/rest.config.ts b/src/shared/libs/config/rest.config.ts index f7fa8b6..804a5c8 100644 --- a/src/shared/libs/config/rest.config.ts +++ b/src/shared/libs/config/rest.config.ts @@ -1,9 +1,9 @@ import { config } from 'dotenv'; import { injectable, inject } from 'inversify'; import { Config } from './config.interface.js'; -import { PinoLogger } from '#libs/logger/pino.logger.js'; import { configRestSchema, RestSchema } from './rest.schema.js'; -import { Component } from '#shared/const.js'; +import { Component } from '../../const.js'; +import { PinoLogger } from '../logger/pino.logger.js'; @injectable() export class RestConfig implements Config { diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index a2ff5ce..18a212b 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -7,6 +7,10 @@ export type RestSchema = { PORT: number; SALT: string; DB_HOST: string; + DB_USER: string; + DB_PASSWORD: string; + DB_PORT: string; + DB_NAME: string; } export const configRestSchema = convict({ @@ -28,4 +32,28 @@ export const configRestSchema = convict({ env: 'DB_HOST', default: '127.0.0.1' }, + DB_USER: { + doc: 'Username to connect to the database (MongoDB)', + format: String, + env: 'DB_USER', + default: null, + }, + DB_PASSWORD: { + doc: 'Password to connect to the database (MongoDB)', + format: String, + env: 'DB_PASSWORD', + default: null, + }, + DB_PORT: { + doc: 'Port to connect to the database (MongoDB)', + format: 'port', + env: 'DB_PORT', + default: '27017', + }, + DB_NAME: { + doc: 'Database name (MongoDB)', + format: String, + env: 'DB_NAME', + default: 'six-cities' + }, }); diff --git a/src/shared/libs/database-client/database-client.interface.ts b/src/shared/libs/database-client/database-client.interface.ts new file mode 100644 index 0000000..db39976 --- /dev/null +++ b/src/shared/libs/database-client/database-client.interface.ts @@ -0,0 +1,4 @@ +export interface DatabaseClient { + connect(uri: string): Promise; + disconnect(): Promise; +} diff --git a/src/shared/libs/database-client/mongo.database-client.ts b/src/shared/libs/database-client/mongo.database-client.ts new file mode 100644 index 0000000..789c545 --- /dev/null +++ b/src/shared/libs/database-client/mongo.database-client.ts @@ -0,0 +1,54 @@ +import * as Mongoose from 'mongoose'; +import { inject, injectable } from 'inversify'; +import { setTimeout } from 'node:timers/promises'; +import { DatabaseClient } from './database-client.interface.js'; +import { Logger } from '../logger/logger.interface.js'; +import { Component } from '../../const.js'; + +@injectable() +export class MongoDatabaseClient implements DatabaseClient { + private mongoose: typeof Mongoose; + private isConnected: boolean = false; + + constructor( + @inject(Component.Logger) private readonly logger: Logger + ) { } + + public isConnectedToDatabase() { + return this.isConnected; + } + + public async connect(uri: string): Promise { + if (this.isConnectedToDatabase()) { + throw new Error('MongoDB client already connected'); + } + + this.logger.info('Trying to connect to MongoDB...'); + + let attempt = 0; + while (attempt < 5) { + try { + this.mongoose = await Mongoose.connect(uri); + this.isConnected = true; + this.logger.info('Database connection established'); + return; + } catch (error) { + attempt++; + this.logger.error(`Failed to connect to the database. Attempt ${attempt}`, error as Error); + await setTimeout(1000); + } + } + + throw new Error('Unable to establish database connection after 5 attempts'); + } + + public async disconnect(): Promise { + if (!this.isConnectedToDatabase()) { + throw new Error('Not connected to the database'); + } + + await this.mongoose.disconnect?.(); + this.isConnected = false; + this.logger.info('Database connection closed'); + } +} diff --git a/src/shared/libs/file-reader/tsv.file-reader.ts b/src/shared/libs/file-reader/tsv.file-reader.ts index d490e8a..64f5acc 100644 --- a/src/shared/libs/file-reader/tsv.file-reader.ts +++ b/src/shared/libs/file-reader/tsv.file-reader.ts @@ -1,8 +1,8 @@ import EventEmitter from 'node:events'; import { createReadStream } from 'node:fs'; import { FileReader } from './file-reader.interface.js'; -import { Offer, OfferType, User, Location, City } from '#types/index.js'; -import { CITIES, CITIES_LIST } from '#shared/const.js'; +import { City, Location, Offer, OfferType, User } from '../../types/index.js'; +import { CITIES, CITIES_LIST } from '../../const.js'; export class TSVFileReader extends EventEmitter implements FileReader { private CHUNK_SIZE = 16384; @@ -25,6 +25,10 @@ export class TSVFileReader extends EventEmitter implements FileReader { return line.split(separator) ?? []; } + private parseFacilities(line: string): { name: string }[] { + return line.split(';').map((name) => ({ name })); + } + private parseUser(...params: string[]): User { const [name, email, avatar, password, isPro] = params; @@ -82,7 +86,7 @@ export class TSVFileReader extends EventEmitter implements FileReader { rooms: this.parseToNumber(rooms), guests: this.parseToNumber(guests), price: this.parseToNumber(price), - facilities: this.parseToArray(facilities), + facilities: this.parseFacilities(facilities), user: this.parseUser(name, email, avatar, password, isPro), location: this.parseLocation(lat, long), }; @@ -107,7 +111,9 @@ export class TSVFileReader extends EventEmitter implements FileReader { lineCount++; const parsedOffer = this.parseLineToOffer(singleLine); - this.emit('line', parsedOffer); + await new Promise((resolve) => { + this.emit('line', parsedOffer, resolve); + }); } } diff --git a/src/shared/libs/logger/console.logger.ts b/src/shared/libs/logger/console.logger.ts new file mode 100644 index 0000000..0e5c26e --- /dev/null +++ b/src/shared/libs/logger/console.logger.ts @@ -0,0 +1,22 @@ +import { Logger } from './logger.interface.js'; + +export class ConsoleLogger implements Logger { + public debug(message: string, ...args: unknown[]): void { + console.debug(message, ...args); + } + + public error(message: string, error: Error, ...args: unknown[]): void { + console.error(message, ...args); + if (error instanceof Error) { + console.error(`Details: ${error.message}`); + } + } + + public info(message: string, ...args: unknown[]): void { + console.info(message, ...args); + } + + public warn(message: string, ...args: unknown[]): void { + console.warn(message, ...args); + } +} diff --git a/src/shared/libs/logger/logger.interface.ts b/src/shared/libs/logger/logger.interface.ts new file mode 100644 index 0000000..927837e --- /dev/null +++ b/src/shared/libs/logger/logger.interface.ts @@ -0,0 +1,6 @@ +export interface Logger { + info(message: string, ...args: unknown[]): void; + warn(message: string, ...args: unknown[]): void; + error(message: string, error: Error, ...args: unknown[]): void; + debug(message: string, ...args: unknown[]): void; +} diff --git a/src/shared/libs/logger/pino.logger.ts b/src/shared/libs/logger/pino.logger.ts index f3c016b..84c023c 100644 --- a/src/shared/libs/logger/pino.logger.ts +++ b/src/shared/libs/logger/pino.logger.ts @@ -1,11 +1,12 @@ import { resolve } from 'node:path'; -import { Logger, pino, transport } from 'pino'; +import { Logger as PinoInstance, pino, transport } from 'pino'; +import { Logger } from './logger.interface.js'; import { injectable } from 'inversify'; -import { getCurrentModuleDirectoryPath } from '#helpers/file-system.js'; +import { getCurrentModuleDirectoryPath } from '../../helpers/file-system.js'; @injectable() -export class PinoLogger { - private readonly logger: Logger; +export class PinoLogger implements Logger { + private readonly logger: PinoInstance; constructor() { const modulePath = getCurrentModuleDirectoryPath(); @@ -34,7 +35,7 @@ export class PinoLogger { this.logger.debug(message, ...args); } - public error(error: Error, message: string, ...args: unknown[]): void { + public error(message: string, error: Error, ...args: unknown[]): void { this.logger.error(error, message, ...args); } diff --git a/src/shared/libs/offer-generator/tsv.offer-generator.ts b/src/shared/libs/offer-generator/tsv.offer-generator.ts index 38f6026..8663ee5 100644 --- a/src/shared/libs/offer-generator/tsv.offer-generator.ts +++ b/src/shared/libs/offer-generator/tsv.offer-generator.ts @@ -1,8 +1,8 @@ import dayjs from 'dayjs'; import { OfferGenerator } from './offer-generator.interface.js'; -import { MockServerData } from '#types/index.js'; -import { getRandomNumber, getSomeArrayItems } from '#helpers/common.js'; -import { CITIES_LIST } from '#shared/const.js'; +import { MockServerData } from '../../types/index.js'; +import { getRandomNumber, getSomeArrayItems } from '../../helpers/common.js'; +import { CITIES_LIST } from '../../const.js'; const Price = { MIN: 100, @@ -40,7 +40,7 @@ export class TSVOfferGenerator implements OfferGenerator { } private getFacilities(): string { - const number = getRandomNumber(0, this.mockData.facilities.length); + const number = getRandomNumber(0, this.mockData.facilities.length - 1); if (number === 0) { return ''; @@ -73,6 +73,6 @@ export class TSVOfferGenerator implements OfferGenerator { const lat = getRandomNumber(Position.LAT_MIN, Position.LAT_MAX, Position.RANK); const long = getRandomNumber(Position.LONG_MIN, Position.LONG_MAX, Position.RANK); - return [title, description, createdDate, city, preview, images, isPremium, isFavorite, rating, type, rooms, guests, price, facilities, user, email, avatar, '*******', isPro, lat, long].join('\t'); + return [title, description, createdDate, city, preview, images, isPremium, isFavorite, rating, type, rooms, guests, price, facilities, user, email, avatar, '344Djklfjsdk', isPro, lat, long].join('\t'); } } diff --git a/src/shared/modules/facility/default-facility.service.ts b/src/shared/modules/facility/default-facility.service.ts new file mode 100644 index 0000000..ae1520b --- /dev/null +++ b/src/shared/modules/facility/default-facility.service.ts @@ -0,0 +1,39 @@ +import { inject, injectable } from 'inversify'; +import { DocumentType, types } from '@typegoose/typegoose'; +import { Logger } from '../../libs/logger/logger.interface.js'; +import { FacilityService } from './facility-service.interface.js'; +import { FacilityEntity } from './facility.entity.js'; +import { CreateFacilityDto } from './dto/create-facility.dto.js'; +import { Component } from '../../const.js'; + +@injectable() +export class DefaultFacilityService implements FacilityService { + constructor( + @inject(Component.Logger) private readonly logger: Logger, + @inject(Component.FacilityModel) private readonly facilityModel: types.ModelType + ) { } + + public async create(dto: CreateFacilityDto): Promise> { + const result = await this.facilityModel.create(dto); + this.logger.info(`New facility created: ${dto.name}`); + return result; + } + + public async findByFacilityId(facilityId: string): Promise | null> { + return this.facilityModel.findById(facilityId).exec(); + } + + public async findByFacilityName(facilityName: string): Promise | null> { + return this.facilityModel.findOne({ name: facilityName }).exec(); + } + + public async findByFacilityNameOrCreate(facilityName: string, dto: CreateFacilityDto): Promise> { + const existedFacility = await this.findByFacilityName(facilityName); + + if (existedFacility) { + return existedFacility; + } + + return this.create(dto); + } +} diff --git a/src/shared/modules/facility/dto/create-facility.dto.ts b/src/shared/modules/facility/dto/create-facility.dto.ts new file mode 100644 index 0000000..8c6ed42 --- /dev/null +++ b/src/shared/modules/facility/dto/create-facility.dto.ts @@ -0,0 +1,3 @@ +export class CreateFacilityDto { + public name: string; +} diff --git a/src/shared/modules/facility/facility-service.interface.ts b/src/shared/modules/facility/facility-service.interface.ts new file mode 100644 index 0000000..8388583 --- /dev/null +++ b/src/shared/modules/facility/facility-service.interface.ts @@ -0,0 +1,10 @@ +import { DocumentType } from '@typegoose/typegoose'; +import { CreateFacilityDto } from './dto/create-facility.dto.js'; +import { FacilityEntity } from './facility.entity.js'; + +export interface FacilityService { + create(dto: CreateFacilityDto): Promise>; + findByFacilityId(facilityId: string): Promise | null>; + findByFacilityName(facilityName: string): Promise | null>; + findByFacilityNameOrCreate(facilityName: string, dto: CreateFacilityDto): Promise>; +} diff --git a/src/shared/modules/facility/facility.container.ts b/src/shared/modules/facility/facility.container.ts new file mode 100644 index 0000000..bc302ee --- /dev/null +++ b/src/shared/modules/facility/facility.container.ts @@ -0,0 +1,16 @@ +import { Container } from 'inversify'; +import { Component } from '../../const.js'; +import { FacilityService } from './facility-service.interface.js'; +import { types } from '@typegoose/typegoose'; +import { FacilityEntity, FacilityModel } from './facility.entity.js'; +import { DefaultFacilityService } from './default-facility.service.js'; + + +export function createFacilityContainer() { + const facilityContainer = new Container(); + + facilityContainer.bind(Component.FacilityService).to(DefaultFacilityService); + facilityContainer.bind>(Component.FacilityModel).toConstantValue(FacilityModel); + + return facilityContainer; +} diff --git a/src/shared/modules/facility/facility.entity.ts b/src/shared/modules/facility/facility.entity.ts new file mode 100644 index 0000000..7203ed4 --- /dev/null +++ b/src/shared/modules/facility/facility.entity.ts @@ -0,0 +1,18 @@ +import { defaultClasses, getModelForClass, modelOptions, prop } from '@typegoose/typegoose'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface FacilityEntity extends defaultClasses.Base { } + +@modelOptions({ + schemaOptions: { + collection: 'facilities', + timestamps: true, + } +}) +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class FacilityEntity extends defaultClasses.TimeStamps { + @prop({ required: true, trim: true }) + public name: string; +} + +export const FacilityModel = getModelForClass(FacilityEntity); diff --git a/src/shared/modules/facility/index.ts b/src/shared/modules/facility/index.ts new file mode 100644 index 0000000..17dfc82 --- /dev/null +++ b/src/shared/modules/facility/index.ts @@ -0,0 +1,5 @@ +export { CreateFacilityDto } from './dto/create-facility.dto.js'; +export { FacilityService } from './facility-service.interface.js'; +export { FacilityEntity, FacilityModel } from './facility.entity.js'; +export { DefaultFacilityService } from './default-facility.service.js'; +export { createFacilityContainer } from './facility.container.js'; diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts new file mode 100644 index 0000000..4de5780 --- /dev/null +++ b/src/shared/modules/offer/default-offer.service.ts @@ -0,0 +1,26 @@ +import { inject, injectable } from 'inversify'; +import { Component } from '../../const.js'; +import { Logger } from '../../libs/logger/logger.interface.js'; +import { OfferEntity } from './offer.entity.js'; +import { DocumentType, types } from '@typegoose/typegoose'; +import { OfferService } from './offer-service.interface.js'; +import { CreateOfferDto } from './dto/create-offer.dto.js'; + +@injectable() +export class DefaultOfferService implements OfferService { + constructor( + @inject(Component.Logger) private readonly logger: Logger, + @inject(Component.OfferModel) private readonly offerModel: types.ModelType + ) { } + + public async create(dto: CreateOfferDto): Promise> { + const result = await this.offerModel.create(dto); + this.logger.info(`New offer created: ${dto.title}`); + + return result; + } + + public async findById(offerId: string): Promise | null> { + return this.offerModel.findById(offerId).exec(); + } +} diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts new file mode 100644 index 0000000..76bac86 --- /dev/null +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -0,0 +1,19 @@ +import { City, Location, OfferType } from '../../../types/index.js'; + +export class CreateOfferDto { + public title: string; + public description: string; + public postDate: Date; + public city: City; + public preview: string; + public images: string[]; + public isPremium: boolean; + public isFavorite: boolean; + public type: OfferType; + public rooms: number; + public guests: number; + public price: number; + public facilities: string[]; + public userId: string; + public location: Location; +} diff --git a/src/shared/modules/offer/index.ts b/src/shared/modules/offer/index.ts new file mode 100644 index 0000000..86e042a --- /dev/null +++ b/src/shared/modules/offer/index.ts @@ -0,0 +1,5 @@ +export { OfferEntity, OfferModel } from './offer.entity.js'; +export { CreateOfferDto } from './dto/create-offer.dto.js'; +export { OfferService } from './offer-service.interface.js'; +export { DefaultOfferService } from './default-offer.service.js'; +export { createOfferContainer } from './offer.container.js'; diff --git a/src/shared/modules/offer/offer-service.interface.ts b/src/shared/modules/offer/offer-service.interface.ts new file mode 100644 index 0000000..2109d34 --- /dev/null +++ b/src/shared/modules/offer/offer-service.interface.ts @@ -0,0 +1,8 @@ +import { DocumentType } from '@typegoose/typegoose'; +import { CreateOfferDto } from './dto/create-offer.dto.js'; +import { OfferEntity } from './offer.entity.js'; + +export interface OfferService { + create(dto: CreateOfferDto): Promise>; + findById(offerId: string): Promise | null>; +} diff --git a/src/shared/modules/offer/offer.container.ts b/src/shared/modules/offer/offer.container.ts new file mode 100644 index 0000000..1225589 --- /dev/null +++ b/src/shared/modules/offer/offer.container.ts @@ -0,0 +1,15 @@ +import { Container } from 'inversify'; +import { OfferService } from './offer-service.interface.js'; +import { Component } from '../../const.js'; +import { DefaultOfferService } from './default-offer.service.js'; +import { OfferEntity, OfferModel } from './offer.entity.js'; +import { types } from '@typegoose/typegoose'; + +export function createOfferContainer() { + const offerContainer = new Container(); + + offerContainer.bind(Component.OfferService).to(DefaultOfferService); + offerContainer.bind>(Component.OfferModel).toConstantValue(OfferModel); + + return offerContainer; +} diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts new file mode 100644 index 0000000..3f1fbaf --- /dev/null +++ b/src/shared/modules/offer/offer.entity.ts @@ -0,0 +1,81 @@ +import { defaultClasses, getModelForClass, modelOptions, prop, Ref, Severity } from '@typegoose/typegoose'; +import { City, Location, OfferType } from '../../types/index.js'; +import { FacilityEntity } from '../facility/index.js'; +import { UserEntity } from '../user/index.js'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface OfferEntity extends defaultClasses.Base { } + +@modelOptions({ + schemaOptions: { + collection: 'offers', + timestamps: true, + }, + options: { + allowMixed: Severity.ALLOW, + }, +}) + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class OfferEntity extends defaultClasses.TimeStamps { + @prop({ trim: true, required: true }) + public title: string; + + @prop({ trim: true, required: true }) + public description: string; + + @prop({ required: true }) + public postDate: Date; + + @prop({ required: true }) + public city: City; + + @prop({ required: true }) + public preview: string; + + @prop({ required: true }) + public images: string[]; + + @prop({ required: true }) + public isPremium: boolean; + + @prop({ required: true }) + public isFavorite: boolean; + + @prop({ required: false, default: 0 }) + public rating: number; + + @prop({ required: true }) + public type: OfferType; + + @prop({ required: true }) + public rooms: number; + + @prop({ required: true }) + public guests: number; + + @prop({ required: true }) + public price: number; + + @prop({ + ref: FacilityEntity, + required: true, + default: [], + _id: false + }) + public facilities: Ref[]; + + @prop({ + ref: UserEntity, + required: true + }) + public userId: Ref; + + @prop({ required: false, default: 0 }) + public commentCount: number; + + @prop({ required: true }) + public location: Location; +} + +export const OfferModel = getModelForClass(OfferEntity); diff --git a/src/shared/modules/user/default-user.service.ts b/src/shared/modules/user/default-user.service.ts new file mode 100644 index 0000000..19da05f --- /dev/null +++ b/src/shared/modules/user/default-user.service.ts @@ -0,0 +1,39 @@ +import { DocumentType, types } from '@typegoose/typegoose'; +import { UserService } from './user-service.interface.js'; +import { UserEntity } from './user.entity.js'; +import { CreateUserDto } from './dto/create-user.dto.js'; +import { inject, injectable } from 'inversify'; +import { Component } from '../../const.js'; +import { Logger } from '../../libs/logger/logger.interface.js'; + +@injectable() +export class DefaultUserService implements UserService { + constructor( + @inject(Component.Logger) private readonly logger: Logger, + @inject(Component.UserModel) private readonly userModel: types.ModelType + ) { } + + public async create(dto: CreateUserDto, salt: string): Promise> { + const user = new UserEntity(dto); + user.setPassword(dto.password, salt); + + const result = await this.userModel.create(user); + this.logger.info(`New user created: ${user.email}`); + + return result; + } + + public async findByEmail(email: string): Promise | null> { + return this.userModel.findOne({ email }).exec(); + } + + public async findOrCreate(dto: CreateUserDto, salt: string): Promise> { + const existedUser = await this.findByEmail(dto.email); + + if (existedUser) { + return existedUser; + } + + return this.create(dto, salt); + } +} diff --git a/src/shared/modules/user/dto/create-user.dto.ts b/src/shared/modules/user/dto/create-user.dto.ts new file mode 100644 index 0000000..79a0f2c --- /dev/null +++ b/src/shared/modules/user/dto/create-user.dto.ts @@ -0,0 +1,7 @@ +export class CreateUserDto { + public name: string; + public email: string; + public avatar: string; + public isPro: boolean; + public password: string; +} diff --git a/src/shared/modules/user/index.ts b/src/shared/modules/user/index.ts new file mode 100644 index 0000000..cad1901 --- /dev/null +++ b/src/shared/modules/user/index.ts @@ -0,0 +1,5 @@ +export { UserEntity, UserModel } from './user.entity.js'; +export { CreateUserDto } from './dto/create-user.dto.js'; +export { UserService } from './user-service.interface.js'; +export { DefaultUserService } from './default-user.service.js'; +export { createUserContainer } from './user.container.js'; diff --git a/src/shared/modules/user/user-service.interface.ts b/src/shared/modules/user/user-service.interface.ts new file mode 100644 index 0000000..14aff2d --- /dev/null +++ b/src/shared/modules/user/user-service.interface.ts @@ -0,0 +1,9 @@ +import { DocumentType } from '@typegoose/typegoose'; +import { UserEntity } from './user.entity.js'; +import { CreateUserDto } from './dto/create-user.dto.js'; + +export interface UserService { + create(dto: CreateUserDto, salt: string): Promise>; + findByEmail(email: string): Promise | null>; + findOrCreate(dto: CreateUserDto, salt: string): Promise>; +} diff --git a/src/shared/modules/user/user.container.ts b/src/shared/modules/user/user.container.ts new file mode 100644 index 0000000..67a9f5b --- /dev/null +++ b/src/shared/modules/user/user.container.ts @@ -0,0 +1,15 @@ +import { Container } from 'inversify'; +import { types } from '@typegoose/typegoose'; +import { UserService } from './user-service.interface.js'; +import { UserEntity, UserModel } from './user.entity.js'; +import { DefaultUserService } from './default-user.service.js'; +import { Component } from '../../const.js'; + +export function createUserContainer() { + const userContainer = new Container(); + + userContainer.bind(Component.UserService).to(DefaultUserService).inSingletonScope(); + userContainer.bind>(Component.UserModel).toConstantValue(UserModel); + + return userContainer; +} diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts new file mode 100644 index 0000000..91161e7 --- /dev/null +++ b/src/shared/modules/user/user.entity.ts @@ -0,0 +1,50 @@ +import { defaultClasses, getModelForClass, modelOptions, prop } from '@typegoose/typegoose'; +import { User } from '../../types/index.js'; +import { createSHA256 } from '../../helpers/hash.js'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface UserEntity extends defaultClasses.Base { } + +@modelOptions({ + schemaOptions: { + collection: 'users', + timestamps: true, + } +}) + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class UserEntity extends defaultClasses.TimeStamps { + @prop({ required: true }) + public name: string; + + @prop({ unique: true, required: true }) + public email: string; + + @prop({ required: false, default: '' }) + public avatar: string; + + @prop({ required: true }) + public isPro: boolean; + + @prop({ required: true }) + private password?: string; + + constructor(userData: User) { + super(); + + this.name = userData.name; + this.email = userData.email; + this.avatar = userData.avatar; + this.isPro = userData.isPro; + } + + public setPassword(password: string, salt: string) { + this.password = createSHA256(password, salt); + } + + public getPassword() { + return this.password; + } +} + +export const UserModel = getModelForClass(UserEntity); diff --git a/src/shared/types/facility.type.ts b/src/shared/types/facility.type.ts new file mode 100644 index 0000000..5309492 --- /dev/null +++ b/src/shared/types/facility.type.ts @@ -0,0 +1,3 @@ +export type Facility = { + name: string; +} diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 3c950da..0254c65 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -4,3 +4,4 @@ export { OfferType } from './offer-type.type.js'; export { User } from './user.type.js'; export { Offer } from './offer.type.js'; export { MockServerData } from './mock-server-data.type.js'; +export { Facility } from './facility.type.js'; diff --git a/src/shared/types/offer.type.ts b/src/shared/types/offer.type.ts index e1d3779..e0ea9c9 100644 --- a/src/shared/types/offer.type.ts +++ b/src/shared/types/offer.type.ts @@ -1,8 +1,8 @@ import { City } from './city.type.js'; import { OfferType } from './offer-type.type.js'; -import { FACILITIES } from '../const.js'; import { User } from './user.type.js'; import { Location } from './location.type.js'; +import { Facility } from './facility.type.js'; export type Offer = { title: string; @@ -18,7 +18,7 @@ export type Offer = { rooms: number; guests: number; price: number; - facilities: typeof FACILITIES; + facilities: Facility[]; user: User; location: Location; } diff --git a/tsconfig.json b/tsconfig.json index 7ef381c..d856768 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,7 @@ "strictPropertyInitialization": false, "sourceMap": true, "outDir": "./dist", - "rootDir": "./src", + "rootDir": ".", "emitDecoratorMetadata": true, "types": [ "node",