diff --git a/Workflow.md b/Workflow.md index e0c76aa..173e183 100644 --- a/Workflow.md +++ b/Workflow.md @@ -100,6 +100,21 @@ npm run mock:server npm run ts ./src/main.cli.ts -- --generate 100 ./mocks/test-data.tsv http://localhost:3123/api ``` +### --import + +```bash +npm run ts ./src/main.cli.ts -- --import ./src/mocks/test-data.tsv admin test localhost six-sities secret +``` + ```bash ./cli.js --generate 10 ./mocks/mock-offers.tsv http://localhost:3123/api ``` + +## Запуск docker-compose файла для запуска служб баз данных + +```bash +docker compose --file ./docker-compose.dev.yml --env-file ./.env --project-name "six-cities" up -d +``` + + + diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..c9b9d32 --- /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_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: + - 8082: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 fc2fd39..8955931 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "six-cities", "version": "0.0.1", "dependencies": { + "@typegoose/typegoose": "^12.4.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.2.1", "inversify": "^6.0.2", + "mongoose": "8.3.4", "pino": "9.0.0", "reflect-metadata": "^0.2.2" }, @@ -323,6 +325,15 @@ "@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==", + "license": "MIT", + "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", @@ -423,6 +434,25 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "node_modules/@typegoose/typegoose": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-12.4.0.tgz", + "integrity": "sha512-KlTE9zqTFdt8q7m64ETcO7nk8idiY0EtxAR8m8DF9mZIwiQyp/KlJeT5sBdEo+wkzRO12VUmoqZm86qacz5m/Q==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "loglevel": "^1.9.1", + "reflect-metadata": "^0.2.2", + "semver": "^7.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "mongoose": "~8.3.1" + } + }, "node_modules/@types/convict": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.6.tgz", @@ -483,6 +513,21 @@ "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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", @@ -1083,6 +1128,15 @@ "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1613,7 +1667,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" }, @@ -4284,6 +4337,15 @@ "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4381,8 +4443,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", @@ -4406,6 +4467,19 @@ "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==", + "license": "MIT", + "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", @@ -4447,18 +4521,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", @@ -4475,6 +4537,12 @@ "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==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -4649,6 +4717,90 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mongodb": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.5.0.tgz", + "integrity": "sha512-Fozq68InT+JKABGLqctgtb8P56pRrJFkbhW0ux+x1mdHeyinor8oNzJqwLjV/t5X5nJGfTlluxfyMnOXNggIUA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.4.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==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.3.4.tgz", + "integrity": "sha512-ckBaBzKgtWgCalW/LPkcBsR3wKCOYEJ9jLFPmYCYV7TLStpETY757ELx8/1stL11+6HxLLVffawBffXzd0Y7YA==", + "license": "MIT", + "dependencies": { + "bson": "^6.5.0", + "kareem": "2.6.3", + "mongodb": "6.5.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==", + "license": "MIT" + }, "node_modules/morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -4696,11 +4848,31 @@ "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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", @@ -5367,7 +5539,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" } @@ -5859,13 +6030,10 @@ "license": "BSD-3-Clause" }, "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==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -6043,6 +6211,12 @@ "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==", + "license": "MIT" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -6073,6 +6247,15 @@ "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==", + "license": "MIT", + "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", @@ -6508,6 +6691,18 @@ "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==", + "license": "MIT", + "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", @@ -6580,7 +6775,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { @@ -6807,6 +7001,28 @@ "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==", + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "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", @@ -7025,12 +7241,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", @@ -7318,6 +7528,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", @@ -7394,6 +7612,18 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "@typegoose/typegoose": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-12.4.0.tgz", + "integrity": "sha512-KlTE9zqTFdt8q7m64ETcO7nk8idiY0EtxAR8m8DF9mZIwiQyp/KlJeT5sBdEo+wkzRO12VUmoqZm86qacz5m/Q==", + "requires": { + "lodash": "^4.17.20", + "loglevel": "^1.9.1", + "reflect-metadata": "^0.2.2", + "semver": "^7.6.0", + "tslib": "^2.6.2" + } + }, "@types/convict": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.6.tgz", @@ -7450,6 +7680,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", @@ -7833,6 +8076,11 @@ "fill-range": "^7.1.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", @@ -8191,7 +8439,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" } @@ -10023,6 +10270,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", @@ -10096,8 +10348,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", @@ -10116,6 +10367,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", @@ -10143,15 +10399,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", @@ -10164,6 +10411,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", @@ -10282,6 +10534,46 @@ "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "dev": true }, + "mongodb": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.5.0.tgz", + "integrity": "sha512-Fozq68InT+JKABGLqctgtb8P56pRrJFkbhW0ux+x1mdHeyinor8oNzJqwLjV/t5X5nJGfTlluxfyMnOXNggIUA==", + "requires": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.4.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.3.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.3.4.tgz", + "integrity": "sha512-ckBaBzKgtWgCalW/LPkcBsR3wKCOYEJ9jLFPmYCYV7TLStpETY757ELx8/1stL11+6HxLLVffawBffXzd0Y7YA==", + "requires": { + "bson": "^6.5.0", + "kareem": "2.6.3", + "mongodb": "6.5.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", @@ -10321,11 +10613,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", @@ -10790,8 +11094,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", @@ -11112,13 +11415,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", @@ -11255,6 +11554,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", @@ -11275,6 +11579,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", @@ -11565,6 +11877,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", @@ -11608,8 +11928,7 @@ "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "type-check": { "version": "0.4.0", @@ -11766,6 +12085,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", @@ -11915,12 +12248,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 7b976fc..413b858 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "npm": ">=10" }, "dependencies": { + "@typegoose/typegoose": "^12.4.0", "chalk": "^5.3.0", "convict": "6.2.4", "convict-format-with-validator": "6.2.0", @@ -46,6 +47,7 @@ "dotenv": "16.4.5", "got": "^14.2.1", "inversify": "^6.0.2", + "mongoose": "8.3.4", "pino": "9.0.0", "reflect-metadata": "^0.2.2" } diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index 644c4bd..4656856 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -1,40 +1,121 @@ -import chalk from 'chalk'; - import { Command } from './command.interface.js'; import { TSVFileReader } from '../../shared/libs/file-reader/index.js'; import { Offer } from '../../shared/types/offer.interface.js'; -import { getErrorMessage } from '../../shared/helpers/index.js'; +import { getErrorMessage, getMongoURI } from '../../shared/helpers/index.js'; +import { UserModel, UserService } from '../../shared/modules/user/index.js'; +import { DefaultOfferService, OfferModel, OfferService } from '../../shared/modules/offer/index.js'; +import { DatabaseClient, MongoDatabaseClient } from '../../shared/libs/database-client/index.js'; +import { Logger } from '../../shared/libs/logger/index.js'; +import { ConsoleLogger } from '../../shared/libs/logger/console.logger.js'; +import { DefaultUserService } from '../../shared/modules/user/default-user.service.js'; + +const DEFAULT_USER_PASSWORD = '123456'; +// TODO: Доставать из .env переменных +const DEFAULT_DB_PORT = '27017'; export class ImportCommand implements Command { - public getName(): string { - return '--import'; + + 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); } - private onImportedOffer(offer: Offer): void { - console.info(offer); + public getName(): string { + return '--import'; } private onCompleteImport(count: number) { console.info(`${count} rows imported.`); + this.databaseClient.disconnect(); } + private async onImportedOffer(offer: Offer, resolve: () => void) { + await this.saveOffer(offer); + resolve(); + } - public async execute(...parameters: string[]): Promise { - const [filename] = parameters; + private async saveOffer(offer: Offer) { + const user = await this.userService.findOrCreate({ + ...offer.author, // offer.user + email: 'ilkolmakov@yandex.ru', + password: DEFAULT_USER_PASSWORD + }, this.salt); - let fileReader; - if (filename) { - fileReader = new TSVFileReader(filename.trim()); + // for (const { name } of offer.categories) { + // const existCategory = await this.categoryService.findByCategoryNameOrCreate(name, { name }); + // categories.push(existCategory.id); + // } - fileReader.on('line', this.onImportedOffer); - fileReader.on('end', this.onCompleteImport); - } + await this.offerService.create({ + // categories, + title: offer.title, + description: offer.description, + publicationDate: offer.publicationDate, + city: offer.city, + previewImg: offer.previewImg, + images: offer.images, + isPremium: offer.isPremium, + rating: offer.rating, + type: offer.type, + flatCount: offer.flatCount, + guestCount: offer.guestCount, + cost: offer.cost, + conveniences: offer.conveniences, + author: user.id, // userId + commentCount: offer.commentCount, + coordinate: offer.coordinate, + }); + + } + + // TODO: В --help команду нужно добавить аргументы, которые передаем + 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); + fileReader.on('end', this.onCompleteImport); try { - fileReader?.read(); - } catch (err) { - console.error(chalk.red(`Can't import data from file: ${filename}`)); - console.error(getErrorMessage(err)); + fileReader.read(); + } catch (error) { + console.error(`Can't import data from file: ${filename}`); + console.error(getErrorMessage(error)); } } } + + +// public async execute(...parameters: string[]): Promise { +// const [filename] = parameters; + +// let fileReader; +// if (filename) { +// fileReader = new TSVFileReader(filename.trim()); + +// fileReader.on('line', this.onImportedOffer); +// fileReader.on('end', this.onCompleteImport); +// } + +// try { +// fileReader?.read(); +// } catch (err) { +// console.error(chalk.red(`Can't import data from file: ${filename}`)); +// console.error(getErrorMessage(err)); +// } +// } +// } diff --git a/src/main.cli.ts b/src/main.cli.ts index b377e46..0994cdd 100644 --- a/src/main.cli.ts +++ b/src/main.cli.ts @@ -1,4 +1,7 @@ #!/usr/bin/env node + +// import 'reflect-metadata'; + import { CLIApplication, GenerateCommand, HelpCommand, ImportCommand, VersionCommand } from './cli/index.js'; function bootstrap() { diff --git a/src/main.rest.ts b/src/main.rest.ts index 1736458..4366874 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -1,19 +1,21 @@ import 'reflect-metadata'; import { Container } from 'inversify'; -import { Logger, PinoLogger } from './shared/libs/logger/index.js'; +import { createRestApplicationContainer } from './rest/rest.container.js'; +import { createUserContainer } from './shared/modules/user/index.js'; +import { createOfferContainer } from './shared/modules/offer/index.js'; import { RestApplication } from './rest/index.js'; -import { Config, RestConfig, IRestSchema } from './shared/libs/config/index.js'; import { COMPONENT } from './shared/constants/index.js'; // INFO: Точка входа в приложение async function bootstrap() { - const container = new Container(); - container.bind(COMPONENT.REST_APPLICATION).to(RestApplication).inSingletonScope(); - container.bind(COMPONENT.LOGGER).to(PinoLogger).inSingletonScope(); - container.bind>(COMPONENT.CONFIG).to(RestConfig).inSingletonScope(); + const appContainer = Container.merge( + createRestApplicationContainer(), + createUserContainer(), + createOfferContainer(), + ); - const application = container.get(COMPONENT.REST_APPLICATION); + const application = appContainer.get(COMPONENT.REST_APPLICATION); await application.init(); } diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 638f91a..1eb6e59 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -3,16 +3,48 @@ import { inject, injectable } from 'inversify'; import { Logger } from '../shared/libs/logger/index.js'; import { Config, IRestSchema } from '../shared/libs/config/index.js'; import { COMPONENT } from '../shared/constants/index.js'; +import { DatabaseClient } from '../shared/libs/database-client/index.js'; +import { getMongoURI } from '../shared/helpers/index.js'; +import { UserModel } from '../shared/modules/user/index.js'; @injectable() export class RestApplication { constructor( @inject(COMPONENT.LOGGER) private readonly logger: Logger, @inject(COMPONENT.CONFIG) private readonly config: Config, + @inject(COMPONENT.DATABASE_CLIENT) 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'); + + const user = await UserModel.create({ + email: 'ilkolmakov@yandex.ru', + avatarPath: 'keks.jpg', + firstname: 'Keks', + lastname: 'Unknown' + }); + + const error = user.validateSync(); + console.log(error); + + // console.log(user); } } diff --git a/src/rest/rest.container.ts b/src/rest/rest.container.ts new file mode 100644 index 0000000..de59252 --- /dev/null +++ b/src/rest/rest.container.ts @@ -0,0 +1,19 @@ +import { Container } from 'inversify'; + +import { RestApplication } from './rest.application.js'; +// import { Component } from '../shared/types/index.js'; +import { Logger, PinoLogger } from '../shared/libs/logger/index.js'; +import { Config, IRestSchema, RestConfig } from '../shared/libs/config/index.js'; +import { DatabaseClient, MongoDatabaseClient } from '../shared/libs/database-client/index.js'; +import { COMPONENT } from '../shared/constants/index.js'; + +export function createRestApplicationContainer() { + const restApplicationContainer = new Container(); + + restApplicationContainer.bind(COMPONENT.REST_APPLICATION).to(RestApplication).inSingletonScope(); + restApplicationContainer.bind(COMPONENT.LOGGER).to(PinoLogger).inSingletonScope(); + restApplicationContainer.bind>(COMPONENT.CONFIG).to(RestConfig).inSingletonScope(); + restApplicationContainer.bind(COMPONENT.DATABASE_CLIENT).to(MongoDatabaseClient).inSingletonScope(); + + return restApplicationContainer; +} diff --git a/src/shared/constants/component.constant.ts b/src/shared/constants/component.constant.ts index a5844f9..ec68c8f 100644 --- a/src/shared/constants/component.constant.ts +++ b/src/shared/constants/component.constant.ts @@ -2,4 +2,9 @@ export const COMPONENT = { REST_APPLICATION: Symbol.for('kRestApplication'), LOGGER: Symbol.for('kLogger'), CONFIG: Symbol.for('kConfig'), + DATABASE_CLIENT: Symbol.for('kDatabaseClient'), + USER_SERVICE: Symbol.for('kUserService'), + USER_MODEL: Symbol.for('kUserModel'), + OFFER_SERVICE: Symbol.for('kOfferService'), + OFFER_MODEL: Symbol.for('kOfferModel'), } as const; diff --git a/src/shared/helpers/database.ts b/src/shared/helpers/database.ts new file mode 100644 index 0000000..f79b628 --- /dev/null +++ b/src/shared/helpers/database.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..f8ee429 --- /dev/null +++ b/src/shared/helpers/hash.ts @@ -0,0 +1,4 @@ +import { createHmac } from 'node:crypto'; + +export const createSHA256 = (line: string, salt: string): string => + createHmac('sha256', salt).update(line).digest('hex'); diff --git a/src/shared/helpers/index.ts b/src/shared/helpers/index.ts index 6d0639b..43bbf5a 100644 --- a/src/shared/helpers/index.ts +++ b/src/shared/helpers/index.ts @@ -6,3 +6,5 @@ export { } from './common.js'; export { getCurrentModuleDirectoryPath } from './file-system.js'; +export { getMongoURI } from './database.js'; +export { createSHA256 } from './hash.js'; diff --git a/src/shared/libs/config/rest.schema.interface.ts b/src/shared/libs/config/rest.schema.interface.ts index 2835ef5..857aba0 100644 --- a/src/shared/libs/config/rest.schema.interface.ts +++ b/src/shared/libs/config/rest.schema.interface.ts @@ -2,4 +2,8 @@ export interface IRestSchema { PORT: number; SALT: string; DB_HOST: string; + DB_USER: string; + DB_PASSWORD: string; + DB_PORT: string; + DB_NAME: string; } diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index c2dbcec..7882cf7 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -23,4 +23,28 @@ export const configRestSchema = convict({ env: 'DB_HOST', default: null }, + DB_USER: { + doc: 'Username to connect to the database', + format: String, + env: 'DB_USER', + 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: null + }, + DB_NAME: { + doc: 'Database name (MongoDB)', + format: String, + env: 'DB_NAME', + default: null + }, }); 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..cedd15c --- /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..873a3aa --- /dev/null +++ b/src/shared/libs/database-client/mongo.database-client.ts @@ -0,0 +1,65 @@ +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/index.js'; +import { COMPONENT } from '../../constants/component.constant.js'; + + +enum RetryParam { + Count = 5, + Timeout = 1000 +} + +@injectable() +export class MongoDatabaseClient implements DatabaseClient { + private mongoose?: typeof Mongoose; + private isConnected: boolean; + + constructor( + @inject(COMPONENT.LOGGER) private readonly logger: Logger + ) { + this.isConnected = false; + } + + // TODO: public get + public isConnectedToDatabase() { + // TODO: Заменить на параметр от mongoose + return this.isConnected; // this.mongoose?.instance?.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 < RetryParam.Count) { + 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(RetryParam.Timeout); + } + } + + throw new Error(`Unable to establish database connection after ${RetryParam.Count}`); + } + + 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 a7025d1..ae40ed2 100644 --- a/src/shared/libs/file-reader/tsv-file-reader.ts +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -100,7 +100,9 @@ export class TSVFileReader extends EventEmitter implements FileReader { importedRowCount++; const parsedOffer = this.parseLineToOffer(completeRow); - this.emit('line', parsedOffer); + await new Promise((resolve) => { + this.emit('line', parsedOffer, resolve); + }); } } this.emit('end', importedRowCount); diff --git a/src/shared/libs/logger/console.logger.ts b/src/shared/libs/logger/console.logger.ts new file mode 100644 index 0000000..72ce310 --- /dev/null +++ b/src/shared/libs/logger/console.logger.ts @@ -0,0 +1,21 @@ +import { Logger } from './logger.interface.js'; +import { getErrorMessage } from '../../helpers/index.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: ${getErrorMessage(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/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts new file mode 100644 index 0000000..dcebf66 --- /dev/null +++ b/src/shared/modules/offer/default-offer.service.ts @@ -0,0 +1,27 @@ +import { inject, injectable } from 'inversify'; +import { DocumentType, types } from '@typegoose/typegoose'; + +import { OfferService } from './offer-service.interface.js'; +import { Logger } from '../../libs/logger/index.js'; +import { OfferEntity } from './offer.entity.js'; +import { CreateOfferDto } from './dto/create-offer.dto.js'; +import { COMPONENT } from '../../constants/component.constant.js'; + +@injectable() +export class DefaultOfferService implements OfferService { + constructor( + @inject(COMPONENT.LOGGER) private readonly logger: Logger, + @inject(COMPONENT.OFFER_MODEL) 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..cbe811e --- /dev/null +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -0,0 +1,20 @@ +import { City, ConvenienceType, Coordinate, OfferType } from '../../../types/index.js'; + +export class CreateOfferDto { + public title!: string; + public description!: string; + public publicationDate!: Date; + public city!: City; + public previewImg!: string; + public images!: string[]; + public isPremium!: boolean; + public rating!: number; + public type!: OfferType; + public flatCount!: number; + public guestCount!: number; + public cost!: number; + public conveniences!: ConvenienceType[]; + public author!: string; + public commentCount!: number; + public coordinate!: Coordinate; +} 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..04d4d80 --- /dev/null +++ b/src/shared/modules/offer/offer-service.interface.ts @@ -0,0 +1,9 @@ +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..44a98b4 --- /dev/null +++ b/src/shared/modules/offer/offer.container.ts @@ -0,0 +1,17 @@ + +import { Container } from 'inversify'; +import { types } from '@typegoose/typegoose'; + +import { OfferService } from './offer-service.interface.js'; +import { DefaultOfferService } from './default-offer.service.js'; +import { OfferEntity, OfferModel } from './offer.entity.js'; +import { COMPONENT } from '../../constants/index.js'; + +export function createOfferContainer() { + const offerContainer = new Container(); + + offerContainer.bind(COMPONENT.OFFER_SERVICE).to(DefaultOfferService); + offerContainer.bind>(COMPONENT.OFFER_MODEL).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..72ece3d --- /dev/null +++ b/src/shared/modules/offer/offer.entity.ts @@ -0,0 +1,79 @@ +import { + defaultClasses, + getModelForClass, + modelOptions, + prop, + Ref +} from '@typegoose/typegoose'; + +import { City, ConvenienceType, Coordinate, OfferType } from '../../types/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, + } + }) + // 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 publicationDate!: Date; + + @prop({ type: () => String, enum: City, required: true }) + public city!: City; + + @prop({ required: true }) + public previewImg!: string; + + @prop({ type: () => [String], required: true }) + public images!: string[]; + + @prop({ required: true }) + public isPremium!: boolean; + + @prop({ required: true }) + public rating!: number; + + @prop({ + type: () => String, + enum: OfferType, + required: true + }) + public type!: OfferType; + + @prop({ required: true }) + public flatCount!: number; + + @prop({ required: true }) + public guestCount!: number; + + @prop({ required: true }) + public cost!: number; + + @prop({ type: [String], required: true }) + public conveniences!: ConvenienceType[]; + + @prop({ + ref: () => UserEntity, + required: true + }) + public author!: Ref; // userId + + @prop({default: 0}) + public commentCount!: number; + + @prop({ required: true }) + public coordinate!: Coordinate; +} + +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..a964469 --- /dev/null +++ b/src/shared/modules/user/default-user.service.ts @@ -0,0 +1,44 @@ +import { DocumentType, types } from '@typegoose/typegoose'; +import { inject, injectable } from 'inversify'; + +import { UserService } from './user-service.interface.js'; +import { UserEntity } from './user.entity.js'; +import { CreateUserDto } from './dto/create-user.dto.js'; + +import { Logger } from '../../libs/logger/index.js'; +import { COMPONENT } from '../../constants/index.js'; + +@injectable() +export class DefaultUserService implements UserService { + + constructor( + @inject(COMPONENT.LOGGER) private readonly logger: Logger, + @inject(COMPONENT.USER_MODEL) private readonly userModel: types.ModelType, + ) {} + + public async create(dto: CreateUserDto, salt: string): Promise> { + const user = new UserEntity(dto); + // TODO: Не устанавливать пароль в сервисе - должен хэшироваться в конструкторе UserEntity + 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..d23bbab --- /dev/null +++ b/src/shared/modules/user/dto/create-user.dto.ts @@ -0,0 +1,14 @@ +// DTO (Data Transfer Object) — ещё один паттерн проектирования +// Его применяют для описания контрактов передачи данных между слоями приложения или подсистемами приложения. + +import { UserType } from '../../../types/user-type.enum.js'; + +export class CreateUserDto { + public email!: string; + // TODO: Указываем дефолтное значение и поле необязательно + public avatarPath!: string; + public firstName!: string; + public lastName!: string; + public password!: string; + public type!: UserType; +} diff --git a/src/shared/modules/user/index.ts b/src/shared/modules/user/index.ts new file mode 100644 index 0000000..437f584 --- /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 { createUserContainer } from './user.container.js'; +export { UserService } from './user-service.interface.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..6a2d91b --- /dev/null +++ b/src/shared/modules/user/user-service.interface.ts @@ -0,0 +1,10 @@ +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..653827a --- /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 { DefaultUserService } from './default-user.service.js'; +import { COMPONENT } from '../../constants/component.constant.js'; +import { UserEntity, UserModel } from './user.entity.js'; + +export function createUserContainer() { + const userContainer = new Container(); + userContainer.bind(COMPONENT.USER_SERVICE).to(DefaultUserService).inSingletonScope(); + userContainer.bind>(COMPONENT.USER_MODEL).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..d903217 --- /dev/null +++ b/src/shared/modules/user/user.entity.ts @@ -0,0 +1,56 @@ +import { defaultClasses, getModelForClass, prop, modelOptions } from '@typegoose/typegoose'; + +import { User, UserType } from '../../types/index.js'; +import { createSHA256 } from '../../helpers/index.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 firstName!: string; + +@prop({ required: true }) +public lastName!: string; + +@prop({ unique: true, required: true }) +public email!: string; + +@prop({ default: 'default-avatar.jpg' }) +public avatarPath!: string; + +@prop({ required: true }) +public password!: string; + +@prop({ required: true, enum: UserType }) +public type!: UserType; + +constructor(userData: User) { + super(); + + this.email = userData.email; + this.avatarPath = userData.avatarPath; + this.firstName = userData.firstName; + this.lastName = userData.lastName; + this.type = userData.type; +} + +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/modules/user/user.model.ts b/src/shared/modules/user/user.model.ts new file mode 100644 index 0000000..870dd90 --- /dev/null +++ b/src/shared/modules/user/user.model.ts @@ -0,0 +1,34 @@ +// TODO: Удалить в будущем, работа через mongoose +import { Schema, Document, model } from 'mongoose'; +import { User, UserType } from '../../types/index.js'; + +export interface UserDocument extends User, Document { + createdAt: Date, + updatedAt: Date, +} + +const userSchema = new Schema({ + email: { + type: String, + unique: true, + match: [/^([\w-\\.]+@([\w-]+\.)+[\w-]{2,4})?$/, 'Email is incorrect'], + required: true, + }, + avatarPath: { + type: String, + required: true, + minlength: [5, 'Min length for avatar path is 5'], + }, + firstName: { + type: String, + required: true, + minlength: [2, 'Min length for firstname is 2'] + }, + lastName: String, + type: { + enum: UserType, + required: true, + } +}, { timestamps: true }); + +export const UserModel = model('User', userSchema); diff --git a/src/shared/types/user.interface.ts b/src/shared/types/user.interface.ts index 1598f73..e6c58a9 100644 --- a/src/shared/types/user.interface.ts +++ b/src/shared/types/user.interface.ts @@ -5,6 +5,6 @@ export interface User { lastName: string; email: string; avatarPath: string; - password: string; + password?: string; type: UserType; }