diff --git a/.eslintrc.js b/.eslintrc.js index 25ca5b9..b6a36b6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,5 +30,7 @@ module.exports = { "@typescript-eslint" ], "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], } } diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9e34be8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "command": "npm start", + "name": "Run npm start", + "request": "launch", + "type": "node-terminal" + }, + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}\\src\\index.ts", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f6ef976 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.formatOnSave": true, + "editor.fontSize": 15, + "workbench.colorTheme": "Default Dark Modern", + "files.autoSave": "afterDelay" +} diff --git a/README.md b/README.md index 7bf1c65..720bded 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,41 @@ # node-mentoring-shop +https://d17btkcdsmqrmh.cloudfront.net/node-gmp/docs/express-layered-architecture/task 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. + +The application has 4 primary entities: + +User - can add some products to the cart and then order them ([scheme](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/src/scheme/UserScheme.ts)). + +Product - represents product information that user can order ([scheme](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/src/scheme/ProductScheme.ts)). + +Cart - contains a list of products and their amount that user wants to order ([scheme](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/src/scheme/CartScheme.ts)). + +Order - contains list of products from cart that user has ordered ([scheme](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/src/scheme/OrderScheme.ts)). + +**Relations between entities:** + +Each User can have only one non-deleted Cart at a time. +Each Cart is attached to a specific User. + +One User can have multiple Order. Each Order is attached to a specific User. + +Cart contains a list of products that user wants to order with the amount of those products specified. + +**Implementation criteria:** + +- TypeScript is used. +- Application is implemented following Three Layered Architecture. Layers are separated by file names. For example xxx.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. + +**API endpoints** + +[swagger](https://github.com/kandalova/node-mentoring-shop/blob/task_6_shop_express.js/swagger.md) + +image diff --git a/package-lock.json b/package-lock.json index d3b94ea..9de0178 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,17 @@ "name": "node-mentoring-shop", "version": "1.0.0", "license": "ISC", + "dependencies": { + "express": "^4.18.2", + "joi": "^17.11.0", + "lodash.clonedeep": "^4.5.0", + "uuid": "^9.0.1" + }, "devDependencies": { + "@types/express": "^4.17.18", + "@types/lodash.clonedeep": "^4.5.7", "@types/node": "^20.8.2", + "@types/uuid": "^9.0.4", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "eslint": "^8.50.0", @@ -118,6 +127,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -234,6 +256,24 @@ "node": ">= 8" } }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -258,24 +298,133 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/body-parser": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", + "integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", + "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.37", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz", + "integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", + "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.13", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.199", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", + "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==", + "dev": true + }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.7.tgz", + "integrity": "sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", + "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==", + "dev": true + }, "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 }, + "node_modules/@types/qs": { + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz", + "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz", + "integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", "dev": true }, + "node_modules/@types/send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz", + "integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz", + "integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.4.tgz", + "integrity": "sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==", + "dev": true + }, "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", @@ -563,6 +712,18 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -658,6 +819,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -682,6 +848,42 @@ "node": ">=8" } }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -704,6 +906,26 @@ "node": ">=8" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -801,6 +1023,38 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -836,6 +1090,23 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -869,6 +1140,24 @@ "node": ">=6.0.0" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1057,6 +1346,68 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1124,6 +1475,36 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1160,6 +1541,22 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1180,6 +1577,25 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1253,6 +1669,14 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1262,6 +1686,54 @@ "node": ">=4" } }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1315,8 +1787,15 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -1375,6 +1854,18 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/joi": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", + "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1442,6 +1933,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1466,6 +1962,19 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1475,6 +1984,14 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -1488,6 +2005,36 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1503,8 +2050,7 @@ "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==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -1512,6 +2058,14 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nodemon": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", @@ -1564,6 +2118,25 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1632,6 +2205,14 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1659,6 +2240,11 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -1689,6 +2275,18 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -1704,6 +2302,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -1724,6 +2336,28 @@ } ] }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1793,6 +2427,30 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -1808,6 +2466,61 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1829,6 +2542,19 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1850,6 +2576,14 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -1904,6 +2638,14 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -1995,6 +2737,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -2014,6 +2768,14 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2023,12 +2785,40 @@ "punycode": "^2.1.0" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, "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 b3d0f38..59441cb 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,21 @@ "author": "", "license": "ISC", "devDependencies": { + "@types/express": "^4.17.18", + "@types/lodash.clonedeep": "^4.5.7", "@types/node": "^20.8.2", + "@types/uuid": "^9.0.4", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "eslint": "^8.50.0", "nodemon": "^3.0.1", "ts-node": "^10.9.1", "typescript": "^5.2.2" + }, + "dependencies": { + "express": "^4.18.2", + "joi": "^17.11.0", + "lodash.clonedeep": "^4.5.0", + "uuid": "^9.0.1" } } diff --git a/src/controller/cart.controller.ts b/src/controller/cart.controller.ts new file mode 100644 index 0000000..13ab135 --- /dev/null +++ b/src/controller/cart.controller.ts @@ -0,0 +1,77 @@ +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(), + count: Joi.number().integer().min(0).max(20).required() +}) + +const putValidator = async (req: Request, _: Response, next: NextFunction) => { + try { + await putSchema.validateAsync(req.body); + next(); + } + 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); + res.send(cart); + } + catch (err) { + next(err); + } + +}); + +cartRouter.delete("/", async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = req.header('x-user-id')!; + const data = await deleteCart(userId); + res.send(data); + } + catch (err) { + next(err); + } +}); + +cartRouter.post("/checkout", async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = req.header('x-user-id')!; + const order = await createOrder(userId, req.body); + res.send(order); + } + catch (err) { + next(err); + } +}); + +cartRouter.put("/", putValidator, async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = req.header('x-user-id')!; + const order = await updateCart(userId, req.body); + res.send(order); + } + catch (err) { + next(err); + } +}); + +export default cartRouter; diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts new file mode 100644 index 0000000..84c8c13 --- /dev/null +++ b/src/controller/product.controller.ts @@ -0,0 +1,28 @@ +import express, { NextFunction, Request, Response } from "express"; +import { getProduct, getProducts } from "../service/product.service"; + +const productRouter = express.Router(); +productRouter.use(express.json()); + +productRouter.get("/", async (req: Request, res: Response, next: NextFunction) => { + try { + const products = await getProducts(); + res.send(products); + } + catch (err) { + next(err); + } +}); + +productRouter.get("/:id", async (req: Request, res: Response, next: NextFunction) => { + try { + const productId = req.params.id; + const products = await getProduct(productId); + res.send(products); + } + catch (err) { + next(err); + } +}); + +export default productRouter; diff --git a/src/db/cart.db.js b/src/db/cart.db.js new file mode 100644 index 0000000..263d7f4 --- /dev/null +++ b/src/db/cart.db.js @@ -0,0 +1 @@ +export const carts = []; diff --git a/src/db/db.js b/src/db/db.js new file mode 100644 index 0000000..9f039a3 --- /dev/null +++ b/src/db/db.js @@ -0,0 +1,9 @@ +import { products } from "./product.db.js" +import { carts } from "./cart.db.js" +import { orders } from "./order.db.js" +import { users } from "./user.db.js"; + +export const userDB = users; +export const productDB = products; +export const cartDB = carts; +export const orderDB = orders; diff --git a/src/db/order.db.js b/src/db/order.db.js new file mode 100644 index 0000000..abdacfb --- /dev/null +++ b/src/db/order.db.js @@ -0,0 +1 @@ +export const orders=[]; diff --git a/src/db/product.db.js b/src/db/product.db.js new file mode 100644 index 0000000..20e4e18 --- /dev/null +++ b/src/db/product.db.js @@ -0,0 +1,38 @@ +export const products = [ + { + id: '32c985f8-ba49-42d1-aa7a-5f0ba24d0354', + title: 'PUMA', + description: ' Carina Street Mid PUMA Black', + price: 203.4, + }, + { + id: '2f592419-bb89-475e-b3fe-c7ecadaf2856', + title: 'Reebok', + description: 'REEBOK COURT ADVANCE', + price: 202.4, + }, + { + id: 'b7924f9e-98c1-463f-8dfd-677c1bd22bd6', + title: 'adidas Originals', + description: 'TREZIOD 2', + price: 305, + }, + { + id: '2a9d4f30-93df-4f86-aa10-5a45aece7ebf', + title: 'adidas Originals', + description: 'GAZELLE', + price: 139, + }, + { + id: '38ecc31a-bce8-46f7-bdfd-3a320bd3c157', + title: 'Reebok', + description: 'SMASH EDGE S', + price: 139, + }, + { + id: 'fb0bc7a1-a731-4a2e-ad12-2478a9e3fd82', + title: 'Vans', + description: 'UA SK8-HI MTE-1', + price: 173.9, + } +] diff --git a/src/db/user.db.js b/src/db/user.db.js new file mode 100644 index 0000000..558bc33 --- /dev/null +++ b/src/db/user.db.js @@ -0,0 +1,6 @@ +export const users = [ + '99c27b1a-0ae2-486d-9d72-9c82579cf361', + 'ed3d57d6-c699-4909-910a-364745332f88', + 'e73b79aa-9cb0-4406-97fd-df231243beec', + 'cf35167a-574d-49e3-a0ed-affd8d746d6c', +] diff --git a/src/index.ts b/src/index.ts index b9d3e23..4071da3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,39 @@ -console.log('Hello world!'); +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"; + +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()); + } + const user = await findUser(userId); + if (!user) { + next(getUnauthorizedError()); + } + next(); +} + +const errorHandler = async (err: Error | ResponseError, _req: Request, res: Response, _next: NextFunction) => { + if (err instanceof ResponseError) { + res.status(err.status); + } + else { + res.status(500); + } + res.send(await getResponseError(err.message)); +} + +app.use('/api/profile/cart', headerHandler, cartRouter, errorHandler); +app.use('/api/products', headerHandler, productRouter, errorHandler); + + +app.listen(PORT, () => { + console.log(`Server is Fire at http://localhost:${PORT}`); +}); diff --git a/src/repository/cart.repository.ts b/src/repository/cart.repository.ts new file mode 100644 index 0000000..7218331 --- /dev/null +++ b/src/repository/cart.repository.ts @@ -0,0 +1,47 @@ +import { cartDB } from "../db/db"; +import { CartEditableProperties, ICart, ICartItem } from "../scheme/CartScheme"; + +export const pushCart = async (cart: ICart) => { + await cartDB.push(cart); +} + +export const findUserCart = async (userId: string): Promise => { + const cart = await cartDB.find((cart) => cart.userId === userId && !cart.isDeleted); + return cart; +} + +export const findUserCartIdx = async (userId: string) => { + const index = await cartDB.findIndex((cart) => cart.userId === userId && !cart.isDeleted); + return index; +} + +export const findUserCartIdxByID = async (userId: string, cartId: string) => { + const index = await cartDB.findIndex((cart) => cart.userId === userId && cart.id === cartId && !cart.isDeleted); + return index; +} + +export const updateCartProperty = async (cartIdx: number, property: CartEditableProperties, value: boolean | ICartItem[]) => { + cartDB[cartIdx][property] = value; +} + +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); + return index; +} + +export const pushCartItem = async (cartIdx: number, product: ICartItem) => { + await cartDB[cartIdx].items.push(product) +} + +export const updateCartItemCount = async (cartIdx: number, productIdx: number, count: number) => { + const cartItem = cartDB[cartIdx].items[productIdx]; + cartItem.count = count; +} + +export const deleteCartItem = async (cartIdx: number, productIdx: number) => { + const cartItems = cartDB[cartIdx].items; + await cartItems.splice(productIdx, 1); +} diff --git a/src/repository/order.repository.ts b/src/repository/order.repository.ts new file mode 100644 index 0000000..ea45c75 --- /dev/null +++ b/src/repository/order.repository.ts @@ -0,0 +1,11 @@ +import { orderDB } from "../db/db"; +import { IOrder } from "../scheme/OrderScheme"; + +export const pushOrder = async (order: IOrder): Promise => { + await orderDB.push(order); + return orderDB.length - 1; +} + +export const getOrderByIdx = async (idx: number): Promise => { + return await orderDB[idx]; +} diff --git a/src/repository/product.repository.ts b/src/repository/product.repository.ts new file mode 100644 index 0000000..32ff596 --- /dev/null +++ b/src/repository/product.repository.ts @@ -0,0 +1,11 @@ +import { productDB } from "../db/db"; +import { IProduct } from "../scheme/ProductScheme"; + +export const findProduct = async (id: string): Promise => { + const product = await productDB.find((product) => product.id === id); + return product; +} + +export const findProducts = async (): Promise => { + return productDB; +} diff --git a/src/repository/user.repository.ts b/src/repository/user.repository.ts new file mode 100644 index 0000000..6651535 --- /dev/null +++ b/src/repository/user.repository.ts @@ -0,0 +1,5 @@ +import { userDB } from "../db/db"; + +export const findUser = async (userId: string) => { + return await userDB.find((id) => id === userId); +} diff --git a/src/scheme/CartScheme.ts b/src/scheme/CartScheme.ts new file mode 100644 index 0000000..77035bf --- /dev/null +++ b/src/scheme/CartScheme.ts @@ -0,0 +1,39 @@ +import { IProduct } from "./ProductScheme"; +import { IResponse } from "./ServiceUtils"; + +export interface ICart { + id: string; // uuid + userId: string; + isDeleted: boolean; + items: ICartItem[]; +} + +export interface ICartItem { + product: IProduct, + count: number, +} + +export interface ICartItemByID { + productId: string, + count: number, +} + +export type OmitCart = Omit; + +export enum CartEditableProperties { + Items = 'items', + IsDeleted = "isDeleted" +} + +export interface ICartResponse extends IResponse { + data: { + cart: OmitCart, + totalPrice: number, + }, +} + +export interface IDeleteCartResponse extends IResponse { + data: { + success: boolean + }, +} diff --git a/src/scheme/OrderScheme.ts b/src/scheme/OrderScheme.ts new file mode 100644 index 0000000..e07b3bc --- /dev/null +++ b/src/scheme/OrderScheme.ts @@ -0,0 +1,30 @@ +import { ICartItem } from "./CartScheme"; + +type ORDER_STATUS = 'created' | 'completed'; + +export interface IOrder { + id: string, + userId: string; + cartId: string; + items: ICartItem[] + payment: { + type: string, + address?: unknown, + creditCard?: unknown, + }, + delivery: { + type: string, + address: unknown, + }, + comments: string, + status: ORDER_STATUS; + total: number; +} + + +export type IOrderInfo = Pick; + +export enum OrderStatuses { + Created = 'created', + Complited = "completed" +} diff --git a/src/scheme/ProductScheme.ts b/src/scheme/ProductScheme.ts new file mode 100644 index 0000000..d834db1 --- /dev/null +++ b/src/scheme/ProductScheme.ts @@ -0,0 +1,16 @@ +import { IResponse } from "./ServiceUtils"; + +export interface IProduct { + id: string; + title: string; + description: string; + price: number; +} + +export interface IProductResponse extends IResponse { + data: IProduct, +} + +export interface IProductsResponse extends IResponse { + data: IProduct[], +} diff --git a/src/scheme/ServiceUtils.ts b/src/scheme/ServiceUtils.ts new file mode 100644 index 0000000..5eef1bd --- /dev/null +++ b/src/scheme/ServiceUtils.ts @@ -0,0 +1,12 @@ +export interface IResponseError { + data: null, + error: { + message: string + }, +} + +export interface IResponse { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any, + error: null, +} diff --git a/src/scheme/UserScheme.ts b/src/scheme/UserScheme.ts new file mode 100644 index 0000000..0fe48b5 --- /dev/null +++ b/src/scheme/UserScheme.ts @@ -0,0 +1,3 @@ +export interface IUser { + id: string, +} diff --git a/src/service/cart.service.ts b/src/service/cart.service.ts new file mode 100644 index 0000000..0beec0e --- /dev/null +++ b/src/service/cart.service.ts @@ -0,0 +1,79 @@ +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"; + + + +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); + } + const newCart = generateCart(userId); + await pushCart(newCart); + 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 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); + if (!product) { + throwNoProductExists(); + } + else { + await pushCartItem(cartIdx, { product, count }) + } + } + if (count > 0 && productIdx >= 0) { + await updateCartItemCount(cartIdx, productIdx, count); + } + if (count === 0 && productIdx >= 0) { + await deleteCartItem(cartIdx, productIdx); + } + return await getCartResponse(await getCartByIdx(cartIdx)); +} + +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) { + throwEmptyCart(userId); + } + const newOrder = generateOrder(cart, orderInfo); + await pushOrder(newOrder); + await updateCartProperty(cartIdx, CartEditableProperties.IsDeleted, true); + return newOrder; +} diff --git a/src/service/product.service.ts b/src/service/product.service.ts new file mode 100644 index 0000000..3b7708a --- /dev/null +++ b/src/service/product.service.ts @@ -0,0 +1,19 @@ +import { findProduct, findProducts } from "../repository/product.repository"; +import { IProductResponse, IProductsResponse } 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); + if (!product) { + throwNoProductExists(); + } + else { + return getProductResponse(product); + } +}; + +export const getProducts = async (): Promise => { + const products = await findProducts(); + return getProductsResponse(products); +}; diff --git a/src/utils/cartUtils.ts b/src/utils/cartUtils.ts new file mode 100644 index 0000000..75839e5 --- /dev/null +++ b/src/utils/cartUtils.ts @@ -0,0 +1,71 @@ +import { ICart, ICartItem, ICartResponse, IDeleteCartResponse, OmitCart } from "../scheme/CartScheme"; +import { IOrderInfo, IOrder, OrderStatuses } from "../scheme/OrderScheme"; +import { createDeepCopy, getUUID } from "./utils"; + +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 getTotalPrice = (items: ICartItem[]): number => { + let totalPrice = 0; + items.map((item) => { + const price = item.product.price; + totalPrice += price * item.count; + }); + return totalPrice; +} + +export const getCartResponse = (cart: ICart): ICartResponse => { + const omitCart = getOmitCart(cart); + const totalPrice = getTotalPrice(cart.items); + const response: ICartResponse = { + data: { + cart: omitCart, + totalPrice: totalPrice + }, + error: null, + } + return response; +} + +export const getDeleteCartResponse = (): IDeleteCartResponse => { + const response: IDeleteCartResponse = { + data: { + success: true, + }, + error: null, + } + 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), + payment: { + type: orderInfo.payment.type, + address: orderInfo.payment.address, + creditCard: orderInfo.payment.creditCard, + }, + delivery: { + type: orderInfo.delivery.type, + address: orderInfo.delivery.address, + }, + comments: orderInfo.comments, + status: OrderStatuses.Created, + total: 0, + } + return newOrder; +} diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..620d5d4 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,62 @@ +export class ResponseError extends Error implements IResponseError { + status: number; + constructor(message: string, status: number) { + super(message); + this.status = status; + } +} + +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}`); +} + +export const throwCartExistsError = (id: string): ResponseError => { + throw new ResponseError(`Cart for user ${id} already exists`, 400); +} + +export const throwNoCartExistsById = (id: string, cartId: string) => { + throw new Error(`No cart ${cartId} for user ${id}`); +} + +export const throwNoCartExists = (id: string) => { + throw new ResponseError(`No cart for user ${id}`, 400); +} + +export const getNoCartExists = (id: string): ResponseError => { + return new ResponseError(`No cart for user ${id}`, 400); +} + +export const throwNoProductExists = () => { + throw new ResponseError(`No product for user`, 400); +} + +export const throwEmptyCart = (id: string) => { + throw new Error(`Cart for user ${id} is empty`); +} + +export const throwUnauthorized = (status = 401) => { + throw getResposeError(`Header x-user-id is missing or no user with such id`, status) +} + +export const getUnauthorizedError = (status = 401) => { + return getResposeError(`Header x-user-id is missing or no user with such id`, status) +} + +export const getForbidenError = (status = 403) => { + return getResposeError(`You must be authorized user`, status) +} + +export const getPutSchemeError = (status = 400) => { + return getResposeError(`Bad request body`, status) +} diff --git a/src/utils/productUtils.ts b/src/utils/productUtils.ts new file mode 100644 index 0000000..efc8800 --- /dev/null +++ b/src/utils/productUtils.ts @@ -0,0 +1,17 @@ +import { IProduct, IProductResponse, IProductsResponse } from "../scheme/ProductScheme"; + +export const getProductResponse = (product: IProduct): IProductResponse => { + const response: IProductResponse = { + data: product, + error: null, + } + return response; +} + +export const getProductsResponse = (products: IProduct[]): IProductsResponse => { + const response: IProductsResponse = { + data: products, + error: null, + } + return response; +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..7d7e611 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,37 @@ +import { v4 as uuid } from 'uuid'; +import cloneDeep from 'lodash.clonedeep'; +import { NextFunction } from 'express'; +import { IResponse, IResponseError } from '../scheme/ServiceUtils'; + +export const getUUID = () => { + return uuid(); +}; + +export const createDeepCopy = (arr: T[]): T[] => { + return cloneDeep(arr); +} + + +export const promiseHandler = async (prom: Promise, next: NextFunction) => { + try { + return await prom; + } catch (error) { + next(error); + } +} + +export const getResponseError = async (message: string): Promise => { + return { + data: null, + error: { + message: message + } + } +} + +export const getResponse = (): IResponse => { + return { + data: "", + error: null + } +} diff --git a/swagger.md b/swagger.md new file mode 100644 index 0000000..6e1231d --- /dev/null +++ b/swagger.md @@ -0,0 +1,1106 @@ +--- +title: API v1.0.0 +language_tabs: + - http: http + - HTTP: HTTP +language_clients: + - http: "" + - HTTP: "" +toc_footers: [] +includes: [] +search: false +highlight_theme: darkula +headingLevel: 2 + +--- + + + +

null v1.0.0

+ +> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. + +

cart

+ +## get__api_profile_cart + +> Code samples + +```http +GET /api/profile/cart HTTP/1.1 + +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +```http +GET /api/profile/cart HTTP/1.1 + +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +`GET /api/profile/cart` + +*Get user cart or create if it is missing* + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|x-user-id|header|string(uuid)|true|User id (uuid)| + +> Example responses + +> 200 Response + +```json +{ + "data": { + "cart": { + "id": "eb5a26af-6e4c-4f31-a9b1-3450d42ac66c", + "items": [ + { + "product": { + "id": "891389f0-4312-42d6-a650-6fda0959c734", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + "count": 2 + } + ] + }, + "total": 400 + }, + "error": null +} +``` + +> 401 Response + +```json +{ + "data": null, + "error": { + "message": "User is not authorized" + } +} +``` + +> 403 Response + +```json +{ + "data": null, + "error": { + "message": "You must be authorized user" + } +} +``` + +> 500 Response + +```json +{ + "data": null, + "error": { + "message": "Internal Server error" + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Returns user cart|[CartResponse](#schemacartresponse)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized (when no user matching authorization header is found)|[CartResponse](#schemacartresponse)| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden (when authorization header is missing)|[CartResponse](#schemacartresponse)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error|[CartResponse](#schemacartresponse)| + + + +## put__api_profile_cart + +> Code samples + +```http +PUT /api/profile/cart HTTP/1.1 + +Content-Type: application/json +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +```http +PUT /api/profile/cart HTTP/1.1 + +Content-Type: application/json +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +`PUT /api/profile/cart` + +*Update user cart* + +> Body parameter + +```json +{ + "productId": "915b2f40-9fd9-47f2-9b51-628f3dc69aac", + "count": 5 +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|x-user-id|header|string(uuid)|true|User id (uuid)| +|body|body|[UpdateCartRequestBody](#schemaupdatecartrequestbody)|false|none| + +> Example responses + +> 200 Response + +```json +{ + "data": { + "cart": { + "id": "eb5a26af-6e4c-4f31-a9b1-3450d42ac66c", + "items": [ + { + "product": { + "id": "891389f0-4312-42d6-a650-6fda0959c734", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + "count": 2 + } + ] + }, + "total": 400 + }, + "error": null +} +``` + +> 400 Response + +```json +{ + "data": null, + "error": { + "message": "Products are not valid" + } +} +``` + +> 401 Response + +```json +{ + "data": null, + "error": { + "message": "User is not authorized" + } +} +``` + +> 403 Response + +```json +{ + "data": null, + "error": { + "message": "You must be authorized user" + } +} +``` + +> 404 Response + +```json +{ + "data": null, + "error": { + "message": "Cart was not found" + } +} +``` + +> 500 Response + +```json +{ + "data": null, + "error": { + "message": "Internal Server error" + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Cart can be updated in the following ways - 1) products can be added 2) products can be dropped 3) amount of product might have changed. Request body to be provided contains a snapshot of cart after changes.|[CartResponse](#schemacartresponse)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request|[CartResponse](#schemacartresponse)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized (when no user matching authorization header is found)|[CartResponse](#schemacartresponse)| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden (when authorization header is missing)|[CartResponse](#schemacartresponse)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found|[CartResponse](#schemacartresponse)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error|[CartResponse](#schemacartresponse)| + + + +## delete__api_profile_cart + +> Code samples + +```http +DELETE /api/profile/cart HTTP/1.1 + +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +```http +DELETE /api/profile/cart HTTP/1.1 + +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +`DELETE /api/profile/cart` + +*Empty user cart* + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|x-user-id|header|string(uuid)|true|User id (uuid)| + +> Example responses + +> 200 Response + +```json +{ + "data": { + "success": true + }, + "error": null +} +``` + +> 401 Response + +```json +{ + "data": null, + "error": { + "message": "User is not authorized" + } +} +``` + +> 403 Response + +```json +{ + "data": null, + "error": { + "message": "You must be authorized user" + } +} +``` + +> 500 Response + +```json +{ + "data": null, + "error": { + "message": "Internal Server error" + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Returns success = true if cart was successfully emptied|[EmptySuccessResponse](#schemaemptysuccessresponse)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized (when no user matching authorization header is found)|[CartResponse](#schemacartresponse)| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden (when authorization header is missing)|[CartResponse](#schemacartresponse)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error|[CartResponse](#schemacartresponse)| + + + +## post__api_profile_cart_checkout + +> Code samples + +```http +POST /api/profile/cart/checkout HTTP/1.1 + +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +```http +POST /api/profile/cart/checkout HTTP/1.1 + +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +`POST /api/profile/cart/checkout` + +*Create an order* + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|x-user-id|header|string(uuid)|true|User id (uuid)| + +> Example responses + +> 200 Response + +```json +{ + "data": { + "order": { + "id": "6c36d6fa-f694-4f9c-9b2c-6f7049d38f4a", + "userId": "6dc52b3c-de7e-431a-84b8-0ec56e0774d4", + "cartId": "cadff0c5-0079-4db8-b6bf-84c9c2633ca3", + "items": [ + { + "product": { + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + "count": 2 + }, + { + "product": { + "id": "afdd68c4-d359-45e6-b9fd-c8fdb2a162a0", + "title": "Pen", + "description": "Cute pen", + "price": 20 + }, + "count": 5 + } + ], + "payment": { + "type": "paypal", + "address": "London", + "creditCard": "1234-1234-1234-1234" + }, + "delivery": { + "type": "post", + "address": "London" + }, + "comments": "", + "status": "created", + "total": 500 + } + }, + "error": null +} +``` + +> 400 Response + +```json +{ + "data": null, + "error": { + "message": "Cart is empty" + } +} +``` + +> 401 Response + +```json +{ + "data": null, + "error": { + "message": "User is not authorized" + } +} +``` + +> 403 Response + +```json +{ + "data": null, + "error": { + "message": "You must be authorized user" + } +} +``` + +> 500 Response + +```json +{ + "data": null, + "error": { + "message": "Internal Server error" + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful operation|[CheckoutResponse](#schemacheckoutresponse)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request|[CartResponse](#schemacartresponse)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized (when no user matching authorization header is found)|[CartResponse](#schemacartresponse)| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden (when authorization header is missing)|[CartResponse](#schemacartresponse)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error|[CartResponse](#schemacartresponse)| + + + +

product

+ +## get__api_products + +> Code samples + +```http +GET /api/products HTTP/1.1 + +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +```http +GET /api/products HTTP/1.1 + +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +`GET /api/products` + +*Returns a list of products* + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|x-user-id|header|string(uuid)|true|User id (uuid)| + +> Example responses + +> 200 Response + +```json +{ + "data": [ + { + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + { + "id": "afdd68c4-d359-45e6-b9fd-c8fdb2a162a0", + "title": "Pen", + "description": "Cute pen", + "price": 20 + } + ], + "error": null +} +``` + +> 401 Response + +```json +{ + "data": null, + "error": { + "message": "User is not authorized" + } +} +``` + +> 403 Response + +```json +{ + "data": null, + "error": { + "message": "You must be authorized user" + } +} +``` + +> 500 Response + +```json +{ + "data": null, + "error": { + "message": "Internal Server error" + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Returns a list of all products available|Inline| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized (when no user matching authorization header is found)|[CartResponse](#schemacartresponse)| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden (when authorization header is missing)|[CartResponse](#schemacartresponse)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error|[CartResponse](#schemacartresponse)| + +

Response Schema

+ +Status Code **200** + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[ProductsResponse](#schemaproductsresponse)]|false|none|none| +|» data|[[Product](#schemaproduct)]|false|none|none| +|»» id|string|false|none|Product id (uuid)| +|»» title|string|false|none|Product name| +|»» description|string|false|none|Product description| +|»» price|number|false|none|Product price| +|» error|[ErrorResponse](#schemaerrorresponse)¦null|false|none|none| +|»» message|string|false|none|none| + + + +## get__api_products_{productId} + +> Code samples + +```http +GET /api/products/{productId} HTTP/1.1 + +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +```http +GET /api/products/{productId} HTTP/1.1 + +Accept: application/json +x-user-id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + +``` + +`GET /api/products/{productId}` + +*Returns single product* + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|productId|path|number|true|Id (uuid) of product to return| +|x-user-id|header|string(uuid)|true|User id (uuid)| + +> Example responses + +> 200 Response + +```json +{ + "data": { + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + "error": null +} +``` + +> 401 Response + +```json +{ + "data": null, + "error": { + "message": "User is not authorized" + } +} +``` + +> 403 Response + +```json +{ + "data": null, + "error": { + "message": "You must be authorized user" + } +} +``` + +> 404 Response + +```json +{ + "data": null, + "error": { + "message": "No product with such id" + } +} +``` + +> 500 Response + +```json +{ + "data": null, + "error": { + "message": "Internal Server error" + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful operation|[ProductResponse](#schemaproductresponse)| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized (when no user matching authorization header is found)|[CartResponse](#schemacartresponse)| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Forbidden (when authorization header is missing)|[CartResponse](#schemacartresponse)| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|[CartResponse](#schemacartresponse)| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal server error|[CartResponse](#schemacartresponse)| + + + +# Schemas + +

Cart

+ + + + + + +```json +{ + "id": "dd5ec5ab-deaa-419c-8a6b-7e67b1f7ec87", + "items": [ + { + "product": { + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + "count": 2 + } + ] +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|string|false|none|Cart id (uuid)| +|items|[[CartItem](#schemacartitem)]|false|none|Items added to cart| + +

CartItem

+ + + + + + +```json +{ + "product": { + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + "count": 2 +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|product|[Product](#schemaproduct)|false|none|none| +|count|number|false|none|Total count of specific products| + +

Product

+ + + + + + +```json +{ + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|string|false|none|Product id (uuid)| +|title|string|false|none|Product name| +|description|string|false|none|Product description| +|price|number|false|none|Product price| + +

Order

+ + + + + + +```json +{ + "id": "string", + "userId": "string", + "cartId": "string", + "items": [ + { + "product": { + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + "count": 2 + } + ], + "payment": { + "type": "string", + "address": "string", + "creditCard": "string" + }, + "delivery": { + "type": "string", + "address": "string" + }, + "comments": "string", + "status": "string", + "total": 0 +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|string|false|none|none| +|userId|string|false|none|none| +|cartId|string|false|none|none| +|items|[[CartItem](#schemacartitem)]|false|none|none| +|payment|object|false|none|none| +|» type|string|false|none|none| +|» address|string|false|none|none| +|» creditCard|string|false|none|none| +|delivery|object|false|none|none| +|» type|string|false|none|none| +|» address|string|false|none|none| +|comments|string|false|none|none| +|status|string|false|none|none| +|total|number|false|none|none| + +

CartResponse

+ + + + + + +```json +{ + "data": { + "cart": { + "id": "dd5ec5ab-deaa-419c-8a6b-7e67b1f7ec87", + "items": [ + { + "product": { + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + "count": 2 + } + ] + }, + "total": 0 + }, + "error": { + "message": "string" + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|data|object|false|none|none| +|» cart|[Cart](#schemacart)|false|none|none| +|» total|number|false|none|none| +|error|[ErrorResponse](#schemaerrorresponse)|false|none|none| + +

CheckoutResponse

+ + + + + + +```json +{ + "data": { + "order": { + "id": "string", + "userId": "string", + "cartId": "string", + "items": [ + { + "product": { + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + "count": 2 + } + ], + "payment": { + "type": "string", + "address": "string", + "creditCard": "string" + }, + "delivery": { + "type": "string", + "address": "string" + }, + "comments": "string", + "status": "string", + "total": 0 + } + }, + "error": { + "message": "string" + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|data|object|false|none|none| +|» order|[Order](#schemaorder)|false|none|none| +|error|[ErrorResponse](#schemaerrorresponse)|false|none|none| + +

ProductResponse

+ + + + + + +```json +{ + "data": { + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 + }, + "error": { + "message": "string" + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|data|[Product](#schemaproduct)|false|none|none| +|error|[ErrorResponse](#schemaerrorresponse)|false|none|none| + +

ProductsResponse

+ + + + + + +```json +{ + "data": [ + { + "id": "5c293ad0-19d0-41ee-baa3-4c648f9f7697", + "title": "Book", + "description": "Interesting book", + "price": 200 + } + ], + "error": { + "message": "string" + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|data|[[Product](#schemaproduct)]|false|none|none| +|error|[ErrorResponse](#schemaerrorresponse)|false|none|none| + +

UpdateCartRequestBody

+ + + + + + +```json +{ + "productId": "915b2f40-9fd9-47f2-9b51-628f3dc69aac", + "count": 5 +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|productId|string|false|none|none| +|count|number|false|none|none| + +

UserCredentials

+ + + + + + +```json +{ + "email": "ann.jones@gmail.com", + "password": "DDQldls?kdpw0fk" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|email|string|false|none|none| +|password|string|false|none|none| + +

EmptySuccessResponse

+ + + + + + +```json +{ + "data": { + "success": true + }, + "error": { + "message": "string" + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|data|object|false|none|none| +|» success|boolean|false|none|none| +|error|[ErrorResponse](#schemaerrorresponse)|false|none|none| + +

ErrorResponse

+ + + + + + +```json +{ + "message": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|message|string|false|none|none| + diff --git a/swagger.yaml b/swagger.yaml new file mode 100644 index 0000000..d0906d7 --- /dev/null +++ b/swagger.yaml @@ -0,0 +1,592 @@ +openapi: 3.0.3 +info: + title: + version: 1.0.0 +tags: + - name: cart + - name: product +paths: + /api/profile/cart: + get: + tags: + - cart + summary: Get user cart or create if it is missing + parameters: + - in: header + name: x-user-id + schema: + type: string + format: uuid + required: true + description: User id (uuid) + example: 'eb5a26af-6e4c-4f31-a9b1-3450d42ac66c' + responses: + '200': + description: Returns user cart + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: + cart: + id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + items: + - product: + id: 891389f0-4312-42d6-a650-6fda0959c734 + title: Book + description: Interesting book + price: 200 + count: 2 + total: 400 + error: null + '403': + description: Forbidden (when authorization header is missing) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: You must be authorized user + '401': + description: Unauthorized (when no user matching authorization header is found) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: User is not authorized + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: 'Internal Server error' + put: + tags: + - cart + summary: Update user cart + parameters: + - in: header + name: x-user-id + schema: + type: string + format: uuid + required: true + description: User id (uuid) + example: 'eb5a26af-6e4c-4f31-a9b1-3450d42ac66c' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateCartRequestBody' + responses: + '200': + description: Cart can be updated in the following ways - 1) products can be added 2) products can be dropped 3) amount of product might have changed. Request body to be provided contains a snapshot of cart after changes. + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: + cart: + id: eb5a26af-6e4c-4f31-a9b1-3450d42ac66c + items: + - product: + id: 891389f0-4312-42d6-a650-6fda0959c734 + title: Book + description: Interesting book + price: 200 + count: 2 + total: 400 + error: null + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: Products are not valid + '403': + description: Forbidden (when authorization header is missing) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: You must be authorized user + '401': + description: Unauthorized (when no user matching authorization header is found) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: User is not authorized + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: Cart was not found + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: 'Internal Server error' + delete: + tags: + - cart + summary: Empty user cart + parameters: + - in: header + name: x-user-id + schema: + type: string + format: uuid + required: true + description: User id (uuid) + example: 'eb5a26af-6e4c-4f31-a9b1-3450d42ac66c' + responses: + '200': + description: Returns success = true if cart was successfully emptied + content: + application/json: + schema: + $ref: '#/components/schemas/EmptySuccessResponse' + example: + data: + success: true + error: null + '403': + description: Forbidden (when authorization header is missing) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: You must be authorized user + '401': + description: Unauthorized (when no user matching authorization header is found) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: User is not authorized + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: 'Internal Server error' + /api/profile/cart/checkout: + post: + tags: + - cart + summary: Create an order + parameters: + - in: header + name: x-user-id + schema: + type: string + format: uuid + required: true + description: User id (uuid) + example: 'eb5a26af-6e4c-4f31-a9b1-3450d42ac66c' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/CheckoutResponse' + example: + data: + order: + id: 6c36d6fa-f694-4f9c-9b2c-6f7049d38f4a + userId: 6dc52b3c-de7e-431a-84b8-0ec56e0774d4 + cartId: cadff0c5-0079-4db8-b6bf-84c9c2633ca3 + items: + - product: + id: 5c293ad0-19d0-41ee-baa3-4c648f9f7697 + title: Book + description: Interesting book + price: 200 + count: 2 + - product: + id: afdd68c4-d359-45e6-b9fd-c8fdb2a162a0 + title: Pen + description: Cute pen + price: 20 + count: 5 + payment: + type: paypal + address: London + creditCard: 1234-1234-1234-1234 + delivery: + type: post + address: London + comments: '' + status: created + total: 500 + error: null + '403': + description: Forbidden (when authorization header is missing) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: You must be authorized user + '401': + description: Unauthorized (when no user matching authorization header is found) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: User is not authorized + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: Cart is empty + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: 'Internal Server error' + /api/products: + get: + tags: + - product + summary: Returns a list of products + parameters: + - in: header + name: x-user-id + schema: + type: string + format: uuid + required: true + description: User id (uuid) + example: 'eb5a26af-6e4c-4f31-a9b1-3450d42ac66c' + responses: + '200': + description: Returns a list of all products available + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProductsResponse' + example: + data: + - id: 5c293ad0-19d0-41ee-baa3-4c648f9f7697 + title: Book + description: Interesting book + price: 200 + - id: afdd68c4-d359-45e6-b9fd-c8fdb2a162a0 + title: Pen + description: Cute pen + price: 20 + error: null + '403': + description: Forbidden (when authorization header is missing) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: You must be authorized user + '401': + description: Unauthorized (when no user matching authorization header is found) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: User is not authorized + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: 'Internal Server error' + /api/products/{productId}: + get: + tags: + - product + summary: Returns single product + parameters: + - name: productId + in: path + description: Id (uuid) of product to return + required: true + schema: + type: number + - in: header + name: x-user-id + schema: + type: string + format: uuid + required: true + description: User id (uuid) + example: 'eb5a26af-6e4c-4f31-a9b1-3450d42ac66c' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ProductResponse' + example: + data: + id: 5c293ad0-19d0-41ee-baa3-4c648f9f7697 + title: Book + description: Interesting book + price: 200 + error: null + '403': + description: Forbidden (when authorization header is missing) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: You must be authorized user + '401': + description: Unauthorized (when no user matching authorization header is found) + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: User is not authorized + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: No product with such id + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/CartResponse' + example: + data: null + error: + message: 'Internal Server error' +components: + schemas: + Cart: + type: object + properties: + id: + type: string + description: Cart id (uuid) + example: dd5ec5ab-deaa-419c-8a6b-7e67b1f7ec87 + items: + type: array + description: Items added to cart + items: + $ref: '#/components/schemas/CartItem' + CartItem: + type: object + properties: + product: + $ref: '#/components/schemas/Product' + count: + type: number + description: Total count of specific products + example: 2 + Product: + type: object + properties: + id: + type: string + description: Product id (uuid) + example: 5c293ad0-19d0-41ee-baa3-4c648f9f7697 + title: + type: string + description: Product name + example: Book + description: + type: string + description: Product description + example: Interesting book + price: + type: number + description: Product price + example: 200 + Order: + type: object + properties: + id: + type: string + userId: + type: string + cartId: + type: string + items: + type: array + items: + $ref: '#/components/schemas/CartItem' + payment: + type: object + properties: + type: + type: string + address: + type: string + creditCard: + type: string + delivery: + type: object + properties: + type: + type: string + address: + type: string + comments: + type: string + status: + type: string + total: + type: number + CartResponse: + type: object + properties: + data: + type: object + properties: + cart: + $ref: '#/components/schemas/Cart' + total: + type: number + error: + $ref: '#/components/schemas/ErrorResponse' + CheckoutResponse: + type: object + properties: + data: + type: object + properties: + order: + $ref: '#/components/schemas/Order' + error: + $ref: '#/components/schemas/ErrorResponse' + ProductResponse: + type: object + properties: + data: + $ref: '#/components/schemas/Product' + error: + $ref: '#/components/schemas/ErrorResponse' + ProductsResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Product' + error: + $ref: '#/components/schemas/ErrorResponse' + UpdateCartRequestBody: + type: object + properties: + productId: + type: string + example: "915b2f40-9fd9-47f2-9b51-628f3dc69aac" + count: + type: number + example: 5 + UserCredentials: + type: object + properties: + email: + type: string + example: "ann.jones@gmail.com" + password: + type: string + example: "DDQldls?kdpw0fk" + EmptySuccessResponse: + type: object + properties: + data: + type: object + properties: + success: + type: boolean + error: + $ref: '#/components/schemas/ErrorResponse' + ErrorResponse: + type: object + nullable: true + properties: + message: + type: string diff --git a/tsconfig.json b/tsconfig.json index 230cc40..ad21052 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,9 @@ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "allowImportingTsExtensions": true, + "noEmit": true } }