diff --git a/.gitignore b/.gitignore index c6bba59..9c508c3 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +/venv-podman diff --git a/README.md b/README.md index 720bded..947daac 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # node-mentoring-shop -https://d17btkcdsmqrmh.cloudfront.net/node-gmp/docs/express-layered-architecture/task +[https://d17btkcdsmqrmh.cloudfront.net/node-gmp/docs/nosql/homework](https://d17btkcdsmqrmh.cloudfront.net/node-gmp/docs/nosql/homework) We are going to create an Express application for online shop which sells different types of products (like e.g Amazon). -In this task we are going to implement functionality for managing carts, creating orders and products. +In this task we will modify the application we created in [Express and Layered Architecture](https://github.com/kandalova/node-mentoring-shop/pull/1) module by moving data storage to NoSQL database. The application has 4 primary entities: @@ -27,15 +27,41 @@ One User can have multiple Order. Each Orderxxx.repository.ts contains functions to retrieve data (data access layer), xxx.service.ts contains services that implement business logic, xxx.controller.ts contains functions that manage status codes/responses returned (presentation layer). -- Data is stored either in memory or on file system. -- joi is used to validate data in PUT /api/profile/cart. -- Simple authentication middleware is added to check if user with such id exists. User id is passed in x-user-id header. -- Order entity has copy of products. If you have only product id in order, it may lead to inconsistency. For example, if user creates an order and after that price is changed, the order price shouldn't be changed. -- For ```DELETE``` ```/api/profile/cart``` soft-delete approach is be used. Make sure that client of your API will not know that soft-delete approach is used. +- Data is stored in `MongoDB` database. +- `Docker` image is used for local development (check Running [MongoDB as a Docker Container](https://www.baeldung.com/linux/mongodb-as-docker-container#2-building-container-using-a-compose-file) for an example of docker-compose file). +- `Mongoose` is used as ODM for querying. +- `Data Access Layer` is rewritten to query `MongoDB`. +- Models are created based on entity schemas used in Express and Layered Architecture module. +- Models have proper relations between each other based on information specified above. **API endpoints** [swagger](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/swagger.md) image + +**Setup for startring (once)** + +In the project root folder + +- Install [podman](https://podman.io/docs/installation) +- Install [podman-compose](https://github.com/containers/podman-compose#installation) +- Init podman machine `podman machine init` +- Install [MongoDB server](https://www.mongodb.com/try/download/community) +- Install MongoDB tools to connect to MongoDB: [Shell](https://www.mongodb.com/try/download/shell) or [Compass Gui](https://www.mongodb.com/try/download/compass) + +**To start(every time)** +- `podman machine start` - start virtual machine +- `podman-compose up -d` - starts container from `docker-compose.yaml` +- `podman ps` - check running containers +- Connect to Mongo via Compass or shell `mongosh --port 27017` + +[Running MongoDB as a Docker Container](https://www.baeldung.com/linux/mongodb-as-docker-container#2-building-container-using-a-compose-file) + +**First time starting** +- Create your first database in MongoDB. Then, create your first collection (Example - User). + +**Results** + +![image](https://github.com/kandalova/node-mentoring-shop/assets/26093763/9e525a2f-c86b-4ecd-9f4e-5f8fe95a6904) + diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..c33fb20 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3' +services: + mongo: + ports: + - '27017:27017' + container_name: mongo + restart: always + logging: + options: + max-size: 1g + environment: + - MONGO_INITDB_ROOT_USERNAME=mongoadmin + - MONGO_INITDB_ROOT_PASSWORD=123admin + image: mongo diff --git a/package-lock.json b/package-lock.json index 9de0178..abf2a2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,15 @@ "express": "^4.18.2", "joi": "^17.11.0", "lodash.clonedeep": "^4.5.0", + "mongodb": "^6.2.0", + "mongoose": "^7.6.4", "uuid": "^9.0.1" }, "devDependencies": { "@types/express": "^4.17.18", "@types/lodash.clonedeep": "^4.5.7", - "@types/node": "^20.8.2", + "@types/mongoose": "^5.11.97", + "@types/node": "^20.8.10", "@types/uuid": "^9.0.4", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", @@ -221,6 +224,14 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.1.tgz", + "integrity": "sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==", + "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", @@ -374,11 +385,23 @@ "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==", "dev": true }, + "node_modules/@types/mongoose": { + "version": "5.11.97", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.97.tgz", + "integrity": "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==", + "deprecated": "Mongoose publishes its own types, so you do not need to install this package.", + "dev": true, + "dependencies": { + "mongoose": "*" + } + }, "node_modules/@types/node": { - "version": "20.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", - "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", - "dev": true + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/qs": { "version": "6.9.8", @@ -425,6 +448,20 @@ "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==", "dev": true }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.2.tgz", + "integrity": "sha512-uNv6b/uGRLlCVmelat2rA8bcVd3k/42mV2EmjhPh6JLkd35T5bgwR/t6xy7a9MWhd9sixIeBUzhBenvk3NO+DQ==" + }, + "node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.7.4", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz", @@ -906,6 +943,14 @@ "node": ">=8" } }, + "node_modules/bson": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.2.0.tgz", + "integrity": "sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1789,6 +1834,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1896,6 +1946,14 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/kareem": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -1970,6 +2028,11 @@ "node": ">= 0.6" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -2047,6 +2110,169 @@ "node": "*" } }, + "node_modules/mongodb": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.2.0.tgz", + "integrity": "sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^6.2.0", + "mongodb-connection-string-url": "^2.6.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongoose": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.6.4.tgz", + "integrity": "sha512-kadPkS/f5iZJrrMxxOvSoOAErXmdnb28lMvHmuYgmV1ZQTpRqpp132PIPHkJMbG4OC2H0eSXYw/fNzYTH+LUcw==", + "dependencies": { + "bson": "^5.5.0", + "kareem": "2.5.1", + "mongodb": "5.9.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "16.0.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.0.tgz", + "integrity": "sha512-g+GCMHN1CoRUA+wb1Agv0TI4YTSiWr42B5ulkiAfLLHitGK1R+PkSAf3Lr5rPZwi/3F04LiaZEW0Kxro9Fi2TA==", + "dependencies": { + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2297,7 +2523,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, "engines": { "node": ">=6" } @@ -2555,6 +2780,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -2576,6 +2806,36 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2658,6 +2918,17 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -2768,6 +3039,11 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2819,6 +3095,26 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 59441cb..c1c88b9 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "devDependencies": { "@types/express": "^4.17.18", "@types/lodash.clonedeep": "^4.5.7", - "@types/node": "^20.8.2", + "@types/mongoose": "^5.11.97", + "@types/node": "^20.8.10", "@types/uuid": "^9.0.4", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", @@ -27,6 +28,8 @@ "express": "^4.18.2", "joi": "^17.11.0", "lodash.clonedeep": "^4.5.0", + "mongodb": "^6.2.0", + "mongoose": "^7.6.4", "uuid": "^9.0.1" } } diff --git a/src/controller/cart.controller.ts b/src/controller/cart.controller.ts index 13ab135..a6bd7b7 100644 --- a/src/controller/cart.controller.ts +++ b/src/controller/cart.controller.ts @@ -2,13 +2,12 @@ import express, { NextFunction, Request, Response } from "express"; import { createOrGetCart, createOrder, deleteCart, updateCart } from "../service/cart.service"; import Joi from "joi"; import { getPutSchemeError } from "../utils/errors"; -// import { promiseHandler } from "../utils/utils"; const cartRouter = express.Router(); cartRouter.use(express.json()); const putSchema = Joi.object({ - productId: Joi.string().uuid().required(), + productId: Joi.string().required(), count: Joi.number().integer().min(0).max(20).required() }) @@ -20,25 +19,17 @@ const putValidator = async (req: Request, _: Response, next: NextFunction) => { catch (err) { next(getPutSchemeError()); } - } -// cartRouter.post("/", async (req:Request, res:Response, next:NextFunction) => { -// const userId = req.header('x-user-id')!; -// const cart = await promiseHandler(createCart(userId), next); -// res.status(201).send(cart); -// }); - cartRouter.get("/", async (req: Request, res: Response, next: NextFunction) => { try { const userId = req.header('x-user-id')!; - const cart = await createOrGetCart(userId, true); + const cart = await createOrGetCart(userId); res.send(cart); } catch (err) { next(err); } - }); cartRouter.delete("/", async (req: Request, res: Response, next: NextFunction) => { diff --git a/src/index.ts b/src/index.ts index 4071da3..06157ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,23 +1,36 @@ import express, { NextFunction, Request, Response } from "express"; -import { findUser } from "./repository/user.repository.ts"; import cartRouter from "./controller/cart.controller.ts"; import productRouter from "./controller/product.controller.ts"; import { ResponseError, getForbidenError, getUnauthorizedError } from "./utils/errors.ts"; import { getResponseError } from "./utils/utils.ts"; +import { connect, disconnect } from "mongoose"; +import { UserModel } from "./scheme/UserScheme.ts"; + +//for seeders +// import { runUserSeeder } from "./seeder/userSeeder.ts"; +// import { ProductModel } from "./scheme/ProductScheme.ts"; +// import { runProductSeeder } from "./seeder/productSeeder.ts"; +//for cart check +// import { CartModel } from "./scheme/CartScheme.ts"; export const app = express(); const PORT = 3000; const headerHandler = async (req: Request, _: Response, next: NextFunction) => { - const userId = req.header('x-user-id') ?? ''; - if (!userId) { - next(getForbidenError()); + try { + const userId = req.header('x-user-id') ?? ''; + if (!userId) { + next(getForbidenError()); + } + const user = await UserModel.findById(userId); + if (!user) { + next(getUnauthorizedError()); + } + next(); } - const user = await findUser(userId); - if (!user) { - next(getUnauthorizedError()); + catch (err) { + next(err); } - next(); } const errorHandler = async (err: Error | ResponseError, _req: Request, res: Response, _next: NextFunction) => { @@ -33,7 +46,34 @@ const errorHandler = async (err: Error | ResponseError, _req: Request, res: Resp app.use('/api/profile/cart', headerHandler, cartRouter, errorHandler); app.use('/api/products', headerHandler, productRouter, errorHandler); +export const init = (async () => { + try { + await connect("mongodb://127.0.0.1:27017/NM_SHOP"); + app.listen(PORT, () => { + console.log(`Server is Fire at http://localhost:${PORT}`); + }); + //mock data for first run + // const user = await UserModel.findOne(); + // if (!user) { + // await runUserSeeder(); + // } + // const product = await ProductModel.findOne(); + // if (!product) { + // await runProductSeeder(); + // } + //check cart + // const doc = new CartModel({items: [{ count: 5 }]}); + // console.log('doc', doc); + // console.log(doc.items[0].ownerDocument()===doc); + } + catch (err) { + console.log(err); + } +})(); -app.listen(PORT, () => { - console.log(`Server is Fire at http://localhost:${PORT}`); +// listen (ctrl-c) +process.on("SIGINT", async () => { + await disconnect(); + console.log("Server is disconnected"); + process.exit(); }); diff --git a/src/repository/cart.repository.ts b/src/repository/cart.repository.ts index 7218331..4dafc3c 100644 --- a/src/repository/cart.repository.ts +++ b/src/repository/cart.repository.ts @@ -1,5 +1,28 @@ import { cartDB } from "../db/db"; -import { CartEditableProperties, ICart, ICartItem } from "../scheme/CartScheme"; +import { IPopulatedCartItem } from "../scheme/CartScheme"; +import { getUUID } from "../utils/utils"; + +interface ICart { + id: string; // uuid + userId: string; + isDeleted: boolean; + items: IPopulatedCartItem[]; +} + +enum CartEditableProperties { + Items = 'items', + IsDeleted = "isDeleted" +} + +export const generateCart = (userId: string): ICart => { + const newCart: ICart = { + id: getUUID(), + userId: userId, + isDeleted: false, + items: [], + } + return newCart; +} export const pushCart = async (cart: ICart) => { await cartDB.push(cart); @@ -20,7 +43,7 @@ export const findUserCartIdxByID = async (userId: string, cartId: string) => { return index; } -export const updateCartProperty = async (cartIdx: number, property: CartEditableProperties, value: boolean | ICartItem[]) => { +export const updateCartProperty = async (cartIdx: number, property: CartEditableProperties, value: boolean | IPopulatedCartItem[]) => { cartDB[cartIdx][property] = value; } @@ -28,11 +51,11 @@ export const getCartByIdx = async (cartIdx: number): Promise => { return await cartDB[cartIdx]; } export const getCartProductIdx = async (cartIdx: number, productId: string) => { - const index = await cartDB[cartIdx].items.findIndex((item: ICartItem) => item.product.id === productId); + const index = await cartDB[cartIdx].items.findIndex((item: IPopulatedCartItem) => item.product.product === productId); return index; } -export const pushCartItem = async (cartIdx: number, product: ICartItem) => { +export const pushCartItem = async (cartIdx: number, product: IPopulatedCartItem) => { await cartDB[cartIdx].items.push(product) } diff --git a/src/repository/order.repository.ts b/src/repository/order.repository.ts index ea45c75..950dc29 100644 --- a/src/repository/order.repository.ts +++ b/src/repository/order.repository.ts @@ -1,11 +1,32 @@ import { orderDB } from "../db/db"; -import { IOrder } from "../scheme/OrderScheme"; +import { IPopulatedCartItem } from "../scheme/CartScheme"; -export const pushOrder = async (order: IOrder): Promise => { +type ORDER_STATUS = 'created' | 'completed'; + +interface IPopulatedOrder { + id: string, + userId: string; + cartId: string; + items: IPopulatedCartItem[] + payment: { + type: string, + address?: unknown, + creditCard?: unknown, + }, + delivery: { + type: string, + address: unknown, + }, + comments: string, + status: ORDER_STATUS; + total: number; +} + +export const pushOrder = async (order: IPopulatedOrder): Promise => { await orderDB.push(order); return orderDB.length - 1; } -export const getOrderByIdx = async (idx: number): Promise => { +export const getOrderByIdx = async (idx: number): Promise => { return await orderDB[idx]; } diff --git a/src/scheme/CartScheme.ts b/src/scheme/CartScheme.ts index 77035bf..714b80e 100644 --- a/src/scheme/CartScheme.ts +++ b/src/scheme/CartScheme.ts @@ -1,15 +1,20 @@ -import { IProduct } from "./ProductScheme"; +import { Schema, Types, model } from "mongoose"; import { IResponse } from "./ServiceUtils"; +import { IProduct } from "./ProductScheme"; export interface ICart { - id: string; // uuid - userId: string; + _id: Types.ObjectId; + user: Types.ObjectId; isDeleted: boolean; - items: ICartItem[]; + items: Types.DocumentArray; } +export type PopulatedCart = Omit & { + items: IPopulatedCartItem[]; +}; + export interface ICartItem { - product: IProduct, + product: Types.ObjectId, count: number, } @@ -18,13 +23,26 @@ export interface ICartItemByID { count: number, } -export type OmitCart = Omit; - -export enum CartEditableProperties { - Items = 'items', - IsDeleted = "isDeleted" +export interface IPopulatedCartItem { + product: IProduct, + count: number, } +const cartItemSchema = new Schema({ + product: { type: Schema.Types.ObjectId, ref: 'Product', required: true }, + count: { type: Number, required: true }, +}, { versionKey: false }); + +const cartSchema = new Schema({ + user: { type: Schema.Types.ObjectId, ref: 'User', required: true, }, + isDeleted: { type: Boolean, default: false, required: true }, + items: [cartItemSchema], +}, { versionKey: false }); + +export const CartModel = model('Cart', cartSchema); + +export type OmitCart = Pick; + export interface ICartResponse extends IResponse { data: { cart: OmitCart, diff --git a/src/scheme/OrderScheme.ts b/src/scheme/OrderScheme.ts index e07b3bc..9e6ebfa 100644 --- a/src/scheme/OrderScheme.ts +++ b/src/scheme/OrderScheme.ts @@ -1,12 +1,10 @@ -import { ICartItem } from "./CartScheme"; - -type ORDER_STATUS = 'created' | 'completed'; +import { Schema, Types, model } from "mongoose"; +import { IPopulatedCartItem } from "./CartScheme"; export interface IOrder { - id: string, - userId: string; - cartId: string; - items: ICartItem[] + user: Types.ObjectId; + cart: Types.ObjectId; + items: IPopulatedCartItem[]; payment: { type: string, address?: unknown, @@ -17,14 +15,40 @@ export interface IOrder { address: unknown, }, comments: string, - status: ORDER_STATUS; + status: OrderStatuses; total: number; } - -export type IOrderInfo = Pick; - export enum OrderStatuses { Created = 'created', Complited = "completed" } + +const orderSchema = new Schema({ + user: { type: Schema.Types.ObjectId, ref: 'User', required: true }, + cart: { type: Schema.Types.ObjectId, ref: 'Cart', required: true }, + items: { + type: [{ + title: { type: String, required: true }, + description: { type: String, required: true }, + price: { type: Number, required: true }, + product: { type: String, required: true } + }], + }, + payment: { + type: { type: String, required: true }, + address: Schema.Types.Mixed, + creditCard: Schema.Types.Mixed, + }, + delivery: { + type: { type: String, required: true }, + address: { type: Schema.Types.Mixed, required: true }, + }, + comments: { type: String }, + status: { type: String, enum: Object.values(OrderStatuses), required: true, default: OrderStatuses.Created }, + total: { type: Number, required: true }, +}, { versionKey: false }); + +export const OrderModel = model('Order', orderSchema); + +export type IOrderInfo = Pick; diff --git a/src/scheme/ProductScheme.ts b/src/scheme/ProductScheme.ts index d834db1..8b3954b 100644 --- a/src/scheme/ProductScheme.ts +++ b/src/scheme/ProductScheme.ts @@ -1,12 +1,23 @@ +import { Schema, Types, model } from "mongoose"; import { IResponse } from "./ServiceUtils"; export interface IProduct { - id: string; + _id: Types.ObjectId; title: string; description: string; price: number; + product?: string; } +const productSchema = new Schema({ + title: { type: String, required: true }, + description: { type: String, required: true }, + price: { type: Number, required: true }, + product: String, +}, { versionKey: false }); + +export const ProductModel = model("Product", productSchema); + export interface IProductResponse extends IResponse { data: IProduct, } diff --git a/src/scheme/UserScheme.ts b/src/scheme/UserScheme.ts index 0fe48b5..d3b8c38 100644 --- a/src/scheme/UserScheme.ts +++ b/src/scheme/UserScheme.ts @@ -1,3 +1,11 @@ +import { Schema, model } from "mongoose"; + export interface IUser { - id: string, + name: string, } + +const userSchema = new Schema({ + name: { type: String, required: true }, +}, { versionKey: false }) + +export const UserModel = model("User", userSchema); diff --git a/src/seeder/productSeeder.ts b/src/seeder/productSeeder.ts new file mode 100644 index 0000000..6f4d101 --- /dev/null +++ b/src/seeder/productSeeder.ts @@ -0,0 +1,9 @@ +import { productDB } from "../db/db"; +import { ProductModel } from "../scheme/ProductScheme"; + +export const runProductSeeder = async () => { + productDB.forEach(async ({ title, description, price }) => { + const newProduct = new ProductModel({ title, description, price }); + await newProduct.save(); + }); +} diff --git a/src/seeder/userSeeder.ts b/src/seeder/userSeeder.ts new file mode 100644 index 0000000..a8c3548 --- /dev/null +++ b/src/seeder/userSeeder.ts @@ -0,0 +1,9 @@ +import { userDB } from "../db/db" +import { UserModel } from "../scheme/UserScheme"; + +export const runUserSeeder = async () => { + userDB.forEach(async (value, i) => { + const newUser = new UserModel({ name: `User-${i}` }); + await newUser.save(); + }); +} diff --git a/src/service/cart.service.ts b/src/service/cart.service.ts index 0beec0e..b0567a6 100644 --- a/src/service/cart.service.ts +++ b/src/service/cart.service.ts @@ -1,79 +1,69 @@ -import { deleteCartItem, findUserCart, findUserCartIdx, getCartByIdx, getCartProductIdx, pushCart, pushCartItem, updateCartItemCount, updateCartProperty } from "../repository/cart.repository"; -import { pushOrder } from "../repository/order.repository"; -import { findProduct } from "../repository/product.repository"; -import { CartEditableProperties, ICartItemByID, ICartResponse, IDeleteCartResponse } from "../scheme/CartScheme"; -import { IOrderInfo } from "../scheme/OrderScheme"; -import { generateCart, generateOrder, getCartResponse, getDeleteCartResponse } from "../utils/cartUtils"; -import { throwCartExistsError, throwEmptyCart, throwNoCartExists, throwNoProductExists } from "../utils/errors"; +import { CartModel, IPopulatedCartItem, ICartItemByID, ICartResponse, IDeleteCartResponse } from "../scheme/CartScheme"; +import { IOrderInfo, OrderModel } from "../scheme/OrderScheme"; +import { ProductModel } from "../scheme/ProductScheme"; +import { generateOrderDTO, getCartResponse, getDeleteCartResponse, getPopulatedCartResponse } from "../utils/cartUtils"; +import { throwEmptyCart, throwNoCartExists, throwNoProductExists } from "../utils/errors"; +const findActiveUserCart = async (userId: string) => { + return await CartModel.findOne({ user: userId, isDeleted: false }).populate<{ items: IPopulatedCartItem[] }>('items.product'); +} +const findActiveUserCartOrFail = async (userId: string) => { + return await CartModel.findOne({ user: userId, isDeleted: false }).orFail(() => throwNoCartExists(userId)); +} +const findUserCartWithProductOrFail = async (userId: string) => { + return await CartModel + .findOne({ user: userId, isDeleted: false }).populate<{ items: IPopulatedCartItem[] }>('items.product') + .orFail(() => throwNoCartExists(userId)); +} -export const createOrGetCart = async (userId: string, isGetOrCreate = false): Promise => { - const userCart = await findUserCart(userId); - if (userCart && isGetOrCreate) { - return getCartResponse(userCart); - } - if (userCart && !isGetOrCreate) { - throwCartExistsError(userId); +export const createOrGetCart = async (userId: string): Promise => { + const userCart = await findActiveUserCart(userId); + if (userCart) { + return getPopulatedCartResponse(userCart); } - const newCart = generateCart(userId); - await pushCart(newCart); + const newCart = await CartModel.create({ user: userId }); return getCartResponse(newCart); }; export const deleteCart = async (userId: string): Promise => { - const cartIdx = await findUserCartIdx(userId); - if (cartIdx === -1) { - throwNoCartExists(userId); - } - await updateCartProperty(cartIdx, CartEditableProperties.IsDeleted, true); + const userCart = await findActiveUserCartOrFail(userId); + userCart.isDeleted = true; + await userCart.save(); const response = getDeleteCartResponse(); return response; } -export const postCart = async (userId: string): Promise => { - return createOrGetCart(userId); -}; - -export const getCart = async (userId: string): Promise => { - return createOrGetCart(userId, true); -} - export const updateCart = async (userId: string, { productId, count }: ICartItemByID): Promise => { - const cartIdx = await findUserCartIdx(userId); - if (cartIdx === -1) { - throwEmptyCart(userId); - } - const productIdx = await getCartProductIdx(cartIdx, productId); - if (count > 0 && productIdx === -1) { - const product = await findProduct(productId); + const userCart = await findActiveUserCartOrFail(userId); + const indexToUpdate = userCart.items.findIndex(item => item.product.toString() === productId); + + if (indexToUpdate === -1 && count > 0) { + const product = await ProductModel.findById(productId); if (!product) { throwNoProductExists(); } - else { - await pushCartItem(cartIdx, { product, count }) - } + userCart.items.push({ product: productId, count: count }); } - if (count > 0 && productIdx >= 0) { - await updateCartItemCount(cartIdx, productIdx, count); + if (indexToUpdate >= 0 && count > 0) { + userCart.items[indexToUpdate].count = count; } - if (count === 0 && productIdx >= 0) { - await deleteCartItem(cartIdx, productIdx); + if (indexToUpdate >= 0 && count === 0) { + userCart.items.splice(indexToUpdate, 1); } - return await getCartResponse(await getCartByIdx(cartIdx)); + await userCart.save(); + const updatedCart = await findUserCartWithProductOrFail(userId); + return getPopulatedCartResponse(updatedCart); } export const createOrder = async (userId: string, orderInfo: IOrderInfo) => { - const cartIdx = await findUserCartIdx(userId); - if (cartIdx === -1) { - throwEmptyCart(userId); - } - const cart = await getCartByIdx(cartIdx); - if (cart.items.length === 0) { + const userCart = await findUserCartWithProductOrFail(userId); + if (userCart.items.length === 0) { throwEmptyCart(userId); } - const newOrder = generateOrder(cart, orderInfo); - await pushOrder(newOrder); - await updateCartProperty(cartIdx, CartEditableProperties.IsDeleted, true); + const newOrderDTO = generateOrderDTO(userCart, orderInfo); + const newOrder = await OrderModel.create(newOrderDTO); + userCart.isDeleted = true; + await userCart.save(); return newOrder; } diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 3b7708a..9f1d42d 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -1,10 +1,9 @@ -import { findProduct, findProducts } from "../repository/product.repository"; -import { IProductResponse, IProductsResponse } from "../scheme/ProductScheme"; +import { IProductResponse, IProductsResponse, ProductModel } from "../scheme/ProductScheme"; import { throwNoProductExists } from "../utils/errors"; import { getProductResponse, getProductsResponse } from "../utils/productUtils"; export const getProduct = async (productId: string): Promise => { - const product = await findProduct(productId); + const product = await ProductModel.findById(productId); if (!product) { throwNoProductExists(); } @@ -14,6 +13,6 @@ export const getProduct = async (productId: string): Promise => { - const products = await findProducts(); + const products = await ProductModel.find(); return getProductsResponse(products); }; diff --git a/src/utils/cartUtils.ts b/src/utils/cartUtils.ts index 75839e5..d66e008 100644 --- a/src/utils/cartUtils.ts +++ b/src/utils/cartUtils.ts @@ -1,43 +1,38 @@ -import { ICart, ICartItem, ICartResponse, IDeleteCartResponse, OmitCart } from "../scheme/CartScheme"; -import { IOrderInfo, IOrder, OrderStatuses } from "../scheme/OrderScheme"; -import { createDeepCopy, getUUID } from "./utils"; +import { IPopulatedCartItem, ICartResponse, ICart, IDeleteCartResponse, OmitCart, PopulatedCart } from "../scheme/CartScheme"; +import { IOrderInfo } from "../scheme/OrderScheme"; -export const generateCart = (userId: string): ICart => { - const newCart: ICart = { - id: getUUID(), - userId: userId, - isDeleted: false, - items: [], - } - return newCart; -} - -export const getOmitCart = ({ id, items }: ICart): OmitCart => { - return { id, items }; +export const getOmitCart = ({ _id, items }: PopulatedCart | ICart): OmitCart => { + return { _id, items }; } -export const getTotalPrice = (items: ICartItem[]): number => { +export const getTotalPrice = (items: IPopulatedCartItem[]): number => { let totalPrice = 0; items.map((item) => { - const price = item.product.price; + const price = item.product.price totalPrice += price * item.count; }); return totalPrice; } -export const getCartResponse = (cart: ICart): ICartResponse => { +export const getCartResponse = (cart: PopulatedCart | ICart): ICartResponse => { const omitCart = getOmitCart(cart); - const totalPrice = getTotalPrice(cart.items); const response: ICartResponse = { data: { cart: omitCart, - totalPrice: totalPrice + totalPrice: 0 }, error: null, } return response; } +export const getPopulatedCartResponse = (cart: PopulatedCart): ICartResponse => { + const response = getCartResponse(cart); + const totalPrice = getTotalPrice(cart.items); + response.data.totalPrice = totalPrice; + return response; +} + export const getDeleteCartResponse = (): IDeleteCartResponse => { const response: IDeleteCartResponse = { data: { @@ -48,12 +43,16 @@ export const getDeleteCartResponse = (): IDeleteCartResponse => { return response; } -export const generateOrder = (cart: ICart, orderInfo: IOrderInfo): IOrder => { - const newOrder: IOrder = { - id: getUUID(), - userId: cart.userId, - cartId: cart.id, - items: createDeepCopy(cart.items), +export const generateOrderDTO = (cart: PopulatedCart, orderInfo: IOrderInfo) => { + const newOrderDTO = { + user: cart.user, + cart: cart._id, + items: cart.items.map(item => ({ + product: item.product._id.toString(), + title: item.product.title, + description: item.product.description, + price: item.product.price, + })), payment: { type: orderInfo.payment.type, address: orderInfo.payment.address, @@ -64,8 +63,8 @@ export const generateOrder = (cart: ICart, orderInfo: IOrderInfo): IOrder => { address: orderInfo.delivery.address, }, comments: orderInfo.comments, - status: OrderStatuses.Created, total: 0, } - return newOrder; + newOrderDTO.total = getTotalPrice(cart.items); + return newOrderDTO; } diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 620d5d4..9832f11 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -10,13 +10,11 @@ interface IResponseError extends Error { status: number; } - const getResposeError = (message: string, status: number): ResponseError => { const err = new ResponseError(message, status); return err; } - export const throwNoUser = (id: string) => { throw new Error(`No user ${id}`); } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 7d7e611..5662690 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -11,7 +11,6 @@ export const createDeepCopy = (arr: T[]): T[] => { return cloneDeep(arr); } - export const promiseHandler = async (prom: Promise, next: NextFunction) => { try { return await prom;