From bcdcb8dd40e33957b8c81893cf11330a5e60f651 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 23 Mar 2026 21:07:54 +0300 Subject: [PATCH 01/12] adds docker config --- .env-example | 5 ++++- docker-compose.dev.yml | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 docker-compose.dev.yml diff --git a/.env-example b/.env-example index bb3fce3..9e97339 100644 --- a/.env-example +++ b/.env-example @@ -1,3 +1,6 @@ PORT=5000 SALT="iamsalt" -DB_HOST="127.0.0.1" +DB_HOST=127.0.0.1 +DB_USER=admin +DB_PASSWORD=test +DB_PORT=27017 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..2be3a7d --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,31 @@ +version: '3.9' +services: + db: + image: mongo:4.2 + restart: always + container_name: six-cities_mongodb + environment: + MONGO_INITDB_ROOT_USERNAME: ${DB_USER} + MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD} + ports: + - ${DB_PORT}:27017 + volumes: + - six-cities_data:/data/db + + db_ui: + image: mongo-express:latest + restart: always + container_name: six-cities_mongo_express + ports: + - 8081:8081 + depends_on: + - db + 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: From a9b373e0307fd572ffaaacd4c3f425c0cc35d1ee Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 23 Mar 2026 21:08:12 +0300 Subject: [PATCH 02/12] adds mongoose and typegoose --- package-lock.json | 460 ++++++++++++++++++++++++++++++++++++++++------ package.json | 2 + 2 files changed, 401 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 05a4e46..77494cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,13 @@ "dayjs": "1.11.19", "got": "14.6.6", "inversify": "7.11.0", + "mongoose": "9.2.2", "pino": "10.3.1", "reflect-metadata": "0.2.2", "tslib": "2.8.1" }, "devDependencies": { + "@typegoose/typegoose": "13.2.1", "@types/convict": "6.1.6", "@types/convict-format-with-validator": "6.0.6", "@types/node": "18.17.17", @@ -388,6 +390,15 @@ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", "license": "MIT" }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "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", @@ -746,6 +757,26 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "node_modules/@typegoose/typegoose": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-13.2.1.tgz", + "integrity": "sha512-RGoaTYTy2P7RQ81Pr+LWS5bhmcGMsL5w3X6GPT16HK8NC7fC3WwUnPrsT92xRyRXByUIA3rTCIHq0uryck8Vpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "loglevel": "^1.9.2", + "reflect-metadata": "^0.2.2", + "semver": "^7.7.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "mongoose": "~9.2.2" + } + }, "node_modules/@types/convict": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.6.tgz", @@ -796,6 +827,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": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "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", @@ -1191,6 +1237,15 @@ "node": ">=8" } }, + "node_modules/bson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -2229,10 +2284,11 @@ } }, "node_modules/flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", - "dev": true + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" }, "node_modules/for-each": { "version": "0.3.3", @@ -3159,6 +3215,15 @@ "node": ">=4.0" } }, + "node_modules/kareem": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz", + "integrity": "sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/keyv": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", @@ -3221,6 +3286,20 @@ "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==", + "dev": true, + "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", @@ -3261,24 +3340,18 @@ "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==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3382,6 +3455,110 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "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": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongoose": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.2.2.tgz", + "integrity": "sha512-e06XdPPlH/L9aEq4vcnqIz5AxFFdfhlqrmymYDO7fZwnqwVp/u8pAH/cCEvvpXg0VlV0Tt5qwu6RUk8lhu6ifg==", + "license": "MIT", + "dependencies": { + "kareem": "3.2.0", + "mongodb": "~7.0", + "mpath": "0.9.0", + "mquery": "6.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "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/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": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -3874,10 +4051,10 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -4274,13 +4451,11 @@ "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==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4323,6 +4498,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", @@ -4384,6 +4565,15 @@ "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==", + "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", @@ -4701,6 +4891,18 @@ "node": ">=6" } }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -4862,6 +5064,28 @@ "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==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5020,12 +5244,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", @@ -5321,6 +5539,14 @@ "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==" }, + "@mongodb-js/saslprep": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "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", @@ -5564,6 +5790,19 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "@typegoose/typegoose": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-13.2.1.tgz", + "integrity": "sha512-RGoaTYTy2P7RQ81Pr+LWS5bhmcGMsL5w3X6GPT16HK8NC7fC3WwUnPrsT92xRyRXByUIA3rTCIHq0uryck8Vpw==", + "dev": true, + "requires": { + "lodash": "^4.17.20", + "loglevel": "^1.9.2", + "reflect-metadata": "^0.2.2", + "semver": "^7.7.3", + "tslib": "^2.8.1" + } + }, "@types/convict": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.6.tgz", @@ -5611,6 +5850,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": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "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", @@ -5859,6 +6111,11 @@ "fill-range": "^7.1.1" } }, + "bson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==" + }, "builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -6603,9 +6860,9 @@ } }, "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, "for-each": { @@ -7247,6 +7504,11 @@ "object.assign": "^4.1.3" } }, + "kareem": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz", + "integrity": "sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==" + }, "keyv": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", @@ -7297,6 +7559,12 @@ "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==", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7320,21 +7588,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", @@ -7395,6 +7659,55 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true }, + "mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "requires": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + } + }, + "mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "requires": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + } + }, + "mongoose": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.2.2.tgz", + "integrity": "sha512-e06XdPPlH/L9aEq4vcnqIz5AxFFdfhlqrmymYDO7fZwnqwVp/u8pAH/cCEvvpXg0VlV0Tt5qwu6RUk8lhu6ifg==", + "requires": { + "kareem": "3.2.0", + "mongodb": "~7.0", + "mpath": "0.9.0", + "mquery": "6.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": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==" + }, "mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -7748,10 +8061,9 @@ } }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, "queue-microtask": { "version": "1.2.3", @@ -7999,13 +8311,10 @@ "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.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true }, "shebang-command": { "version": "2.0.0", @@ -8033,6 +8342,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", @@ -8073,6 +8387,14 @@ "dot-prop": "^10.1.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", @@ -8295,6 +8617,14 @@ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true }, + "tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "requires": { + "punycode": "^2.3.1" + } + }, "ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -8402,6 +8732,20 @@ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==" }, + "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": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "requires": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8508,12 +8852,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 40273c1..cf1ae7e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "mock:server": "json-server ./mocks/mock-server-data.json --port 3123" }, "devDependencies": { + "@typegoose/typegoose": "13.2.1", "@types/convict": "6.1.6", "@types/convict-format-with-validator": "6.0.6", "@types/node": "18.17.17", @@ -45,6 +46,7 @@ "dayjs": "1.11.19", "got": "14.6.6", "inversify": "7.11.0", + "mongoose": "9.2.2", "pino": "10.3.1", "reflect-metadata": "0.2.2", "tslib": "2.8.1" From b9261ed6ccd201198b2ab79153049a162e5379b6 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 23 Mar 2026 21:08:28 +0300 Subject: [PATCH 03/12] updates rest schema --- src/shared/libs/config/rest.schema.ts | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index 2fda343..60c602a 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -7,6 +7,10 @@ export type RestSchema = { PORT: number; SALT: string; DB_HOST: string; + DB_USER: string; + DB_PASSWORD: string; + DB_PORT: string; + DB_NAME: string; }; export const configRestSchema = convict({ @@ -28,4 +32,28 @@ export const configRestSchema = convict({ env: 'DB_HOST', default: '127.0.0.1', }, + DB_USER: { + doc: 'Username to connect to the database', + 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: '27017', + }, + DB_NAME: { + doc: 'Database name (MongoDB)', + format: String, + env: 'DB_NAME', + default: 'buy-and-sell', + }, }); From d96d79a28b0a34cd59a16b1b8e4e98a639ae7696 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Tue, 24 Mar 2026 21:09:23 +0300 Subject: [PATCH 04/12] adds database client --- src/main.rest.ts | 8 ++++ src/shared/helpers/database.ts | 17 ++++++++ src/shared/helpers/index.ts | 1 + src/shared/libs/config/rest.schema.ts | 2 +- .../database-client.interface.ts | 4 ++ src/shared/libs/database-client/index.ts | 2 + .../database-client/mongo.database-client.ts | 42 +++++++++++++++++++ src/shared/types/component.enum.ts | 1 + 8 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/shared/helpers/database.ts create mode 100644 src/shared/libs/database-client/database-client.interface.ts create mode 100644 src/shared/libs/database-client/index.ts create mode 100644 src/shared/libs/database-client/mongo.database-client.ts diff --git a/src/main.rest.ts b/src/main.rest.ts index 93dd317..6ba6744 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -4,6 +4,10 @@ import { RestApplication } from './rest/index.js'; import { Logger, PinoLogger } from './shared/libs/logger/index.js'; import { Config, RestConfig, RestSchema } from './shared/libs/config/index.js'; import { Component } from './shared/types/index.js'; +import { + DatabaseClient, + MongoDatabaseClient, +} from './shared/libs/database-client/index.js'; async function bootstrap() { const container = new Container(); @@ -16,6 +20,10 @@ async function bootstrap() { .bind>(Component.Config) .to(RestConfig) .inSingletonScope(); + container + .bind(Component.DatabaseClient) + .to(MongoDatabaseClient) + .inSingletonScope(); const app = container.get(Component.RestApplication); await app.init(); diff --git a/src/shared/helpers/database.ts b/src/shared/helpers/database.ts new file mode 100644 index 0000000..7df1fa1 --- /dev/null +++ b/src/shared/helpers/database.ts @@ -0,0 +1,17 @@ +type MongoURI = { + username: string; + password: string; + host: string; + port: string; + dbName: string; +}; + +export function getMongoURI({ + username, + password, + host, + port, + dbName, +}: MongoURI): string { + return `mongodb://${username}:${password}@${host}:${port}/${dbName}?authSource=admin`; +} diff --git a/src/shared/helpers/index.ts b/src/shared/helpers/index.ts index 404daf1..cf365fe 100644 --- a/src/shared/helpers/index.ts +++ b/src/shared/helpers/index.ts @@ -3,3 +3,4 @@ export * from './mock-helpers.js'; export * from './offer.js'; export * from './common.js'; export * from './file-system.js'; +export * from './database.js'; diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index 60c602a..20a7e96 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -54,6 +54,6 @@ export const configRestSchema = convict({ doc: 'Database name (MongoDB)', format: String, env: 'DB_NAME', - default: 'buy-and-sell', + default: 'six-cities-2', }, }); 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..e31e958 --- /dev/null +++ b/src/shared/libs/database-client/index.ts @@ -0,0 +1,2 @@ +export * from './database-client.interface.js'; +export * 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..5e11578 --- /dev/null +++ b/src/shared/libs/database-client/mongo.database-client.ts @@ -0,0 +1,42 @@ +import * as Mongoose from 'mongoose'; +import { inject, injectable } from 'inversify'; +import { DatabaseClient } from './database-client.interface.js'; +import { Component } from '../../types/index.js'; +import { Logger } from '../logger/index.js'; + +@injectable() +export class MongoDatabaseClient implements DatabaseClient { + private mongoose: typeof Mongoose; + private isConnected: boolean; + + constructor(@inject(Component.Logger) private readonly logger: Logger) { + this.isConnected = false; + } + + public isConnectedToDatabase() { + return this.isConnected; + } + + public async connect(uri: string): Promise { + if (this.isConnectedToDatabase()) { + throw new Error('MongoDB client already connected'); + } + + this.logger.info('Trying to connect to MongoDB…'); + + this.mongoose = await Mongoose.connect(uri); + this.isConnected = true; + + this.logger.info('Database connection established.'); + } + + 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/types/component.enum.ts b/src/shared/types/component.enum.ts index c7a25ed..6ea69d0 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -2,4 +2,5 @@ export const Component = { RestApplication: Symbol.for('RestApplication'), Logger: Symbol.for('Logger'), Config: Symbol.for('Config'), + DatabaseClient: Symbol.for('DatabaseClient'), } as const; From 76a8b318229eb51c7cfba38d6d03ede4c6b277f0 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Tue, 24 Mar 2026 22:38:23 +0300 Subject: [PATCH 05/12] adds database connection --- src/rest/rest.application.ts | 22 ++++++++++++- src/shared/helpers/database.ts | 24 +++++--------- src/shared/libs/config/rest.schema.ts | 16 +++++++++- .../database-client/mongo.database-client.ts | 31 ++++++++++++++++--- 4 files changed, 71 insertions(+), 22 deletions(-) diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 91f147e..519a8f3 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -1,17 +1,37 @@ import { inject, injectable } from 'inversify'; -import { Config, RestSchema } from '../shared/libs/config/index.js'; import { Logger } from '../shared/libs/logger/index.js'; +import { Config, RestSchema } from '../shared/libs/config/index.js'; import { Component } from '../shared/types/index.js'; +import { DatabaseClient } from '../shared/libs/database-client/index.js'; +import { getMongoURI } from '../shared/helpers/index.js'; @injectable() export class RestApplication { constructor( @inject(Component.Logger) private readonly logger: Logger, @inject(Component.Config) private readonly config: Config, + @inject(Component.DatabaseClient) + private readonly databaseClient: DatabaseClient, ) {} + private async _initDb() { + const mongoUri = getMongoURI( + this.config.get('DB_USER'), + this.config.get('DB_PASSWORD'), + this.config.get('DB_HOST'), + this.config.get('DB_PORT'), + this.config.get('DB_NAME'), + ); + + return this.databaseClient.connect(mongoUri); + } + public async init() { this.logger.info('Application initialization'); this.logger.info(`Get value from env $PORT: ${this.config.get('PORT')}`); + + this.logger.info('Init database...'); + await this._initDb(); + this.logger.info('Init database completed'); } } diff --git a/src/shared/helpers/database.ts b/src/shared/helpers/database.ts index 7df1fa1..f79b628 100644 --- a/src/shared/helpers/database.ts +++ b/src/shared/helpers/database.ts @@ -1,17 +1,9 @@ -type MongoURI = { - username: string; - password: string; - host: string; - port: string; - dbName: string; -}; - -export function getMongoURI({ - username, - password, - host, - port, - dbName, -}: MongoURI): string { - return `mongodb://${username}:${password}@${host}:${port}/${dbName}?authSource=admin`; +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/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index 20a7e96..13e83a0 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -3,6 +3,20 @@ import validator from 'convict-format-with-validator'; convict.addFormats(validator); +// Кастомный формат для IP или localhost +convict.addFormat({ + name: 'ip-or-localhost', + validate: (val: string) => { + // проверка IPv4 + localhost + const ipv4Regex = + /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/; + if (val !== 'localhost' && !ipv4Regex.test(val)) { + throw new Error('DB_HOST must be a valid IPv4 address or "localhost"'); + } + }, + coerce: (val: string) => val, +}); + export type RestSchema = { PORT: number; SALT: string; @@ -28,7 +42,7 @@ export const configRestSchema = convict({ }, DB_HOST: { doc: 'IP address of the database server (MongoDB)', - format: 'ipaddress', + format: 'ip-or-localhost', env: 'DB_HOST', default: '127.0.0.1', }, diff --git a/src/shared/libs/database-client/mongo.database-client.ts b/src/shared/libs/database-client/mongo.database-client.ts index 5e11578..a8a8449 100644 --- a/src/shared/libs/database-client/mongo.database-client.ts +++ b/src/shared/libs/database-client/mongo.database-client.ts @@ -1,9 +1,13 @@ import * as Mongoose from 'mongoose'; import { inject, injectable } from 'inversify'; +import { setTimeout } from 'node:timers/promises'; import { DatabaseClient } from './database-client.interface.js'; import { Component } from '../../types/index.js'; import { Logger } from '../logger/index.js'; +const RETRY_COUNT = 5; +const RETRY_TIMEOUT = 1000; + @injectable() export class MongoDatabaseClient implements DatabaseClient { private mongoose: typeof Mongoose; @@ -22,12 +26,31 @@ export class MongoDatabaseClient implements DatabaseClient { throw new Error('MongoDB client already connected'); } - this.logger.info('Trying to connect to MongoDB…'); + this.logger.info('Trying to connect to MongoDB...'); + + let attempt = 0; + + while (attempt < RETRY_COUNT) { + try { + this.mongoose = await Mongoose.connect(uri); + this.isConnected = true; - 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(RETRY_TIMEOUT); + } + } - this.logger.info('Database connection established.'); + throw new Error( + `Unable to establish database connection after ${RETRY_COUNT} tries`, + ); } public async disconnect(): Promise { From 8057e25da24faa466577a513417af07dab003ab9 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Wed, 25 Mar 2026 22:26:07 +0300 Subject: [PATCH 06/12] adds user model --- src/shared/modules/user/user.model.ts | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/shared/modules/user/user.model.ts diff --git a/src/shared/modules/user/user.model.ts b/src/shared/modules/user/user.model.ts new file mode 100644 index 0000000..f2c8956 --- /dev/null +++ b/src/shared/modules/user/user.model.ts @@ -0,0 +1,32 @@ +import { Document, model, Schema } from 'mongoose'; +import { User } 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, + }, + { timestamps: true }, +); + +export const UserModel = model('User', userSchema); From 883aa38d8e2cabde5a85a9b5e5210219568d83eb Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 29 Mar 2026 15:20:21 +0300 Subject: [PATCH 07/12] adds user service and splits containers --- package-lock.json | 180 ++++++------------ package.json | 4 +- src/main.rest.ts | 28 +-- src/rest/rest.container.ts | 32 ++++ src/shared/helpers/hash.ts | 6 + src/shared/helpers/index.ts | 1 + .../modules/user/default-user.service.ts | 48 +++++ .../modules/user/dto/create-user.dto.ts | 10 + src/shared/modules/user/index.ts | 4 + .../modules/user/user-service.interface.ts | 12 ++ src/shared/modules/user/user.container.ts | 19 ++ src/shared/modules/user/user.entity.ts | 58 ++++++ src/shared/types/component.enum.ts | 2 + src/shared/types/index.ts | 1 + src/shared/types/user-type.type.ts | 1 + src/shared/types/user.type.ts | 9 +- 16 files changed, 272 insertions(+), 143 deletions(-) create mode 100644 src/rest/rest.container.ts create mode 100644 src/shared/helpers/hash.ts create mode 100644 src/shared/modules/user/default-user.service.ts create mode 100644 src/shared/modules/user/dto/create-user.dto.ts create mode 100644 src/shared/modules/user/index.ts create mode 100644 src/shared/modules/user/user-service.interface.ts create mode 100644 src/shared/modules/user/user.container.ts create mode 100644 src/shared/modules/user/user.entity.ts create mode 100644 src/shared/types/user-type.type.ts diff --git a/package-lock.json b/package-lock.json index 77494cc..b98d8d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,11 @@ "version": "5.0.0", "dependencies": { "chalk": "5.6.2", - "convict": "6.2.4", + "convict": "^6.2.5", "convict-format-with-validator": "6.2.0", "dayjs": "1.11.19", "got": "14.6.6", - "inversify": "7.11.0", + "inversify": "^6.2.2", "mongoose": "9.2.2", "pino": "10.3.1", "reflect-metadata": "0.2.2", @@ -257,59 +257,28 @@ "dev": true }, "node_modules/@inversifyjs/common": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.5.2.tgz", - "integrity": "sha512-WlzR9xGadABS9gtgZQ+luoZ8V6qm4Ii6RQfcfC9Ho2SOlE6ZuemFo7PKJvKI0ikm8cmKbU8hw5UK6E4qovH21w==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.4.0.tgz", + "integrity": "sha512-qfRJ/3iOlCL/VfJq8+4o5X4oA14cZSBbpAmHsYj8EsIit1xDndoOl0xKOyglKtQD4u4gdNVxMHx4RWARk/I4QA==", "license": "MIT" }, - "node_modules/@inversifyjs/container": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/container/-/container-1.15.0.tgz", - "integrity": "sha512-U2xYsPrJTz5za2TExi5lg8qOWf8TEVBpN+pQM7B8BVA2rajtbRE9A66SLRHk8c1eGXmg+0K4Hdki6tWAsSQBUA==", - "license": "MIT", - "dependencies": { - "@inversifyjs/common": "1.5.2", - "@inversifyjs/core": "9.2.0", - "@inversifyjs/plugin": "0.2.0", - "@inversifyjs/reflect-metadata-utils": "1.4.1" - }, - "peerDependencies": { - "reflect-metadata": "~0.2.2" - } - }, "node_modules/@inversifyjs/core": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-9.2.0.tgz", - "integrity": "sha512-Nm7BR6KmpgshIHpVQWuEDehqRVb6GBm8LFEuhc2s4kSZWrArZ15RmXQzROLk4m+hkj4kMXgvMm5Qbopot/D6Sg==", - "license": "MIT", - "dependencies": { - "@inversifyjs/common": "1.5.2", - "@inversifyjs/prototype-utils": "0.1.3", - "@inversifyjs/reflect-metadata-utils": "1.4.1" - } - }, - "node_modules/@inversifyjs/plugin": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/plugin/-/plugin-0.2.0.tgz", - "integrity": "sha512-R/JAdkTSD819pV1zi0HP54mWHyX+H2m8SxldXRgPQarS3ySV4KPyRdosWcfB8Se0JJZWZLHYiUNiS6JvMWSPjw==", - "license": "MIT" - }, - "node_modules/@inversifyjs/prototype-utils": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@inversifyjs/prototype-utils/-/prototype-utils-0.1.3.tgz", - "integrity": "sha512-EzRamZzNgE9Sn3QtZ8NncNa2lpPMZfspqbK6BWFguWnOpK8ymp2TUuH46ruFHZhrHKnknPd7fG22ZV7iF517TQ==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-1.3.5.tgz", + "integrity": "sha512-B4MFXabhNTAmrfgB+yeD6wd/GIvmvWC6IQ8Rh/j2C3Ix69kmqwz9pr8Jt3E+Nho9aEHOQCZaGmrALgtqRd+oEQ==", "license": "MIT", "dependencies": { - "@inversifyjs/common": "1.5.2" + "@inversifyjs/common": "1.4.0", + "@inversifyjs/reflect-metadata-utils": "0.2.4" } }, "node_modules/@inversifyjs/reflect-metadata-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-1.4.1.tgz", - "integrity": "sha512-Cp77C4d2wLaHXiUB7iH6Cxb7i1lD/YDuTIHLTDzKINqGSz0DCSoL/Dg2wVkW/6Qx03r/yQMLJ+32Agl32N2X8g==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-0.2.4.tgz", + "integrity": "sha512-u95rV3lKfG+NT2Uy/5vNzoDujos8vN8O18SSA5UyhxsGYd4GLQn/eUsGXfOsfa7m34eKrDelTKRUX1m/BcNX5w==", "license": "MIT", "peerDependencies": { - "reflect-metadata": "~0.2.2" + "reflect-metadata": "0.2.2" } }, "node_modules/@isaacs/cliui": { @@ -1214,9 +1183,9 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1415,9 +1384,9 @@ "dev": true }, "node_modules/convict": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.4.tgz", - "integrity": "sha512-qN60BAwdMVdofckX7AlohVJ2x9UvjTNoKVXCL2LxFk1l7757EJqf1nySdMkPQer0bt8kQ5lQiyZ9/2NvrFBuwQ==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.5.tgz", + "integrity": "sha512-JtXpxqDqJ8P0UwEHwhxLzCIXQy97vlYBZR222Sbzb1q1Erex9ASrztJ29SyhWFQjod1AeFBaPzEEC8YvtZMIYg==", "license": "Apache-2.0", "dependencies": { "lodash.clonedeep": "^4.5.0", @@ -2444,9 +2413,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -2790,14 +2759,16 @@ } }, "node_modules/inversify": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-7.11.0.tgz", - "integrity": "sha512-yZDprSSr8TyVeMGI/AOV4ws6gwjX22hj9Z8/oHAVpJORY6WRFTcUzhnZtibBUHEw2U8ArvHcR+i863DplQ3Cwg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.2.2.tgz", + "integrity": "sha512-KB836KHbZ9WrUnB8ax5MtadOwnqQYa+ZJO3KWbPFgcr4RIEnHM621VaqFZzOZd9+U7ln6upt9n0wJei7x2BNqw==", "license": "MIT", "dependencies": { - "@inversifyjs/common": "1.5.2", - "@inversifyjs/container": "1.15.0", - "@inversifyjs/core": "9.2.0" + "@inversifyjs/common": "1.4.0", + "@inversifyjs/core": "1.3.5" + }, + "peerDependencies": { + "reflect-metadata": "~0.2.2" } }, "node_modules/is-array-buffer": { @@ -3908,10 +3879,11 @@ } }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -5437,48 +5409,23 @@ "dev": true }, "@inversifyjs/common": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.5.2.tgz", - "integrity": "sha512-WlzR9xGadABS9gtgZQ+luoZ8V6qm4Ii6RQfcfC9Ho2SOlE6ZuemFo7PKJvKI0ikm8cmKbU8hw5UK6E4qovH21w==" - }, - "@inversifyjs/container": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/container/-/container-1.15.0.tgz", - "integrity": "sha512-U2xYsPrJTz5za2TExi5lg8qOWf8TEVBpN+pQM7B8BVA2rajtbRE9A66SLRHk8c1eGXmg+0K4Hdki6tWAsSQBUA==", - "requires": { - "@inversifyjs/common": "1.5.2", - "@inversifyjs/core": "9.2.0", - "@inversifyjs/plugin": "0.2.0", - "@inversifyjs/reflect-metadata-utils": "1.4.1" - } + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.4.0.tgz", + "integrity": "sha512-qfRJ/3iOlCL/VfJq8+4o5X4oA14cZSBbpAmHsYj8EsIit1xDndoOl0xKOyglKtQD4u4gdNVxMHx4RWARk/I4QA==" }, "@inversifyjs/core": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-9.2.0.tgz", - "integrity": "sha512-Nm7BR6KmpgshIHpVQWuEDehqRVb6GBm8LFEuhc2s4kSZWrArZ15RmXQzROLk4m+hkj4kMXgvMm5Qbopot/D6Sg==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@inversifyjs/core/-/core-1.3.5.tgz", + "integrity": "sha512-B4MFXabhNTAmrfgB+yeD6wd/GIvmvWC6IQ8Rh/j2C3Ix69kmqwz9pr8Jt3E+Nho9aEHOQCZaGmrALgtqRd+oEQ==", "requires": { - "@inversifyjs/common": "1.5.2", - "@inversifyjs/prototype-utils": "0.1.3", - "@inversifyjs/reflect-metadata-utils": "1.4.1" - } - }, - "@inversifyjs/plugin": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@inversifyjs/plugin/-/plugin-0.2.0.tgz", - "integrity": "sha512-R/JAdkTSD819pV1zi0HP54mWHyX+H2m8SxldXRgPQarS3ySV4KPyRdosWcfB8Se0JJZWZLHYiUNiS6JvMWSPjw==" - }, - "@inversifyjs/prototype-utils": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@inversifyjs/prototype-utils/-/prototype-utils-0.1.3.tgz", - "integrity": "sha512-EzRamZzNgE9Sn3QtZ8NncNa2lpPMZfspqbK6BWFguWnOpK8ymp2TUuH46ruFHZhrHKnknPd7fG22ZV7iF517TQ==", - "requires": { - "@inversifyjs/common": "1.5.2" + "@inversifyjs/common": "1.4.0", + "@inversifyjs/reflect-metadata-utils": "0.2.4" } }, "@inversifyjs/reflect-metadata-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-1.4.1.tgz", - "integrity": "sha512-Cp77C4d2wLaHXiUB7iH6Cxb7i1lD/YDuTIHLTDzKINqGSz0DCSoL/Dg2wVkW/6Qx03r/yQMLJ+32Agl32N2X8g==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-0.2.4.tgz", + "integrity": "sha512-u95rV3lKfG+NT2Uy/5vNzoDujos8vN8O18SSA5UyhxsGYd4GLQn/eUsGXfOsfa7m34eKrDelTKRUX1m/BcNX5w==", "requires": {} }, "@isaacs/cliui": { @@ -6093,9 +6040,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -6227,9 +6174,9 @@ "dev": true }, "convict": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.4.tgz", - "integrity": "sha512-qN60BAwdMVdofckX7AlohVJ2x9UvjTNoKVXCL2LxFk1l7757EJqf1nySdMkPQer0bt8kQ5lQiyZ9/2NvrFBuwQ==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.5.tgz", + "integrity": "sha512-JtXpxqDqJ8P0UwEHwhxLzCIXQy97vlYBZR222Sbzb1q1Erex9ASrztJ29SyhWFQjod1AeFBaPzEEC8YvtZMIYg==", "requires": { "lodash.clonedeep": "^4.5.0", "yargs-parser": "^20.2.7" @@ -6964,9 +6911,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -7216,13 +7163,12 @@ } }, "inversify": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-7.11.0.tgz", - "integrity": "sha512-yZDprSSr8TyVeMGI/AOV4ws6gwjX22hj9Z8/oHAVpJORY6WRFTcUzhnZtibBUHEw2U8ArvHcR+i863DplQ3Cwg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.2.2.tgz", + "integrity": "sha512-KB836KHbZ9WrUnB8ax5MtadOwnqQYa+ZJO3KWbPFgcr4RIEnHM621VaqFZzOZd9+U7ln6upt9n0wJei7x2BNqw==", "requires": { - "@inversifyjs/common": "1.5.2", - "@inversifyjs/container": "1.15.0", - "@inversifyjs/core": "9.2.0" + "@inversifyjs/common": "1.4.0", + "@inversifyjs/core": "1.3.5" } }, "is-array-buffer": { @@ -7957,9 +7903,9 @@ "dev": true }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "pino": { diff --git a/package.json b/package.json index cf1ae7e..7aca554 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,11 @@ }, "dependencies": { "chalk": "5.6.2", - "convict": "6.2.4", + "convict": "6.2.5", "convict-format-with-validator": "6.2.0", "dayjs": "1.11.19", "got": "14.6.6", - "inversify": "7.11.0", + "inversify": "6.2.2", "mongoose": "9.2.2", "pino": "10.3.1", "reflect-metadata": "0.2.2", diff --git a/src/main.rest.ts b/src/main.rest.ts index 6ba6744..9430696 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -1,31 +1,17 @@ import 'reflect-metadata'; import { Container } from 'inversify'; import { RestApplication } from './rest/index.js'; -import { Logger, PinoLogger } from './shared/libs/logger/index.js'; -import { Config, RestConfig, RestSchema } from './shared/libs/config/index.js'; import { Component } from './shared/types/index.js'; -import { - DatabaseClient, - MongoDatabaseClient, -} from './shared/libs/database-client/index.js'; +import { createRestApplicationContainer } from './rest/rest.container.js'; +import { createUserContainer } from './shared/modules/user/index.js'; async function bootstrap() { - const container = new Container(); - container - .bind(Component.RestApplication) - .to(RestApplication) - .inSingletonScope(); - container.bind(Component.Logger).to(PinoLogger).inSingletonScope(); - container - .bind>(Component.Config) - .to(RestConfig) - .inSingletonScope(); - container - .bind(Component.DatabaseClient) - .to(MongoDatabaseClient) - .inSingletonScope(); + const appContainer = Container.merge( + createRestApplicationContainer(), + createUserContainer(), + ); - const app = container.get(Component.RestApplication); + const app = appContainer.get(Component.RestApplication); await app.init(); } diff --git a/src/rest/rest.container.ts b/src/rest/rest.container.ts new file mode 100644 index 0000000..82a7dd0 --- /dev/null +++ b/src/rest/rest.container.ts @@ -0,0 +1,32 @@ +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, RestConfig, RestSchema } from '../shared/libs/config/index.js'; +import { + DatabaseClient, + MongoDatabaseClient, +} from '../shared/libs/database-client/index.js'; + +export function createRestApplicationContainer() { + const restApplicationContainer = new Container(); + + restApplicationContainer + .bind(Component.RestApplication) + .to(RestApplication) + .inSingletonScope(); + restApplicationContainer + .bind(Component.Logger) + .to(PinoLogger) + .inSingletonScope(); + restApplicationContainer + .bind>(Component.Config) + .to(RestConfig) + .inSingletonScope(); + restApplicationContainer + .bind(Component.DatabaseClient) + .to(MongoDatabaseClient) + .inSingletonScope(); + + return restApplicationContainer; +} diff --git a/src/shared/helpers/hash.ts b/src/shared/helpers/hash.ts new file mode 100644 index 0000000..856ad58 --- /dev/null +++ b/src/shared/helpers/hash.ts @@ -0,0 +1,6 @@ +import * as crypto from 'node:crypto'; + +export const createSHA256 = (line: string, salt: string): string => { + const shaHasher = crypto.createHmac('sha256', salt); + return shaHasher.update(line).digest('hex'); +}; diff --git a/src/shared/helpers/index.ts b/src/shared/helpers/index.ts index cf365fe..b2ba0c4 100644 --- a/src/shared/helpers/index.ts +++ b/src/shared/helpers/index.ts @@ -4,3 +4,4 @@ export * from './offer.js'; export * from './common.js'; export * from './file-system.js'; export * from './database.js'; +export * from './hash.js'; 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..c23880b --- /dev/null +++ b/src/shared/modules/user/default-user.service.ts @@ -0,0 +1,48 @@ +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/index.js'; +import { Logger } from '../../libs/logger/index.js'; + +@injectable() +export class DefaultUserService implements UserService { + constructor( + @inject(Component.Logger) private readonly logger: Logger, + @inject(Component.UserModel) + private readonly userModel: types.ModelType, + ) {} + + public async create( + dto: CreateUserDto, + salt: string, + ): Promise> { + const user = new UserEntity(dto); + user.setPassword(dto.password, salt); + + const result = await this.userModel.create(user); + this.logger.info(`New user created: ${user.email}`); + + return result; + } + + public async findByEmail( + email: string, + ): Promise | null> { + return this.userModel.findOne({ email }); + } + + 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..a0a4ea5 --- /dev/null +++ b/src/shared/modules/user/dto/create-user.dto.ts @@ -0,0 +1,10 @@ +import { UserType } from '../../../types/index.js'; + +export class CreateUserDto { + public email: string; + 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..652d8f9 --- /dev/null +++ b/src/shared/modules/user/index.ts @@ -0,0 +1,4 @@ +export * from './user.entity.js'; +export * from './dto/create-user.dto.js'; +export * from './default-user.service.js'; +export * from './user.container.js'; diff --git a/src/shared/modules/user/user-service.interface.ts b/src/shared/modules/user/user-service.interface.ts new file mode 100644 index 0000000..72d9de3 --- /dev/null +++ b/src/shared/modules/user/user-service.interface.ts @@ -0,0 +1,12 @@ +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..eff87a5 --- /dev/null +++ b/src/shared/modules/user/user.container.ts @@ -0,0 +1,19 @@ +import { Container } from 'inversify'; +import { UserService } from './user-service.interface.js'; +import { Component } from '../../types/index.js'; +import { DefaultUserService } from './default-user.service.js'; +import { types } from '@typegoose/typegoose'; +import { UserEntity, UserModel } from './user.entity.js'; + +export function createUserContainer() { + const userContainer = new Container(); + userContainer + .bind(Component.UserServise) + .to(DefaultUserService) + .inSingletonScope(); + userContainer + .bind>(Component.UserModel) + .toConstantValue(UserModel); + + return userContainer; +} diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts new file mode 100644 index 0000000..8636921 --- /dev/null +++ b/src/shared/modules/user/user.entity.ts @@ -0,0 +1,58 @@ +import { + defaultClasses, + getModelForClass, + modelOptions, + prop, +} 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({ unique: true, required: true }) + public email: string; + + @prop({ required: false, default: '' }) + public avatarPath?: string; + + @prop({ required: true, default: '' }) + public firstname: string; + + @prop({ required: true, default: '' }) + public lastname: string; + + @prop({ required: true }) + public type: UserType; + + @prop({ required: true, default: '' }) + private password?: string; + + constructor(userData: User) { + super(); + + this.email = userData.email; + this.avatarPath = userData.avatarPath; + this.firstname = userData.firstname; + this.lastname = userData.lastname; + } + + 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 6ea69d0..b2bf148 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -3,4 +3,6 @@ export const Component = { Logger: Symbol.for('Logger'), Config: Symbol.for('Config'), DatabaseClient: Symbol.for('DatabaseClient'), + UserServise: Symbol.for('UserService'), + UserModel: Symbol.for('UserModel'), } as const; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 9360ccf..7b1772a 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -6,3 +6,4 @@ export * from './offer.type.js'; export * from './user.type.js'; export * from './mock-server-data.type.js'; export * from './component.enum.js'; +export * from './user-type.type.js'; diff --git a/src/shared/types/user-type.type.ts b/src/shared/types/user-type.type.ts new file mode 100644 index 0000000..bc8cb16 --- /dev/null +++ b/src/shared/types/user-type.type.ts @@ -0,0 +1 @@ +export type UserType = 'pro' | 'base'; diff --git a/src/shared/types/user.type.ts b/src/shared/types/user.type.ts index f3888a0..27982a7 100644 --- a/src/shared/types/user.type.ts +++ b/src/shared/types/user.type.ts @@ -1,6 +1,9 @@ +import { UserType } from './user-type.type.js'; + export type User = { - name: string; + firstname: string; + lastname: string; email: string; - // avatarPath?: string; - // isPro: boolean; + avatarPath?: string; + type: UserType; }; From 7de081d953fe0f0d038a3a2eade5b418aefa547d Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 29 Mar 2026 17:08:30 +0300 Subject: [PATCH 08/12] adds offer service (entity, dto, container) --- src/main.rest.ts | 9 +- src/rest/index.ts | 1 + .../modules/offer/default-offer.service.ts | 31 +++++++ .../modules/offer/dto/create-offer.dto.ts | 20 +++++ src/shared/modules/offer/index.ts | 5 ++ .../modules/offer/offer-service.interface.ts | 8 ++ src/shared/modules/offer/offer.container.ts | 19 ++++ src/shared/modules/offer/offer.entity.ts | 88 +++++++++++++++++++ src/shared/types/component.enum.ts | 2 + src/shared/types/offer.type.ts | 5 +- 10 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 src/shared/modules/offer/default-offer.service.ts create mode 100644 src/shared/modules/offer/dto/create-offer.dto.ts create mode 100644 src/shared/modules/offer/index.ts create mode 100644 src/shared/modules/offer/offer-service.interface.ts create mode 100644 src/shared/modules/offer/offer.container.ts create mode 100644 src/shared/modules/offer/offer.entity.ts diff --git a/src/main.rest.ts b/src/main.rest.ts index 9430696..b258ea8 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -1,14 +1,19 @@ import 'reflect-metadata'; import { Container } from 'inversify'; -import { RestApplication } from './rest/index.js'; +import { + createRestApplicationContainer, + RestApplication, +} from './rest/index.js'; import { Component } from './shared/types/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'; async function bootstrap() { const appContainer = Container.merge( createRestApplicationContainer(), createUserContainer(), + createOfferContainer(), ); const app = appContainer.get(Component.RestApplication); diff --git a/src/rest/index.ts b/src/rest/index.ts index 5829d8c..2758071 100644 --- a/src/rest/index.ts +++ b/src/rest/index.ts @@ -1 +1,2 @@ export * from './rest.application.js'; +export * from './rest.container.js'; diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts new file mode 100644 index 0000000..bfb50f1 --- /dev/null +++ b/src/shared/modules/offer/default-offer.service.ts @@ -0,0 +1,31 @@ +import { inject, injectable } from 'inversify'; +import { OfferService } from './offer-service.interface.js'; +import { Component } from '../../types/index.js'; +import { Logger } from '../../libs/logger/index.js'; +import { types } from '@typegoose/typegoose'; +import { OfferEntity } from './offer.entity.js'; +import { CreateOfferDTO } from './dto/create-offer.dto.js'; + +@injectable() +export class DefaultOfferService implements OfferService { + constructor( + @inject(Component.Logger) private readonly logger: Logger, + @inject(Component.OfferModel) + private readonly offerModel: types.ModelType, + ) {} + + public async create( + dto: CreateOfferDTO, + ): Promise> { + const result = await this.offerModel.create(dto); + this.logger.info(`New offer created: ${dto.title}`); + + return result; + } + + public async findById( + offerId: string, + ): Promise | null> { + return this.offerModel.findById(offerId); + } +} 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..3bc4e62 --- /dev/null +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -0,0 +1,20 @@ +import { City, Coordinates, Facility, Housing } from '../../../types/index.js'; + +export class CreateOfferDTO { + title: string; + description: string; + publishDate: Date; + city: City; + previewImage: string; + images: string[]; + isPremium: boolean; + isFavorite: boolean; + rating: number; + type: Housing; + rooms: number; + guests: number; + price: number; + facilities: Facility[]; + userId: string; + location: Coordinates; +} diff --git a/src/shared/modules/offer/index.ts b/src/shared/modules/offer/index.ts new file mode 100644 index 0000000..5b7cedc --- /dev/null +++ b/src/shared/modules/offer/index.ts @@ -0,0 +1,5 @@ +export * from './offer.entity.js'; +export * from './dto/create-offer.dto.js'; +export * from './offer-service.interface.js'; +export * from './default-offer.service.js'; +export * 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..8ff01a1 --- /dev/null +++ b/src/shared/modules/offer/offer-service.interface.ts @@ -0,0 +1,8 @@ +import { CreateOfferDTO } from './dto/create-offer.dto.js'; +import { DocumentType } from '@typegoose/typegoose'; +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..e6d03cd --- /dev/null +++ b/src/shared/modules/offer/offer.container.ts @@ -0,0 +1,19 @@ +import { Container } from 'inversify'; +import { OfferService } from './offer-service.interface.js'; +import { Component } from '../../types/index.js'; +import { DefaultOfferService } from './default-offer.service.js'; +import { types } from '@typegoose/typegoose'; +import { OfferEntity, OfferModel } from './offer.entity.js'; + +export function createOfferContainer() { + const offerContainer = new Container(); + + offerContainer + .bind(Component.OfferService) + .to(DefaultOfferService); + offerContainer + .bind>(Component.OfferModel) + .toConstantValue(OfferModel); + + return offerContainer; +} diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts new file mode 100644 index 0000000..853e41d --- /dev/null +++ b/src/shared/modules/offer/offer.entity.ts @@ -0,0 +1,88 @@ +import { + defaultClasses, + getModelForClass, + modelOptions, + prop, + Ref, +} from '@typegoose/typegoose'; +import { + City, + Coordinates, + Facility, + FacilityValues, + Housing, + HousingValues, + Offer, +} 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 implements Offer { + @prop({ trim: true, required: true, minlength: 10, maxlength: 100 }) + public title!: string; + + @prop({ trim: true, required: true, minlength: 20, maxlength: 1024 }) + public description!: string; + + @prop() + public publishDate!: Date; + + @prop({ required: true, type: () => String, enum: City }) + public city!: City; + + @prop() + public previewImage!: string; + + @prop({ required: true, type: () => [String], minlength: 6, maxlength: 6 }) + public images!: string[]; + + @prop({ required: true }) + public isPremium!: boolean; + + @prop({ required: true }) + public isFavorite: boolean; + + @prop({ required: true, min: 0, max: 5 }) + public rating: number; + + @prop({ required: true, type: () => String, enum: HousingValues }) + public type!: Housing; + + @prop({ required: true, min: 1, max: 8 }) + public rooms!: number; + + @prop({ required: true, min: 1, max: 10 }) + public guests!: number; + + @prop({ required: true, min: 100, max: 1000000 }) + public price!: number; + + @prop({ required: true, type: () => [String], enum: FacilityValues }) + public facilities!: Facility[]; + + @prop({ + required: true, + ref: UserEntity, + }) + public userId!: Ref; + + @prop({ + required: true, + type: () => ({ + latitude: Number, + longitude: Number, + }), + }) + public location!: Coordinates; +} + +export const OfferModel = getModelForClass(OfferEntity); diff --git a/src/shared/types/component.enum.ts b/src/shared/types/component.enum.ts index b2bf148..f3c69a6 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -5,4 +5,6 @@ export const Component = { DatabaseClient: Symbol.for('DatabaseClient'), UserServise: Symbol.for('UserService'), UserModel: Symbol.for('UserModel'), + OfferService: Symbol.for('OfferService'), + OfferModel: Symbol.for('OfferModel'), } as const; diff --git a/src/shared/types/offer.type.ts b/src/shared/types/offer.type.ts index ab6608f..73a9d3f 100644 --- a/src/shared/types/offer.type.ts +++ b/src/shared/types/offer.type.ts @@ -1,8 +1,9 @@ +import { Ref } from '@typegoose/typegoose'; import { City } from './city.enum.js'; import { Coordinates } from './coordinates.type.js'; import { Facility } from './facility.type.js'; import { Housing } from './housing.type.js'; -import { User } from './user.type.js'; +import { UserEntity } from '../modules/user/index.js'; export type Offer = { title: string; // 10–100 @@ -19,6 +20,6 @@ export type Offer = { guests: number; // 1–10 price: number; // 100–100000 facilities: Facility[]; - host: User; + userId: Ref; location: Coordinates; }; From aa9fe47ab8561b27db4fb7efa42fa97f5b85469f Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 29 Mar 2026 17:09:37 +0300 Subject: [PATCH 09/12] adds console logger --- src/shared/libs/logger/console.logger.ts | 21 +++++++++++++++++++++ src/shared/libs/logger/index.ts | 1 + 2 files changed, 22 insertions(+) create mode 100644 src/shared/libs/logger/console.logger.ts 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/libs/logger/index.ts b/src/shared/libs/logger/index.ts index c330526..2e1bf8f 100644 --- a/src/shared/libs/logger/index.ts +++ b/src/shared/libs/logger/index.ts @@ -1,2 +1,3 @@ export * from './logger.interface.js'; export * from './pino.logger.js'; +export * from './console.logger.js'; From b8a988ac9445c1aa2a9d764db12e44c7c9bb71bd Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 29 Mar 2026 22:21:05 +0300 Subject: [PATCH 10/12] updates import command --- mocks/mock-data.tsv | 4 +- src/cli/commands/command.constant.ts | 2 + src/cli/commands/import.command.ts | 88 +++++++++++++++++-- src/main.cli.ts | 1 + .../libs/file-reader/tsv-file-reader.ts | 4 +- src/shared/modules/offer/offer.entity.ts | 8 +- src/shared/modules/user/index.ts | 1 + src/shared/modules/user/user.entity.ts | 2 +- src/shared/types/offer.type.ts | 5 +- src/shared/types/user.type.ts | 2 +- 10 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 src/cli/commands/command.constant.ts diff --git a/mocks/mock-data.tsv b/mocks/mock-data.tsv index 81d8a8c..52073e4 100644 --- a/mocks/mock-data.tsv +++ b/mocks/mock-data.tsv @@ -1,2 +1,2 @@ -Modern apartment near Eiffel Tower Spacious and bright apartment located in the heart of Paris, close to major attractions and public transport. Perfect for couples or small families looking for комфортный отдых. 2026-02-15 Paris https://example.com/images/paris-preview.jpg https://example.com/images/paris1.jpg;https://example.com/images/paris2.jpg;https://example.com/images/paris3.jpg;https://example.com/images/paris4.jpg;https://example.com/images/paris5.jpg;https://example.com/images/paris6.jpg true false 4.8 apartment 2 4 1200 Breakfast;Air conditioning;Washer;Towels;Fridge Angela angela@example.com 48.85661;2.351499 -Cozy house with garden in Hamburg Charming family house with private garden situated in a quiet district of Hamburg. Ideal for a relaxing stay with friends or family, featuring modern amenities and comfortable rooms. 2026-02-15 Hamburg https://example.com/images/hamburg-preview.jpg https://example.com/images/hamburg1.jpg;https://example.com/images/hamburg2.jpg;https://example.com/images/hamburg3.jpg;https://example.com/images/hamburg4.jpg;https://example.com/images/hamburg5.jpg;https://example.com/images/hamburg6.jpg false true 4.5 house 4 6 2500 Air conditioning;Laptop friendly workspace;Baby seat;Washer;Towels;Fridge Johnny johnny@example.com 53.550341;10.000654 +Modern apartment near Eiffel Tower Spacious and bright apartment located in the heart of Paris, close to major attractions and public transport. Perfect for couples or small families looking for комфортный отдых. 2026-02-15 Paris https://example.com/images/paris-preview.jpg https://example.com/images/paris1.jpg;https://example.com/images/paris2.jpg;https://example.com/images/paris3.jpg;https://example.com/images/paris4.jpg;https://example.com/images/paris5.jpg;https://example.com/images/paris6.jpg true false 4.8 apartment 2 4 1200 Breakfast;Air conditioning;Washer;Towels;Fridge Angela Nights angela@example.com 48.85661;2.351499 +Cozy house with garden in Hamburg Charming family house with private garden situated in a quiet district of Hamburg. Ideal for a relaxing stay with friends or family, featuring modern amenities and comfortable rooms. 2026-02-15 Hamburg https://example.com/images/hamburg-preview.jpg https://example.com/images/hamburg1.jpg;https://example.com/images/hamburg2.jpg;https://example.com/images/hamburg3.jpg;https://example.com/images/hamburg4.jpg;https://example.com/images/hamburg5.jpg;https://example.com/images/hamburg6.jpg false true 4.5 house 4 6 2500 Air conditioning;Laptop friendly workspace;Baby seat;Washer;Towels;Fridge Johnny Silverhand johnny@example.com 53.550341;10.000654 diff --git a/src/cli/commands/command.constant.ts b/src/cli/commands/command.constant.ts new file mode 100644 index 0000000..c6c2d83 --- /dev/null +++ b/src/cli/commands/command.constant.ts @@ -0,0 +1,2 @@ +export const DEFAULT_DB_PORT = '27017'; +export const DEFAULT_USER_PASSWORD = 'password'; diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index c81a728..3276cc3 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -1,24 +1,102 @@ import { Command } from './command.interface.js'; import { TSVFileReader } from '../../shared/libs/file-reader/index.js'; -import { createOffer, getErrorMessage } from '../../shared/helpers/index.js'; +import { + createOffer, + getErrorMessage, + getMongoURI, +} from '../../shared/helpers/index.js'; import chalk from 'chalk'; +import { + DefaultUserService, + 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 { ConsoleLogger, Logger } from '../../shared/libs/logger/index.js'; +import { Offer } from '../../shared/types/index.js'; +import { DEFAULT_DB_PORT, DEFAULT_USER_PASSWORD } from './command.constant.js'; export class ImportCommand implements Command { + private userService: UserService; + private offerService: OfferService; + private databaseClient: DatabaseClient; + private logger: Logger; + private salt: string; + + constructor() { + this.onImportedLine = this.onImportedLine.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 onImportedLine(line: string) { + private async saveOffer(offer: Offer) { + const user = await this.userService.findOrCreate( + { ...offer.host, password: DEFAULT_USER_PASSWORD }, + this.salt, + ); + + await this.offerService.create({ + userId: user.id, + title: offer.title, + description: offer.description, + publishDate: offer.publishDate, + city: offer.city, + location: offer.location, + previewImage: offer.previewImage, + images: offer.images, + isPremium: offer.isPremium, + isFavorite: offer.isFavorite, + rating: offer.rating, + type: offer.type, + rooms: offer.rooms, + guests: offer.guests, + price: offer.price, + facilities: offer.facilities, + }); + } + + private async onImportedLine(line: string, resolve: () => void) { const offer = createOffer(line); - console.info(offer); + await this.saveOffer(offer); + resolve(); } private onCompleteImport(count: number) { console.info(chalk.bgBlueBright(`${count} rows imported`)); + this.databaseClient.disconnect(); } - public async execute(...args: string[]): Promise { - const [filename] = args; + public async execute( + filename: string, + login: string, + password: string, + host: string, + dbname: string, + salt: string, + ): Promise { + this.logger.info('Starting import'); + + 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.onImportedLine); diff --git a/src/main.cli.ts b/src/main.cli.ts index 42feb84..a9cc06f 100644 --- a/src/main.cli.ts +++ b/src/main.cli.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import 'reflect-metadata'; import { CLIApplication, HelpCommand, diff --git a/src/shared/libs/file-reader/tsv-file-reader.ts b/src/shared/libs/file-reader/tsv-file-reader.ts index b683712..297bf88 100644 --- a/src/shared/libs/file-reader/tsv-file-reader.ts +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -27,7 +27,9 @@ export class TSVFileReader extends EventEmitter implements FileReader { remainingData = remainingData.slice(++nextLinePosition); importedRowCount++; - this.emit('line', completeRow); + await new Promise((resolve) => { + this.emit('line', completeRow, resolve); + }); } } diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index 853e41d..27df199 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -26,7 +26,7 @@ export interface OfferEntity extends defaultClasses.Base {} }, }) // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -export class OfferEntity extends defaultClasses.TimeStamps implements Offer { +export class OfferEntity extends defaultClasses.TimeStamps { @prop({ trim: true, required: true, minlength: 10, maxlength: 100 }) public title!: string; @@ -49,10 +49,10 @@ export class OfferEntity extends defaultClasses.TimeStamps implements Offer { public isPremium!: boolean; @prop({ required: true }) - public isFavorite: boolean; + public isFavorite!: boolean; @prop({ required: true, min: 0, max: 5 }) - public rating: number; + public rating!: number; @prop({ required: true, type: () => String, enum: HousingValues }) public type!: Housing; @@ -70,8 +70,8 @@ export class OfferEntity extends defaultClasses.TimeStamps implements Offer { public facilities!: Facility[]; @prop({ - required: true, ref: UserEntity, + required: true, }) public userId!: Ref; diff --git a/src/shared/modules/user/index.ts b/src/shared/modules/user/index.ts index 652d8f9..db8f005 100644 --- a/src/shared/modules/user/index.ts +++ b/src/shared/modules/user/index.ts @@ -2,3 +2,4 @@ export * from './user.entity.js'; export * from './dto/create-user.dto.js'; export * from './default-user.service.js'; export * from './user.container.js'; +export * from './user-service.interface.js'; diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts index 8636921..19a506e 100644 --- a/src/shared/modules/user/user.entity.ts +++ b/src/shared/modules/user/user.entity.ts @@ -23,7 +23,7 @@ export class UserEntity extends defaultClasses.TimeStamps implements User { public email: string; @prop({ required: false, default: '' }) - public avatarPath?: string; + public avatarPath: string; @prop({ required: true, default: '' }) public firstname: string; diff --git a/src/shared/types/offer.type.ts b/src/shared/types/offer.type.ts index 73a9d3f..ab6608f 100644 --- a/src/shared/types/offer.type.ts +++ b/src/shared/types/offer.type.ts @@ -1,9 +1,8 @@ -import { Ref } from '@typegoose/typegoose'; import { City } from './city.enum.js'; import { Coordinates } from './coordinates.type.js'; import { Facility } from './facility.type.js'; import { Housing } from './housing.type.js'; -import { UserEntity } from '../modules/user/index.js'; +import { User } from './user.type.js'; export type Offer = { title: string; // 10–100 @@ -20,6 +19,6 @@ export type Offer = { guests: number; // 1–10 price: number; // 100–100000 facilities: Facility[]; - userId: Ref; + host: User; location: Coordinates; }; diff --git a/src/shared/types/user.type.ts b/src/shared/types/user.type.ts index 27982a7..4ac97c9 100644 --- a/src/shared/types/user.type.ts +++ b/src/shared/types/user.type.ts @@ -4,6 +4,6 @@ export type User = { firstname: string; lastname: string; email: string; - avatarPath?: string; + avatarPath: string; type: UserType; }; From c700d2be67a3a6f9ad1c84d3c7e4153a3899585e Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 29 Mar 2026 22:38:54 +0300 Subject: [PATCH 11/12] fixes compile errors --- src/shared/helpers/offer.ts | 18 +++++++++++++++--- src/shared/modules/offer/offer.entity.ts | 6 +----- src/shared/types/coordinates.type.ts | 13 +++++++++---- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/shared/helpers/offer.ts b/src/shared/helpers/offer.ts index 3045334..44b44a7 100644 --- a/src/shared/helpers/offer.ts +++ b/src/shared/helpers/offer.ts @@ -1,4 +1,10 @@ -import { City, Offer, parseFacility, parseHousing } from '../types/index.js'; +import { + City, + Offer, + parseFacility, + parseHousing, + UserType, +} from '../types/index.js'; import { booleanFromString } from './types-helpers.js'; export function createOffer(offerData: string): Offer { @@ -17,8 +23,11 @@ export function createOffer(offerData: string): Offer { guests, price, facilities, - name, + firstname, + lastname, email, + avatarPath, + userType, location, ] = offerData.replace('\n', '').split('\t'); @@ -40,8 +49,11 @@ export function createOffer(offerData: string): Offer { .split(';') .map((facility) => parseFacility(facility)), host: { - name, + firstname, + lastname, email, + avatarPath, + type: userType as UserType, }, location: (() => { const [latitude, longitude] = location.split(';'); diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index 27df199..25548f2 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -12,7 +12,6 @@ import { FacilityValues, Housing, HousingValues, - Offer, } from '../../types/index.js'; import { UserEntity } from '../user/index.js'; @@ -77,10 +76,7 @@ export class OfferEntity extends defaultClasses.TimeStamps { @prop({ required: true, - type: () => ({ - latitude: Number, - longitude: Number, - }), + type: Coordinates, }) public location!: Coordinates; } diff --git a/src/shared/types/coordinates.type.ts b/src/shared/types/coordinates.type.ts index e08f618..f52830c 100644 --- a/src/shared/types/coordinates.type.ts +++ b/src/shared/types/coordinates.type.ts @@ -1,4 +1,9 @@ -export type Coordinates = { - latitude: number; - longitude: number; -}; +import { prop } from '@typegoose/typegoose'; + +export class Coordinates { + @prop({ required: true }) + public latitude!: number; + + @prop({ required: true }) + public longitude!: number; +} From 904d49ee454ca6a61ebc17ca79f8980c4421d956 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 29 Mar 2026 22:57:00 +0300 Subject: [PATCH 12/12] fixes import errors --- mocks/mock-data.tsv | 4 ++-- src/cli/commands/import.command.ts | 2 +- src/shared/modules/offer/offer.entity.ts | 2 +- src/shared/modules/user/user.entity.ts | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mocks/mock-data.tsv b/mocks/mock-data.tsv index 52073e4..4819ba4 100644 --- a/mocks/mock-data.tsv +++ b/mocks/mock-data.tsv @@ -1,2 +1,2 @@ -Modern apartment near Eiffel Tower Spacious and bright apartment located in the heart of Paris, close to major attractions and public transport. Perfect for couples or small families looking for комфортный отдых. 2026-02-15 Paris https://example.com/images/paris-preview.jpg https://example.com/images/paris1.jpg;https://example.com/images/paris2.jpg;https://example.com/images/paris3.jpg;https://example.com/images/paris4.jpg;https://example.com/images/paris5.jpg;https://example.com/images/paris6.jpg true false 4.8 apartment 2 4 1200 Breakfast;Air conditioning;Washer;Towels;Fridge Angela Nights angela@example.com 48.85661;2.351499 -Cozy house with garden in Hamburg Charming family house with private garden situated in a quiet district of Hamburg. Ideal for a relaxing stay with friends or family, featuring modern amenities and comfortable rooms. 2026-02-15 Hamburg https://example.com/images/hamburg-preview.jpg https://example.com/images/hamburg1.jpg;https://example.com/images/hamburg2.jpg;https://example.com/images/hamburg3.jpg;https://example.com/images/hamburg4.jpg;https://example.com/images/hamburg5.jpg;https://example.com/images/hamburg6.jpg false true 4.5 house 4 6 2500 Air conditioning;Laptop friendly workspace;Baby seat;Washer;Towels;Fridge Johnny Silverhand johnny@example.com 53.550341;10.000654 +Modern apartment near Eiffel Tower Spacious and bright apartment located in the heart of Paris, close to major attractions and public transport. Perfect for couples or small families looking for комфортный отдых. 2026-02-15 Paris https://example.com/images/paris-preview.jpg https://example.com/images/paris1.jpg;https://example.com/images/paris2.jpg;https://example.com/images/paris3.jpg;https://example.com/images/paris4.jpg;https://example.com/images/paris5.jpg;https://example.com/images/paris6.jpg true false 4.8 apartment 2 4 1200 Breakfast;Air conditioning;Washer;Towels;Fridge Angela Nights angela@example.com https://example.com/images/avatar.jpg base 48.85661;2.351499 +Cozy house with garden in Hamburg Charming family house with private garden situated in a quiet district of Hamburg. Ideal for a relaxing stay with friends or family, featuring modern amenities and comfortable rooms. 2026-02-15 Hamburg https://example.com/images/hamburg-preview.jpg https://example.com/images/hamburg1.jpg;https://example.com/images/hamburg2.jpg;https://example.com/images/hamburg3.jpg;https://example.com/images/hamburg4.jpg;https://example.com/images/hamburg5.jpg;https://example.com/images/hamburg6.jpg false true 4.5 house 4 6 2500 Air conditioning;Laptop friendly workspace;Baby seat;Washer;Towels;Fridge Johnny Silverhand johnny@example.com https://example.com/images/avatar.jpg pro 53.550341;10.000654 diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index 3276cc3..5312489 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -90,7 +90,7 @@ export class ImportCommand implements Command { dbname: string, salt: string, ): Promise { - this.logger.info('Starting import'); + this.logger.info('Starting import...'); const uri = getMongoURI(login, password, host, DEFAULT_DB_PORT, dbname); this.salt = salt; diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index 25548f2..3cc11fb 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -41,7 +41,7 @@ export class OfferEntity extends defaultClasses.TimeStamps { @prop() public previewImage!: string; - @prop({ required: true, type: () => [String], minlength: 6, maxlength: 6 }) + @prop({ required: true, type: () => [String] }) public images!: string[]; @prop({ required: true }) diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts index 19a506e..dedd51f 100644 --- a/src/shared/modules/user/user.entity.ts +++ b/src/shared/modules/user/user.entity.ts @@ -31,7 +31,7 @@ export class UserEntity extends defaultClasses.TimeStamps implements User { @prop({ required: true, default: '' }) public lastname: string; - @prop({ required: true }) + @prop({ required: true, default: 'base' }) public type: UserType; @prop({ required: true, default: '' }) @@ -44,6 +44,7 @@ export class UserEntity extends defaultClasses.TimeStamps implements User { this.avatarPath = userData.avatarPath; this.firstname = userData.firstname; this.lastname = userData.lastname; + this.type = userData.type; } public setPassword(password: string, salt: string) {