diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..e54d65b --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,28 @@ +services: + db: + image: mongo:4.2 + restart: always + container_name: six-cities_mongodb + environment: + MONGO_INITDB_ROOT_USERNAME: ${DB_USERNAME} + 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_USERNAME} + ME_CONFIG_BASICAUTH_PASSWORD: ${DB_PASSWORD} + ME_CONFIG_MONGODB_ADMINUSERNAME: ${DB_USERNAME} + ME_CONFIG_MONGODB_ADMINPASSWORD: ${DB_PASSWORD} + ME_CONFIG_MONGODB_URL: mongodb://${DB_USERNAME}:${DB_PASSWORD}@db:${DB_PORT}/ + +volumes: + six-cities_data: diff --git a/package-lock.json b/package-lock.json index e4c2294..6379f29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "six-cities", "version": "7.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,7 @@ "dotenv": "16.4.5", "got": "14.4.2", "inversify": "6.0.2", + "mongoose": "8.7.2", "pino": "9.5.0", "reflect-metadata": "0.2.2" }, @@ -323,6 +325,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", @@ -663,6 +673,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", @@ -713,6 +741,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", @@ -1136,6 +1177,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", @@ -1372,7 +1421,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" }, @@ -3150,6 +3198,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", @@ -3195,8 +3251,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.clonedeep": { "version": "4.5.0", @@ -3209,6 +3264,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", @@ -3247,24 +3314,17 @@ "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", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "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/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3361,6 +3421,105 @@ "node": ">=16 || 14 >=14.17" } }, + "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.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.2.tgz", + "integrity": "sha512-Ok4VzMds9p5G3ZSUhmvBm1GdxanbzhS29jpSn02SPj+IXEVFnIdfwAlHHXWkyNscZKlcn8GuMi68FH++jo0flg==", + "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/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/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -3373,8 +3532,7 @@ "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/natural-compare": { "version": "1.4.0", @@ -3835,7 +3993,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" } @@ -4248,13 +4405,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" }, @@ -4297,6 +4450,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", @@ -4355,6 +4513,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -4653,6 +4819,17 @@ "node": ">=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", @@ -4711,8 +4888,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", @@ -4819,6 +4995,26 @@ "node": ">= 0.10" } }, + "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", @@ -4970,12 +5166,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "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-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", @@ -5220,6 +5410,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", @@ -5466,6 +5664,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", @@ -5516,6 +5726,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", @@ -5779,6 +6002,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", @@ -5943,7 +6171,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" } @@ -7213,6 +7440,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", @@ -7249,8 +7481,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.clonedeep": { "version": "4.5.0", @@ -7263,6 +7494,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", @@ -7286,21 +7522,17 @@ "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", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "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==" + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7361,6 +7593,59 @@ "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "dev": true }, + "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.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.2.tgz", + "integrity": "sha512-Ok4VzMds9p5G3ZSUhmvBm1GdxanbzhS29jpSn02SPj+IXEVFnIdfwAlHHXWkyNscZKlcn8GuMi68FH++jo0flg==", + "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==" + } + } + }, + "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" + } + }, "mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -7370,8 +7655,7 @@ "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==" }, "natural-compare": { "version": "1.4.0", @@ -7715,8 +7999,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==" }, "queue-microtask": { "version": "1.2.3", @@ -7983,13 +8266,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==" }, "shebang-command": { "version": "2.0.0", @@ -8017,6 +8296,11 @@ "object-inspect": "^1.9.0" } }, + "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", @@ -8057,6 +8341,14 @@ "dot-prop": "^9.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", @@ -8282,6 +8574,14 @@ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "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", @@ -8313,8 +8613,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", @@ -8396,6 +8695,20 @@ "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==" }, + "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", @@ -8502,12 +8815,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "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-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", diff --git a/package.json b/package.json index b5d827f..ea3a67e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "npm": ">=10" }, "dependencies": { + "@typegoose/typegoose": "12.8.0", "chalk": "5.3.0", "convict": "6.2.4", "convict-format-with-validator": "6.2.0", @@ -45,6 +46,7 @@ "dotenv": "16.4.5", "got": "14.4.2", "inversify": "6.0.2", + "mongoose": "8.7.2", "pino": "9.5.0", "reflect-metadata": "0.2.2" } diff --git a/src/cli/commands/command.constant.ts b/src/cli/commands/command.constant.ts new file mode 100644 index 0000000..2f2f682 --- /dev/null +++ b/src/cli/commands/command.constant.ts @@ -0,0 +1,2 @@ +export const DEFAULT_DB_PORT = '27017'; +export const DEFAULT_USER_PASSWORD = '123qwe'; diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index 109c389..7d6ef6e 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -1,22 +1,81 @@ +import { getMongoURI } from '../../shared/helpers/datadase.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 { DefaultOfferService } from '../../shared/modules/offer/default-offer.service.js'; +import { OfferService } from '../../shared/modules/offer/offer-servise.interface.js'; +import { OfferModel } from '../../shared/modules/offer/offer.entity.js'; +import { DefaultUserService } from '../../shared/modules/user/default-user.service.js'; +import { UserService } from '../../shared/modules/user/user-service.interface.js'; +import { UserModel } from '../../shared/modules/user/user.entity.js'; import { Offer } from '../../shared/types/offer-type.js'; +import { DEFAULT_DB_PORT, DEFAULT_USER_PASSWORD } from './command.constant.js'; import { Command } from './command.interface.js'; export class ImportCommand implements Command { + private userService: UserService; + 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.userService = new DefaultUserService(this.logger, UserModel); + this.databaseClient = new MongoDatabaseClient(this.logger); + } + public getName(): string { return '--import'; } - private onImportedOffer(offer: Offer): void { - console.info(offer); + private async onImportedOffer(offer: Offer, resolve: () => void) { + await this.saveOffer(offer); + resolve(); + } + + private async saveOffer(offer: Offer) { + const user = await this.userService.findOrCreate({ + ...offer.host, + password: DEFAULT_USER_PASSWORD + }, this.salt); + + await this.offerService.create({ + title: offer.title, + description: offer.description, + postDate: offer.postDate, + city: offer.city, + previewImage: offer.previewImage, + images: offer.images, + isFavorite: offer.isFavorite, + isPremium: offer.isPremium, + rating: offer.rating, + type: offer.type, + bedrooms: offer.bedrooms, + maxAdults: offer.maxAdults, + price:offer.price, + goods: offer.goods, + host: user + }); } private onCompleteImport(count: number) { console.info(`${count} rows imported`); + this.databaseClient.disconnect(); } - public async execute(...parameters: string[]): Promise { - const [filename] = parameters; + 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); + const fileReader = new TSVFileReader(filename.trim()); fileReader.on('line', this.onImportedOffer); diff --git a/src/main.cli.ts b/src/main.cli.ts index 62f7e88..2a31dee 100644 --- a/src/main.cli.ts +++ b/src/main.cli.ts @@ -1,3 +1,4 @@ +import 'reflect-metadata'; import { CLIApplication, HelpCommand, VersionCommand, ImportCommand, GenerateCommand} from './cli/index.js'; const bootstrap = () => { diff --git a/src/main.rest.ts b/src/main.rest.ts index 0318f50..16258c2 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -1,18 +1,19 @@ import 'reflect-metadata'; -import { RestApplication } from './rest/index.js'; -import { Config, RestConfig, RestSchema } from './shared/libs/config/index.js'; -import { Logger, PinoLogger} from './shared/libs/logger/index.js'; +import { RestApplication, createReasApplicationContainer} from './rest/index.js'; import { Container } from 'inversify'; import { Component } from './shared/types/index.js'; +import { createUserContainer } from './shared/modules/user/user.container.js'; +import { createOfferContainer } from './shared/modules/offer/offer.container.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(); +async function bootstrap() { + const appContainer = Container.merge( + createReasApplicationContainer(), + createUserContainer(), + createOfferContainer() + ); - const application = container.get(Component.RestApplication); + const application = appContainer.get(Component.RestApplication); await application.init(); } diff --git a/src/rest/index.ts b/src/rest/index.ts index 5e51a1d..830eb09 100644 --- a/src/rest/index.ts +++ b/src/rest/index.ts @@ -1 +1,2 @@ export { RestApplication } from './rest.application.js'; +export { createReasApplicationContainer } from './rest.container.js'; diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 860c4cd..3b1980d 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -2,17 +2,37 @@ import { inject, injectable } from 'inversify'; import { Config, RestSchema } from '../shared/libs/config/index.js'; import { Logger } from '../shared/libs/logger/index.js'; import { Component } from '../shared/types/index.js'; +import { DatabaseClient } from '../shared/libs/database-client/database-client.interface.js'; +import { getMongoURI } from '../shared/helpers/datadase.js'; @injectable() export class RestApplication { constructor( @inject(Component.Logger) private readonly logger: Logger, - @inject(Component.Config) private readonly config: Config + @inject(Component.Config) private readonly config: Config, + @inject(Component.DatabaseClient) private readonly databaseClient: DatabaseClient ) {} + private async initDb() { + + const mongoUri = getMongoURI( + this.config.get('DB_USERNAME'), + 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 initiazation'); 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..e63088d --- /dev/null +++ b/src/rest/rest.container.ts @@ -0,0 +1,18 @@ +import { RestApplication } from './index.js'; +import { Config, RestConfig, RestSchema } from './../shared/libs/config/index.js'; +import { Logger, PinoLogger} from './../shared/libs/logger/index.js'; +import { Container } from 'inversify'; +import { Component } from './../shared/types/index.js'; +import { DatabaseClient, MongoDatabaseClient } from './../shared/libs/database-client/index.js'; + + +export function createReasApplicationContainer() { + 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(); + container.bind(Component.DatabaseClient).to(MongoDatabaseClient).inSingletonScope(); + + return container; +} diff --git a/src/shared/helpers/const.ts b/src/shared/helpers/const.ts index ba19fce..76244bf 100644 --- a/src/shared/helpers/const.ts +++ b/src/shared/helpers/const.ts @@ -13,3 +13,7 @@ export enum Price { Mix = 3500 } +export enum Retry { + Count = 5, + Timeout = 1000 +} diff --git a/src/shared/helpers/datadase.ts b/src/shared/helpers/datadase.ts new file mode 100644 index 0000000..89d4d4d --- /dev/null +++ b/src/shared/helpers/datadase.ts @@ -0,0 +1,9 @@ +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..cff435a --- /dev/null +++ b/src/shared/helpers/hash.ts @@ -0,0 +1,6 @@ +import * as crypto from 'node:crypto'; + +export function createSHA256(line: string, salt: string): string { + const shaHasher = crypto.createHmac('sha256', salt); + return shaHasher.update(line).digest('hex'); +} diff --git a/src/shared/helpers/index.ts b/src/shared/helpers/index.ts index 2f8aee9..0095891 100644 --- a/src/shared/helpers/index.ts +++ b/src/shared/helpers/index.ts @@ -1 +1,4 @@ export { getRandomElement, getRandomNumber, getRandomItems } from './functions.js'; +export { getMongoURI } from './datadase.js'; +export { createSHA256 } from './hash.js'; +export { WeekDay, Attributes, Price, Retry} from './const.js'; diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index 64bb625..e8351c1 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -6,7 +6,11 @@ convict.addFormats(validator); export type RestSchema = { PORT: number, SALT: string, - HOST_DB: string, + DB_HOST: string, + DB_NAME: string, + DB_USERNAME: string, + DB_PASSWORD: string, + DB_PORT: string } export const configRestSchema = convict({ @@ -22,10 +26,34 @@ export const configRestSchema = convict({ env: 'SALT', default: null }, - HOST_DB: { + DB_HOST: { doc: 'IP address of the database server (MongoDB)', format: 'ipaddress', env: 'HOST_DB', default: '127.0.0.1' - } + }, + DB_NAME: { + doc: 'Database name (MongoDB)', + format: String, + env: 'DB_NAME', + default: 'six-cities' + }, + DB_USERNAME: { + doc: 'Username to connect to the database', + format: String, + env: 'DB_USERNAME', + default: null + }, + DB_PASSWORD: { + doc: 'Password to connect to the database', + format: String, + env: 'DB_PASSWORD', + default: null + }, + DB_PORT: { + doc: 'Port to connect to the database (MongoDB)', + format: 'port', + env: 'DB_PORT', + default: '27017' + }, }); 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/index.ts b/src/shared/libs/database-client/index.ts new file mode 100644 index 0000000..8961709 --- /dev/null +++ b/src/shared/libs/database-client/index.ts @@ -0,0 +1,2 @@ +export { DatabaseClient } from './database-client.interface.js'; +export { MongoDatabaseClient } from './mongo.database-client.js'; 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..afc1b45 --- /dev/null +++ b/src/shared/libs/database-client/mongo.database-client.ts @@ -0,0 +1,59 @@ +import * as Mongoose from 'mongoose'; + +import { inject, injectable } from 'inversify'; +import { DatabaseClient } from './index.js'; +import { Component } from '../../types/index.js'; +import { Logger } from '../logger/index.js'; +import { Retry } from '../../helpers/const.js'; +import { setTimeout } from 'node:timers/promises'; + +@injectable() +export class MongoDatabaseClient implements DatabaseClient { + + private isConneted: boolean; + private mongoose: typeof Mongoose; + + constructor( + @inject(Component.Logger) private readonly logger: Logger + ) { + this.isConneted = false; + } + + public isConnectedToDatabase() { + return this.isConneted; + } + + 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 < Retry.Count) { + try { + this.mongoose = await Mongoose.connect(uri); + this.isConneted = 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(Retry.Timeout); + } + } + } + + public async disconnect(): Promise { + if (!this.isConnectedToDatabase()) { + throw new Error('Not connected to the database'); + } + + await this.mongoose.disconnect?.(); + this.isConneted = 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 87c0d1c..fe6e1f1 100644 --- a/src/shared/libs/file-reader/tsv-file-reader.ts +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -96,7 +96,11 @@ export class TSVFileReader extends EventEmitter implements FileReader { importedRowCouint++; const parsedOffer = this.parseLineToOffer(completeRow); - this.emit('line', parsedOffer); + + await new Promise((resolve) => { + this.emit('line', parsedOffer, resolve); + }); + } } diff --git a/src/shared/libs/file-writer/index.ts b/src/shared/libs/file-writer/index.ts index 22c2b5c..4e3dcfa 100644 --- a/src/shared/libs/file-writer/index.ts +++ b/src/shared/libs/file-writer/index.ts @@ -1 +1,2 @@ export { FileWriter } from './file-writer.interface.js'; +export { TSVFileWriter } from './tsv-file-wtiter.js'; diff --git a/src/shared/libs/logger/console.logger.ts b/src/shared/libs/logger/console.logger.ts new file mode 100644 index 0000000..e129ba9 --- /dev/null +++ b/src/shared/libs/logger/console.logger.ts @@ -0,0 +1,20 @@ +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); + console.error(`Error message: ${error}`); + } + + 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/index.ts b/src/shared/libs/logger/index.ts index 2382413..35fcf4f 100644 --- a/src/shared/libs/logger/index.ts +++ b/src/shared/libs/logger/index.ts @@ -1,2 +1,3 @@ export { Logger} from './logger.interface.js'; export { PinoLogger } from './pino.logger.js'; +export { ConsoleLogger } from './console.logger.js'; diff --git a/src/shared/libs/offer-generator/index.ts b/src/shared/libs/offer-generator/index.ts index dc94ca3..782e85a 100644 --- a/src/shared/libs/offer-generator/index.ts +++ b/src/shared/libs/offer-generator/index.ts @@ -1 +1,2 @@ export { OfferGenerator } from './offer-generator.interface.js'; +export { TSVOfferGenerator } from './tsv-offer-generate.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..5ba3f48 --- /dev/null +++ b/src/shared/modules/offer/default-offer.service.ts @@ -0,0 +1,28 @@ +import { inject, injectable } from 'inversify'; +import { OfferService } from './offer-servise.interface.js'; +import { Component } from '../../types/component.enum.js'; +import { types } from '@typegoose/typegoose'; +import { OfferEntity } from './offer.entity.js'; +import { Logger } from '../../libs/logger/logger.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.OfferModul) private readonly offerModul: types.ModelType + ) {} + + + public async create(dto: CreateOfferDto): Promise> { + const offer = await this.offerModul.create(dto); + this.logger.info(`New offer created: ${dto.title}`); + + return offer; + } + + public async findById(offerId: string): Promise | null> { + return this.offerModul.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..710bf43 --- /dev/null +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -0,0 +1,19 @@ +import { Goods, Images, User } from '../../../types/index.js'; + +export class CreateOfferDto { + public title: string; + public description: string; + public postDate: Date; + public city: string; + public previewImage: string; + public images: Images[]; + public isFavorite: boolean; + public isPremium: boolean; + public rating: number; + public type: string; + public bedrooms: number; + public maxAdults: number; + public price: number; + public goods: Goods[]; + public host: User; +} diff --git a/src/shared/modules/offer/offer-servise.interface.ts b/src/shared/modules/offer/offer-servise.interface.ts new file mode 100644 index 0000000..2109d34 --- /dev/null +++ b/src/shared/modules/offer/offer-servise.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..d6c2b59 --- /dev/null +++ b/src/shared/modules/offer/offer.container.ts @@ -0,0 +1,16 @@ +import { Container } from 'inversify'; +import { OfferService } from './offer-servise.interface.js'; +import { Component } from '../../types/component.enum.js'; +import { DefaultOfferService } from './default-offer.service.js'; +import { types } from '@typegoose/typegoose'; +import { OfferEntity, OfferModel } from './offer.entity.js'; + + +export function createOfferContainer() { + const container = new Container(); + + container.bind(Component.OfferService).to(DefaultOfferService).inSingletonScope(); + container.bind>(Component.OfferModul).toConstantValue(OfferModel); + + return container; +} diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts new file mode 100644 index 0000000..b8a7040 --- /dev/null +++ b/src/shared/modules/offer/offer.entity.ts @@ -0,0 +1,67 @@ +import { defaultClasses, getModelForClass, modelOptions, prop, Ref } from '@typegoose/typegoose'; +import { Goods, Images } from '../../types/offer-type.js'; +import { UserEntity } from '../user/user.entity.js'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface OfferEntity extends defaultClasses.Base {} + +@modelOptions({ + schemaOptions: { + collection: 'Offer', + timestamps: true, + } +}) + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class OfferEntity extends defaultClasses.TimeStamps { + @prop({required: true, minlength: 10, maxlength: 100 }) + public title: string; + + @prop({required: true, minlength: 20, maxlength: 1024 }) + public description: string; + + @prop({required: true}) + public postDate: Date; + + @prop({required: true}) + public city: string; + + @prop({required: true}) + public previewImage: string; + + @prop({required: true}) + public images: Images; + + @prop({required: true}) + public isFavorite: boolean; + + @prop({required: true}) + public isPremium: boolean; + + @prop({required: true}) + public rating: number; + + @prop({required: true}) + public type: string; + + @prop({required: true}) + public bedrooms: number; + + @prop({required: true}) + public maxAdults: number; + + @prop({required: true}) + public price: number; + + @prop({required: true}) + public goods: Goods; + + @prop({ + ref: UserEntity, + required: true + }) + public host: Ref; + +} + +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..4e2dd64 --- /dev/null +++ b/src/shared/modules/user/default-user.service.ts @@ -0,0 +1,42 @@ +import { DocumentType, types } from '@typegoose/typegoose'; +import { CreateUserDto } from './dto/create-user.dto.js'; +import { UserService } from './user-service.interface.js'; +import { UserEntity } from './user.entity.js'; +import { inject, injectable } from 'inversify'; +import { Component } from '../../types/component.enum.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.UserModul) 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}); + } + + 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..8f94a62 --- /dev/null +++ b/src/shared/modules/user/dto/create-user.dto.ts @@ -0,0 +1,9 @@ +import { TypeUser } from '../../../types/index.js'; + +export class CreateUserDto { + public name: string; + public email: string; + public avatarUser: string; + public password: string; + public typeUser: TypeUser; +} 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..7389492 --- /dev/null +++ b/src/shared/modules/user/user-service.interface.ts @@ -0,0 +1,9 @@ +import { DocumentType } from '@typegoose/typegoose'; +import { CreateUserDto } from './dto/create-user.dto.js'; +import { UserEntity } from './user.entity.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..bf91574 --- /dev/null +++ b/src/shared/modules/user/user.container.ts @@ -0,0 +1,16 @@ +import { Container } from 'inversify'; +import { UserService } from './user-service.interface.js'; +import { Component } from '../../types/component.enum.js'; +import { DefaultUserService } from './default-user.service.js'; +import { types } from '@typegoose/typegoose'; +import { UserEntity, UserModel } from './user.entity.js'; + + +export function createUserContainer() { + const container = new Container(); + + container.bind(Component.UserService).to(DefaultUserService).inSingletonScope(); + container.bind>(Component.UserModul).toConstantValue(UserModel); + + return container; +} diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts new file mode 100644 index 0000000..e2d7706 --- /dev/null +++ b/src/shared/modules/user/user.entity.ts @@ -0,0 +1,53 @@ +import { defaultClasses, getModelForClass, modelOptions, prop } from '@typegoose/typegoose'; +import { TypeUser, 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 implements User { + @prop({required: true}) + public name: string; + + @prop({ unique: true, required: true}) + public email: string; + + @prop({ unique: true, default: 'true'}) + public avatarUser: string; + + @prop({required: true, default: ''}) + public password: string; + + @prop({required: true, type: () => String, + enum: TypeUser + }) + public typeUser: TypeUser; + + constructor(userData: User) { + super(); + + this.name = userData.name; + this.email = userData.email; + this.avatarUser = userData.avatarUser; + this.typeUser = userData.typeUser; + } + + 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/component.enum.ts b/src/shared/types/component.enum.ts index 91157c1..fefc2a8 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -1,5 +1,12 @@ export const Component = { RestApplication: Symbol.for('RestApplication'), Logger: Symbol.for('Logger'), - Config: Symbol.for('Config') + Config: Symbol.for('Config'), + DatabaseClient: Symbol.for('DatabaseClient'), + + UserService: Symbol.for('UserService'), + UserModul: Symbol.for('UserModul'), + + OfferService: Symbol.for('OfferService'), + OfferModul: Symbol.for('OfferModul') } as const; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 1247e3a..62bd6e5 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,4 +1,4 @@ -export { Offer } from './offer-type.js'; +export { Offer, Goods, Images } from './offer-type.js'; export { TypeUser, User } from './user-type.js'; export { MockServerData } from './mock-server-data.js'; export { Component } from './component.enum.js'; diff --git a/src/shared/types/user-type.ts b/src/shared/types/user-type.ts index 22aeee4..b163841 100644 --- a/src/shared/types/user-type.ts +++ b/src/shared/types/user-type.ts @@ -6,7 +6,7 @@ export enum TypeUser { export type User = { name: string; email: string; - avatarUser?: string; + avatarUser: string; password: string; typeUser: TypeUser; }