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: diff --git a/mocks/mock-data.tsv b/mocks/mock-data.tsv index 81d8a8c..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 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 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/package-lock.json b/package-lock.json index 05a4e46..b98d8d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,18 @@ "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", "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", @@ -255,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": { @@ -388,6 +359,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 +726,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 +796,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", @@ -1168,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": { @@ -1191,6 +1206,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", @@ -1360,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", @@ -2229,10 +2253,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", @@ -2388,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": { @@ -2734,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": { @@ -3159,6 +3186,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 +3257,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 +3311,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 +3426,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", @@ -3731,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" }, @@ -3874,10 +4023,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 +4423,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 +4470,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 +4537,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 +4863,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 +5036,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 +5216,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", @@ -5219,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==", - "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==", + "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/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": { @@ -5321,6 +5486,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 +5737,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 +5797,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", @@ -5841,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", @@ -5859,6 +6058,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", @@ -5970,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" @@ -6603,9 +6807,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": { @@ -6707,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" @@ -6959,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": { @@ -7247,6 +7450,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 +7505,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 +7534,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 +7605,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", @@ -7644,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": { @@ -7748,10 +8007,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 +8257,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 +8288,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 +8333,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 +8563,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 +8678,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 +8798,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..7aca554 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", @@ -40,11 +41,12 @@ }, "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", "tslib": "2.8.1" 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..5312489 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/main.rest.ts b/src/main.rest.ts index 93dd317..b258ea8 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -1,23 +1,22 @@ 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 { + createRestApplicationContainer, + RestApplication, +} from './rest/index.js'; import { Component } from './shared/types/index.js'; +import { createUserContainer } from './shared/modules/user/index.js'; +import { createOfferContainer } from './shared/modules/offer/index.js'; + async function bootstrap() { - const container = new Container(); - container - .bind(Component.RestApplication) - .to(RestApplication) - .inSingletonScope(); - container.bind(Component.Logger).to(PinoLogger).inSingletonScope(); - container - .bind>(Component.Config) - .to(RestConfig) - .inSingletonScope(); + const appContainer = Container.merge( + createRestApplicationContainer(), + createUserContainer(), + createOfferContainer(), + ); - const app = container.get(Component.RestApplication); + const app = appContainer.get(Component.RestApplication); await app.init(); } 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/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/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/database.ts b/src/shared/helpers/database.ts new file mode 100644 index 0000000..f79b628 --- /dev/null +++ b/src/shared/helpers/database.ts @@ -0,0 +1,9 @@ +export function getMongoURI( + username: string, + password: string, + host: string, + port: string, + databaseName: string, +): string { + return `mongodb://${username}:${password}@${host}:${port}/${databaseName}?authSource=admin`; +} diff --git a/src/shared/helpers/hash.ts b/src/shared/helpers/hash.ts new file mode 100644 index 0000000..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 404daf1..b2ba0c4 100644 --- a/src/shared/helpers/index.ts +++ b/src/shared/helpers/index.ts @@ -3,3 +3,5 @@ export * from './mock-helpers.js'; 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/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/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index 2fda343..13e83a0 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -3,10 +3,28 @@ 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; DB_HOST: string; + DB_USER: string; + DB_PASSWORD: string; + DB_PORT: string; + DB_NAME: string; }; export const configRestSchema = convict({ @@ -24,8 +42,32 @@ 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', }, + 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: '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..a8a8449 --- /dev/null +++ b/src/shared/libs/database-client/mongo.database-client.ts @@ -0,0 +1,65 @@ +import * as Mongoose from 'mongoose'; +import { inject, injectable } from 'inversify'; +import { setTimeout } from 'node:timers/promises'; +import { DatabaseClient } from './database-client.interface.js'; +import { 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; + 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...'); + + let attempt = 0; + + while (attempt < RETRY_COUNT) { + try { + this.mongoose = await Mongoose.connect(uri); + this.isConnected = true; + + this.logger.info('Database connection established.'); + + return; + } catch (error) { + attempt++; + this.logger.error( + `Failed to connect to the database. Attempt ${attempt}`, + error as Error, + ); + await setTimeout(RETRY_TIMEOUT); + } + } + + throw new Error( + `Unable to establish database connection after ${RETRY_COUNT} tries`, + ); + } + + public async disconnect(): Promise { + if (!this.isConnectedToDatabase()) { + throw new Error('Not connected to the database'); + } + + await this.mongoose.disconnect?.(); + this.isConnected = false; + this.logger.info('Database connection closed.'); + } +} diff --git a/src/shared/libs/file-reader/tsv-file-reader.ts b/src/shared/libs/file-reader/tsv-file-reader.ts index 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/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'; 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..3cc11fb --- /dev/null +++ b/src/shared/modules/offer/offer.entity.ts @@ -0,0 +1,84 @@ +import { + defaultClasses, + getModelForClass, + modelOptions, + prop, + Ref, +} from '@typegoose/typegoose'; +import { + City, + Coordinates, + Facility, + FacilityValues, + Housing, + HousingValues, +} from '../../types/index.js'; +import { UserEntity } from '../user/index.js'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface OfferEntity extends defaultClasses.Base {} + +@modelOptions({ + schemaOptions: { + collection: 'offers', + timestamps: true, + }, +}) +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class OfferEntity extends defaultClasses.TimeStamps { + @prop({ trim: true, required: true, 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] }) + 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({ + ref: UserEntity, + required: true, + }) + public userId!: Ref; + + @prop({ + required: true, + type: Coordinates, + }) + public location!: Coordinates; +} + +export const OfferModel = getModelForClass(OfferEntity); diff --git a/src/shared/modules/user/default-user.service.ts b/src/shared/modules/user/default-user.service.ts new file mode 100644 index 0000000..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..db8f005 --- /dev/null +++ b/src/shared/modules/user/index.ts @@ -0,0 +1,5 @@ +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-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..dedd51f --- /dev/null +++ b/src/shared/modules/user/user.entity.ts @@ -0,0 +1,59 @@ +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, default: 'base' }) + 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; + this.type = userData.type; + } + + public setPassword(password: string, salt: string) { + this.password = createSHA256(password, salt); + } + + public getPassword() { + return this.password; + } +} + +export const UserModel = getModelForClass(UserEntity); diff --git a/src/shared/modules/user/user.model.ts b/src/shared/modules/user/user.model.ts new file mode 100644 index 0000000..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); diff --git a/src/shared/types/component.enum.ts b/src/shared/types/component.enum.ts index c7a25ed..f3c69a6 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -2,4 +2,9 @@ export const Component = { RestApplication: Symbol.for('RestApplication'), Logger: Symbol.for('Logger'), Config: Symbol.for('Config'), + 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/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; +} 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..4ac97c9 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; };