├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── _Resources ├── CartServiceClient.js ├── OrderServiceClient.js ├── UserServiceClient.js ├── _service_template │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc │ ├── app.js │ ├── bin │ │ └── start.js │ ├── config.js │ ├── lib │ │ ├── mongooseConnection.js │ │ ├── redisConnection.js │ │ └── tracing.js │ ├── models │ │ └── .gitkeep │ ├── package-lock.json │ ├── package.json │ └── routes │ │ └── index.js ├── _support │ ├── items.json │ ├── snippets.md │ └── test-apis.http ├── cart-service │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc │ ├── app.js │ ├── bin │ │ └── start.js │ ├── config.js │ ├── lib │ │ ├── CartService.js │ │ ├── mongooseConnection.js │ │ ├── redisConnection.js │ │ └── tracing.js │ ├── models │ │ ├── Item.js │ │ ├── Order.js │ │ └── User.js │ ├── package-lock.json │ ├── package.json │ └── routes │ │ └── index.js ├── order-service │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc │ ├── app.js │ ├── bin │ │ └── start.js │ ├── config.js │ ├── lib │ │ ├── OrderService.js │ │ ├── mongooseConnection.js │ │ ├── redisConnection.js │ │ └── tracing.js │ ├── models │ │ └── Order.js │ ├── package-lock.json │ ├── package.json │ └── routes │ │ └── index.js ├── registry-service │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc │ ├── app.js │ ├── bin │ │ └── start.js │ ├── config.js │ ├── lib │ │ └── tracing.js │ ├── package-lock.json │ ├── package.json │ └── routes │ │ └── index.js └── user-service │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc │ ├── app.js │ ├── bin │ └── start.js │ ├── config.js │ ├── lib │ ├── UserService.js │ ├── mongooseConnection.js │ ├── redisConnection.js │ └── tracing.js │ ├── models │ ├── Item.js │ ├── Order.js │ └── User.js │ ├── package-lock.json │ ├── package.json │ └── routes │ └── index.js ├── favicon.ico └── workspace ├── microservices └── .gitkeep └── shopper ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── client └── css │ └── site.css ├── package-lock.json ├── package.json └── server ├── app.js ├── bin └── start.js ├── config └── index.js ├── lib ├── middlewares.js ├── mongooseConnection.js ├── redisConnection.js └── tracing.js ├── models ├── Item.js ├── Order.js └── User.js ├── routes ├── admin │ ├── item │ │ └── index.js │ ├── orders │ │ └── index.js │ └── user │ │ └── index.js ├── cart │ └── index.js ├── index.js ├── shop │ └── index.js └── user │ └── index.js ├── services ├── CartService.js ├── CatalogService.js ├── OrderService.js └── UserService.js └── views ├── admin ├── item.pug ├── orders.pug └── user.pug ├── cart.pug ├── index.pug ├── layout.pug └── shop.pug /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Codeowners for these exercise files: 2 | # * (asterisk) denotes "all files and folders" 3 | # Example: * @producer @instructor 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Issue Overview 9 | 10 | 11 | ## Describe your environment 12 | 13 | 14 | ## Steps to Reproduce 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 4. 20 | 21 | ## Expected Behavior 22 | 23 | 24 | ## Current Behavior 25 | 26 | 27 | ## Possible Solution 28 | 29 | 30 | ## Screenshots / Video 31 | 32 | 33 | ## Related Issues 34 | 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Copy To Branches 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | copy-to-branches: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | - name: Copy To Branches Action 12 | uses: planetoftheweb/copy-to-branches@v1.2 13 | env: 14 | key: main 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .tmp 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", 3 | "editor.formatOnPaste": false, // required 4 | "editor.formatOnType": false, // required 5 | "editor.formatOnSave": true, // optional 6 | "editor.formatOnSaveMode": "file", // required to format on save 7 | "files.autoSave": "onFocusChange", // optional but recommended 8 | "vs-code-prettier-eslint.prettierLast": "false" // set as "true" to run 'prettier' last not first 9 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | Contribution Agreement 3 | ====================== 4 | 5 | This repository does not accept pull requests (PRs). All pull requests will be closed. 6 | 7 | However, if any contributions (through pull requests, issues, feedback or otherwise) are provided, as a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code (or otherwise providing feedback), you (and, if applicable, your employer) are licensing the submitted code (and/or feedback) to LinkedIn and the open source community subject to the BSD 2-Clause license. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LinkedIn Learning Exercise Files License Agreement 2 | ================================================== 3 | 4 | This License Agreement (the "Agreement") is a binding legal agreement 5 | between you (as an individual or entity, as applicable) and LinkedIn 6 | Corporation (“LinkedIn”). By downloading or using the LinkedIn Learning 7 | exercise files in this repository (“Licensed Materials”), you agree to 8 | be bound by the terms of this Agreement. If you do not agree to these 9 | terms, do not download or use the Licensed Materials. 10 | 11 | 1. License. 12 | - a. Subject to the terms of this Agreement, LinkedIn hereby grants LinkedIn 13 | members during their LinkedIn Learning subscription a non-exclusive, 14 | non-transferable copyright license, for internal use only, to 1) make a 15 | reasonable number of copies of the Licensed Materials, and 2) make 16 | derivative works of the Licensed Materials for the sole purpose of 17 | practicing skills taught in LinkedIn Learning courses. 18 | - b. Distribution. Unless otherwise noted in the Licensed Materials, subject 19 | to the terms of this Agreement, LinkedIn hereby grants LinkedIn members 20 | with a LinkedIn Learning subscription a non-exclusive, non-transferable 21 | copyright license to distribute the Licensed Materials, except the 22 | Licensed Materials may not be included in any product or service (or 23 | otherwise used) to instruct or educate others. 24 | 25 | 2. Restrictions and Intellectual Property. 26 | - a. You may not to use, modify, copy, make derivative works of, publish, 27 | distribute, rent, lease, sell, sublicense, assign or otherwise transfer the 28 | Licensed Materials, except as expressly set forth above in Section 1. 29 | - b. Linkedin (and its licensors) retains its intellectual property rights 30 | in the Licensed Materials. Except as expressly set forth in Section 1, 31 | LinkedIn grants no licenses. 32 | - c. You indemnify LinkedIn and its licensors and affiliates for i) any 33 | alleged infringement or misappropriation of any intellectual property rights 34 | of any third party based on modifications you make to the Licensed Materials, 35 | ii) any claims arising from your use or distribution of all or part of the 36 | Licensed Materials and iii) a breach of this Agreement. You will defend, hold 37 | harmless, and indemnify LinkedIn and its affiliates (and our and their 38 | respective employees, shareholders, and directors) from any claim or action 39 | brought by a third party, including all damages, liabilities, costs and 40 | expenses, including reasonable attorneys’ fees, to the extent resulting from, 41 | alleged to have resulted from, or in connection with: (a) your breach of your 42 | obligations herein; or (b) your use or distribution of any Licensed Materials. 43 | 44 | 3. Open source. This code may include open source software, which may be 45 | subject to other license terms as provided in the files. 46 | 47 | 4. Warranty Disclaimer. LINKEDIN PROVIDES THE LICENSED MATERIALS ON AN “AS IS” 48 | AND “AS AVAILABLE” BASIS. LINKEDIN MAKES NO REPRESENTATION OR WARRANTY, 49 | WHETHER EXPRESS OR IMPLIED, ABOUT THE LICENSED MATERIALS, INCLUDING ANY 50 | REPRESENTATION THAT THE LICENSED MATERIALS WILL BE FREE OF ERRORS, BUGS OR 51 | INTERRUPTIONS, OR THAT THE LICENSED MATERIALS ARE ACCURATE, COMPLETE OR 52 | OTHERWISE VALID. TO THE FULLEST EXTENT PERMITTED BY LAW, LINKEDIN AND ITS 53 | AFFILIATES DISCLAIM ANY IMPLIED OR STATUTORY WARRANTY OR CONDITION, INCLUDING 54 | ANY IMPLIED WARRANTY OR CONDITION OF MERCHANTABILITY OR FITNESS FOR A 55 | PARTICULAR PURPOSE, AVAILABILITY, SECURITY, TITLE AND/OR NON-INFRINGEMENT. 56 | YOUR USE OF THE LICENSED MATERIALS IS AT YOUR OWN DISCRETION AND RISK, AND 57 | YOU WILL BE SOLELY RESPONSIBLE FOR ANY DAMAGE THAT RESULTS FROM USE OF THE 58 | LICENSED MATERIALS TO YOUR COMPUTER SYSTEM OR LOSS OF DATA. NO ADVICE OR 59 | INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM US OR THROUGH OR 60 | FROM THE LICENSED MATERIALS WILL CREATE ANY WARRANTY OR CONDITION NOT 61 | EXPRESSLY STATED IN THESE TERMS. 62 | 63 | 5. Limitation of Liability. LINKEDIN SHALL NOT BE LIABLE FOR ANY INDIRECT, 64 | INCIDENTAL, SPECIAL, PUNITIVE, CONSEQUENTIAL OR EXEMPLARY DAMAGES, INCLUDING 65 | BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER 66 | INTANGIBLE LOSSES . IN NO EVENT WILL LINKEDIN'S AGGREGATE LIABILITY TO YOU 67 | EXCEED $100. THIS LIMITATION OF LIABILITY SHALL: 68 | - i. APPLY REGARDLESS OF WHETHER (A) YOU BASE YOUR CLAIM ON CONTRACT, TORT, 69 | STATUTE, OR ANY OTHER LEGAL THEORY, (B) WE KNEW OR SHOULD HAVE KNOWN ABOUT 70 | THE POSSIBILITY OF SUCH DAMAGES, OR (C) THE LIMITED REMEDIES PROVIDED IN THIS 71 | SECTION FAIL OF THEIR ESSENTIAL PURPOSE; AND 72 | - ii. NOT APPLY TO ANY DAMAGE THAT LINKEDIN MAY CAUSE YOU INTENTIONALLY OR 73 | KNOWINGLY IN VIOLATION OF THESE TERMS OR APPLICABLE LAW, OR AS OTHERWISE 74 | MANDATED BY APPLICABLE LAW THAT CANNOT BE DISCLAIMED IN THESE TERMS. 75 | 76 | 6. Termination. This Agreement automatically terminates upon your breach of 77 | this Agreement or termination of your LinkedIn Learning subscription. On 78 | termination, all licenses granted under this Agreement will terminate 79 | immediately and you will delete the Licensed Materials. Sections 2-7 of this 80 | Agreement survive any termination of this Agreement. LinkedIn may discontinue 81 | the availability of some or all of the Licensed Materials at any time for any 82 | reason. 83 | 84 | 7. Miscellaneous. This Agreement will be governed by and construed in 85 | accordance with the laws of the State of California without regard to conflict 86 | of laws principles. The exclusive forum for any disputes arising out of or 87 | relating to this Agreement shall be an appropriate federal or state court 88 | sitting in the County of Santa Clara, State of California. If LinkedIn does 89 | not act to enforce a breach of this Agreement, that does not mean that 90 | LinkedIn has waived its right to enforce this Agreement. The Agreement does 91 | not create a partnership, agency relationship, or joint venture between the 92 | parties. Neither party has the power or authority to bind the other or to 93 | create any obligation or responsibility on behalf of the other. You may not, 94 | without LinkedIn’s prior written consent, assign or delegate any rights or 95 | obligations under these terms, including in connection with a change of 96 | control. Any purported assignment and delegation shall be ineffective. The 97 | Agreement shall bind and inure to the benefit of the parties, their respective 98 | successors and permitted assigns. If any provision of the Agreement is 99 | unenforceable, that provision will be modified to render it enforceable to the 100 | extent possible to give effect to the parties’ intentions and the remaining 101 | provisions will not be affected. This Agreement is the only agreement between 102 | you and LinkedIn regarding the Licensed Materials, and supersedes all prior 103 | agreements relating to the Licensed Materials. 104 | 105 | Last Updated: March 2019 106 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2022 LinkedIn Corporation 2 | All Rights Reserved. 3 | 4 | Licensed under the LinkedIn Learning Exercise File License (the "License"). 5 | See LICENSE in the project root for license information. 6 | 7 | ATTRIBUTIONS: 8 | [PLEASE PROVIDE ATTRIBUTIONS OR DELETE THIS AND THE ABOVE LINE “ATTRIBUTIONS”] 9 | 10 | Please note, this project may automatically load third party code from external 11 | repositories (for example, NPM modules, Composer packages, or other dependencies). 12 | If so, such third party code may be subject to other license terms than as set 13 | forth above. In addition, such third party code may also depend on and load 14 | multiple tiers of dependencies. Please review the applicable licenses of the 15 | additional dependencies. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js: Microservices 2 | This is the repository for the LinkedIn Learning course Node.js: Microservices. The full course is available from [LinkedIn Learning][lil-course-url]. 3 | 4 | ![Node.js: Microservices][lil-thumbnail-url] 5 | 6 | In this fast-paced era of distributed systems, mastering microservices—not just deploying services, but truly understanding the patterns and principles that drive them— is essential for developers. And in organizations large and small, Node.js is often the platform of choice for building microservices architectures. In this course, Daniel Khan 7 | shows you how to use Node.js to create a microservice architecture from scratch and tackles the all-too-common challenge of transforming a monolithic app into a flexible, modular system composed of individual services. Throughout the course, Daniel explores crucial concepts like service discovery, resilience, and decoupling. Check out this course to gain practical knowledge of microservices that you can apply to your day-to-day work immediately. 8 | 9 | ## Instructions 10 | This repository has branches for each of the videos in the course. You can use the branch pop up menu in github to switch to a specific branch and take a look at the course at that stage, or you can add `/tree/BRANCH_NAME` to the URL to go to the branch you want to access. 11 | 12 | ## Branches 13 | The branches are structured to correspond to the videos in the course. The naming convention is `CHAPTER#_MOVIE#`. As an example, the branch named `02_03` corresponds to the second chapter and the third video in that chapter. 14 | Some branches will have a beginning and an end state. These are marked with the letters `b` for "beginning" and `e` for "end". The `b` branch contains the code as it is at the beginning of the movie. The `e` branch contains the code as it is at the end of the movie. The `main` branch holds the final state of the code when in the course. 15 | 16 | When switching from one exercise files branch to the next after making changes to the files, you may get a message like this: 17 | 18 | error: Your local changes to the following files would be overwritten by checkout: [files] 19 | Please commit your changes or stash them before you switch branches. 20 | Aborting 21 | 22 | To resolve this issue: 23 | 24 | Add changes to git using this command: git add . 25 | Commit changes using this command: git commit -m "some message" 26 | 27 | ## Installing 28 | 1. We need [Node.js](https://nodejs.org/en). I would just recommend installing the current LTS, means long-term supported version that you see here on the nodejs.org website. 29 | 2. You will also need a Git client on your system to acquire the exercise files from GitHub. On [git-scm.com](https://git-scm.com/downloads) you will be presented with selections for your particular operating system. 30 | 3. We will use [Docker](https://www.docker.com/). You will need to have Docker installed on your system. Download the respective installation files for your system. 31 | 32 | Once you have installed all of that on your system, you can check it in your console or terminal by running: 33 | ``` 34 | node -v 35 | docker -v 36 | git -v 37 | ``` 38 | 39 | These steps are covered in the video "Installing Git, Node.js, and Docker" in the course videos. 40 | 41 | ### Instructor 42 | 43 | Daniel Khan 44 | 45 | Technology Lead, Developer, Application Architect 46 | 47 | 48 | 49 | Check out my other courses on [LinkedIn Learning](https://www.linkedin.com/learning/instructors/daniel-khan). 50 | 51 | [lil-course-url]: https://www.linkedin.com/learning/node-js-microservices-22685072?dApp=59033956&leis=LAA 52 | [lil-thumbnail-url]: https://media.licdn.com/dms/image/D560DAQGNhXwSHfVTTg/learning-public-crop_288_512/0/1691165894653?e=2147483647&v=beta&t=Ct2CRNdv8le_M-0mDHAtTX7YDIyGoRWAQr3D22q9FOY 53 | -------------------------------------------------------------------------------- /_Resources/CartServiceClient.js: -------------------------------------------------------------------------------- 1 | const config = require("../config"); 2 | const ServiceClient = require("./ServiceClient"); 3 | 4 | /** @module CartServiceClient */ 5 | 6 | /** 7 | * Service class for managing a user's cart 8 | */ 9 | class CartServiceClient { 10 | static key(userId) { 11 | return `shopper_cart:${userId}`; 12 | } 13 | 14 | static client() { 15 | return config.redis.client; 16 | } 17 | 18 | /** 19 | * Add an item to the user's cart 20 | * @param {string} itemId - The ID of the item to add 21 | * @returns {Promise} - A promise that resolves to the new quantity of 22 | * the item in the cart 23 | */ 24 | static async add(userId, itemId) { 25 | try { 26 | return ServiceClient.callService("cart-service", { 27 | method: "post", 28 | url: `/items/${userId}`, 29 | data: { itemId } 30 | }); 31 | } catch (error) { 32 | console.error(error); 33 | throw new Error(error); 34 | } 35 | } 36 | 37 | /** 38 | * Get all items in the user's cart 39 | * @returns {Promise} - A promise that resolves to an object containing 40 | * the cart items and their quantities 41 | */ 42 | static async getAll(userId) { 43 | try { 44 | return ServiceClient.callService("cart-service", { 45 | method: "get", 46 | url: `/items/${userId}` 47 | }); 48 | } catch (error) { 49 | console.error(error); 50 | return []; 51 | } 52 | } 53 | 54 | /** 55 | * Remove an item from the user's cart 56 | * @param {string} itemId - The ID of the item to remove 57 | * @returns {Promise} - A promise that resolves to the number of items 58 | * removed (1 if the item was removed, 0 if the item was not in the cart) 59 | */ 60 | static async remove(userId, itemId) { 61 | try { 62 | return ServiceClient.callService("cart-service", { 63 | method: "delete", 64 | url: `/items/${userId}/${itemId}` 65 | }); 66 | } catch (error) { 67 | console.error(error); 68 | return []; 69 | } 70 | } 71 | } 72 | 73 | module.exports = CartServiceClient; 74 | -------------------------------------------------------------------------------- /_Resources/OrderServiceClient.js: -------------------------------------------------------------------------------- 1 | /** @module OrderService */ 2 | const ServiceClient = require("./ServiceClient"); 3 | 4 | /** 5 | * Service class for managing orders 6 | */ 7 | class OrderServiceClient { 8 | /** 9 | * Create a new order 10 | * @param {Object} user - The user who is creating the order 11 | * @param {Array} items - The items in the order 12 | * @returns {Promise} - A promise that resolves to the new order 13 | */ 14 | static async create(userId, email, items) { 15 | return ServiceClient.callService("order-service", { 16 | method: "post", 17 | url: `/orders`, 18 | data: { userId, email, items } 19 | }); 20 | } 21 | 22 | /** 23 | * Get all orders 24 | * @returns {Promise} - A promise that resolves to an array of orders 25 | */ 26 | static async getAll() { 27 | try { 28 | return ServiceClient.callService("order-service", { 29 | method: "get", 30 | url: `/orders` 31 | }); 32 | } catch (error) { 33 | console.error(error); 34 | return []; 35 | } 36 | } 37 | 38 | /** 39 | * Update the status of an order 40 | * @param {string} orderId - The ID of the order to update 41 | * @param {string} status - The new status 42 | * @returns {Promise} - A promise that resolves to the updated 43 | * order, or null if no order was found 44 | */ 45 | static async setStatus(orderId, status) { 46 | return ServiceClient.callService("order-service", { 47 | method: "put", 48 | url: `/orders/${orderId}`, 49 | data: { status } 50 | }); 51 | } 52 | } 53 | 54 | module.exports = OrderServiceClient; 55 | -------------------------------------------------------------------------------- /_Resources/UserServiceClient.js: -------------------------------------------------------------------------------- 1 | /** @module UserService */ 2 | 3 | const ServiceClient = require("./ServiceClient"); 4 | 5 | /** 6 | * Service class for managing users 7 | */ 8 | class UserServiceClient { 9 | 10 | /** 11 | * Authenticate a user 12 | * @param {string} email - The email address 13 | * @param {string} password - The email password 14 | * @returns {Promise} - A promise that either returns the authenticated user or null 15 | */ 16 | static async authenticate(email, password) { 17 | try { 18 | return ServiceClient.callService("user-service", { 19 | method: "post", 20 | url: `/users/authenticate`, 21 | data: { email, password } 22 | }); 23 | } catch (error) { 24 | console.error(error); 25 | return []; 26 | } 27 | } 28 | 29 | /** 30 | * Get all users 31 | * @returns {Promise} - A promise that resolves to an array of users 32 | */ 33 | static async getAll() { 34 | try { 35 | return ServiceClient.callService("user-service", { 36 | method: "get", 37 | url: `/users` 38 | }); 39 | } catch (error) { 40 | console.error(error); 41 | return []; 42 | } 43 | } 44 | 45 | /** 46 | * Get a user by ID 47 | * @param {string} userId - The ID of the user to retrieve 48 | * @returns {Promise} - A promise that resolves to the user, or 49 | * null if no user was found 50 | */ 51 | static async getOne(userId) { 52 | try { 53 | return ServiceClient.callService("user-service", { 54 | method: "get", 55 | url: `/users/${userId}` 56 | }); 57 | } catch (error) { 58 | console.error(error); 59 | return []; 60 | } 61 | } 62 | 63 | /** 64 | * Create a new user 65 | * @param {Object} data - The data for the new user 66 | * @returns {Promise} - A promise that resolves to the new user 67 | */ 68 | static async create(data) { 69 | try { 70 | return ServiceClient.callService("user-service", { 71 | method: "post", 72 | url: `/users`, 73 | data 74 | }); 75 | } catch (error) { 76 | console.error(error); 77 | return []; 78 | } 79 | } 80 | 81 | /** 82 | * Update a user's data 83 | * @param {string} userId - The ID of the user to update 84 | * @param {Object} data - The new data for the user 85 | * @returns {Promise} - A promise that resolves to the updated user 86 | */ 87 | static async update(userId, data) { 88 | try { 89 | return ServiceClient.callService("user-service", { 90 | method: "put", 91 | url: `/users/${userId}`, 92 | data 93 | }); 94 | } catch (error) { 95 | console.error(error); 96 | return []; 97 | } 98 | } 99 | 100 | /** 101 | * Remove a user 102 | * @param {string} userId - The ID of the user to remove 103 | * @returns {Promise} - A promise that resolves to the result of the 104 | * delete operation 105 | */ 106 | static async remove(userId) { 107 | try { 108 | return ServiceClient.callService("user-service", { 109 | method: "delete", 110 | url: `/users/${userId}` 111 | }); 112 | } catch (error) { 113 | console.error(error); 114 | return []; 115 | } 116 | } 117 | } 118 | 119 | module.exports = UserServiceClient; 120 | -------------------------------------------------------------------------------- /_Resources/_service_template/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["airbnb-base", "prettier"], 8 | "plugins": ["prettier", "import"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "no-console": 0, 18 | "no-unused-vars": 1, 19 | "no-underscore-dangle": 0, 20 | "no-undef": 1, 21 | "import/no-extraneous-dependencies": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /_Resources/_service_template/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .DS_STORe -------------------------------------------------------------------------------- /_Resources/_service_template/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "trailingComma": "none" 5 | } -------------------------------------------------------------------------------- /_Resources/_service_template/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const app = express(); 4 | const morgan = require("morgan"); 5 | const routes = require("./routes"); 6 | const config = require("./config"); 7 | 8 | // Middleware to parse JSON request bodies 9 | app.use(express.json()); 10 | 11 | // Middleware to log HTTP requests 12 | app.use(morgan("tiny")); 13 | 14 | // Mount the router 15 | app.use("/", routes); 16 | // Error handling middleware 17 | app.use((err, req, res, next) => { 18 | const status = err.status || 500; 19 | const message = err.message || "Internal Server Error"; 20 | // You can also log the error to a file or console 21 | console.error(err); 22 | 23 | res.status(status).json({ 24 | error: { 25 | message, 26 | status 27 | } 28 | }); 29 | }); 30 | module.exports = app; 31 | -------------------------------------------------------------------------------- /_Resources/_service_template/bin/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable import/order */ 4 | 5 | // Import necessary dependencies 6 | // HTTP server functionality 7 | const config = require("../config"); // Configuration settings 8 | 9 | // eslint-disable-next-line no-unused-vars 10 | const tracing = require("../lib/tracing")( 11 | `${config.serviceName}:${config.serviceVersion}` 12 | ); 13 | 14 | // Import necessary dependencies 15 | const http = require("http"); // HTTP server functionality 16 | 17 | // const connectToMongoose = require("../lib/mongooseConnection"); // Function to connect to MongoDB 18 | // const connectToRedis = require("../lib/redisConnection"); // Function to connect to Redis 19 | 20 | // Prepare the Redis client to connect to later 21 | // This has to come before `app` is initiated because sessions depend on it 22 | // config.redis.client = connectToRedis(config.redis.options); 23 | 24 | const app = require("../app"); // Express application 25 | 26 | // Create the HTTP server with the express app 27 | const server = http.createServer(app); 28 | 29 | // Attach error and listening handlers to the server 30 | server.on("listening", () => { 31 | const addr = server.address(); 32 | const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; 33 | console.info( 34 | `${config.serviceName}:${config.serviceVersion} listening on ${bind}` 35 | ); 36 | }); 37 | 38 | // Start the server 39 | server.listen(0); 40 | -------------------------------------------------------------------------------- /_Resources/_service_template/config.js: -------------------------------------------------------------------------------- 1 | const pkg = require("./package.json"); 2 | 3 | module.exports = { 4 | serviceName: pkg.name, 5 | serviceVersion: pkg.version, 6 | mongodb: { 7 | url: "mongodb://localhost:37017/shopper" 8 | }, 9 | redis: { 10 | options: { 11 | url: "redis://localhost:7379" 12 | }, 13 | client: null 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /_Resources/_service_template/lib/mongooseConnection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const connectToMongoose = async (connectionString) => { 4 | try { 5 | await mongoose.connect(connectionString); 6 | 7 | console.log("Connected to MongoDB"); 8 | } catch (error) { 9 | console.error("Error connecting to Mongoose:", error); 10 | process.exit(1); 11 | } 12 | }; 13 | 14 | module.exports = connectToMongoose; 15 | -------------------------------------------------------------------------------- /_Resources/_service_template/lib/redisConnection.js: -------------------------------------------------------------------------------- 1 | const redis = require('@redis/client'); 2 | 3 | const connectToRedis = (options) => { 4 | const client = redis.createClient(options); 5 | 6 | client.on('connect', () => { 7 | console.log('Connected to Redis'); 8 | }); 9 | 10 | client.on('error', (error) => { 11 | console.error('Error connecting to Redis:', error); 12 | process.exit(1); 13 | }); 14 | 15 | return client; 16 | }; 17 | 18 | module.exports = connectToRedis; 19 | -------------------------------------------------------------------------------- /_Resources/_service_template/lib/tracing.js: -------------------------------------------------------------------------------- 1 | const { node } = require("@opentelemetry/sdk-node"); 2 | 3 | const { NodeTracerProvider } = node; 4 | 5 | const opentelemetry = require("@opentelemetry/api"); 6 | const { registerInstrumentations } = require("@opentelemetry/instrumentation"); 7 | const { 8 | getNodeAutoInstrumentations 9 | } = require("@opentelemetry/auto-instrumentations-node"); 10 | const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace-base"); 11 | const { Resource } = require("@opentelemetry/resources"); 12 | const { 13 | SemanticResourceAttributes 14 | } = require("@opentelemetry/semantic-conventions"); 15 | const { 16 | OTLPTraceExporter 17 | } = require("@opentelemetry/exporter-trace-otlp-http"); 18 | 19 | const sdks = []; 20 | 21 | const exporter = new OTLPTraceExporter(); 22 | 23 | module.exports = (serviceName) => { 24 | if (sdks[serviceName]) return sdks[serviceName]; 25 | 26 | const provider = new NodeTracerProvider({ 27 | resource: new Resource({ 28 | [SemanticResourceAttributes.SERVICE_NAME]: serviceName 29 | }) 30 | }); 31 | 32 | provider.addSpanProcessor( 33 | new BatchSpanProcessor(exporter, { 34 | // The maximum queue size. After the size is reached spans are dropped. 35 | maxQueueSize: 1000, 36 | // The interval between two consecutive exports 37 | scheduledDelayMillis: 30000 38 | }) 39 | ); 40 | provider.register(); 41 | registerInstrumentations({ 42 | instrumentations: getNodeAutoInstrumentations({ 43 | "@opentelemetry/instrumentation-fs": { 44 | enabled: false 45 | } 46 | }) 47 | }); 48 | 49 | return opentelemetry.trace.getTracer(serviceName); 50 | }; 51 | -------------------------------------------------------------------------------- /_Resources/_service_template/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInLearning/nodejs-microservices-4403064/10a30bb719e0126d1248d81c64a54c2a61a9a1f0/_Resources/_service_template/models/.gitkeep -------------------------------------------------------------------------------- /_Resources/_service_template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template-service", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "", 6 | "scripts": { 7 | "start": "node ./bin/start", 8 | "dev": "nodemon ./bin/start" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@redis/client": "^1.5.7", 14 | "express": "^4.18.2", 15 | "mongoose": "^7.1.2", 16 | "morgan": "^1.10.0", 17 | "@opentelemetry/api": "^1.4.1", 18 | "@opentelemetry/auto-instrumentations-node": "^0.37.0", 19 | "@opentelemetry/exporter-trace-otlp-http": "^0.39.1", 20 | "@opentelemetry/sdk-metrics": "^1.13.0", 21 | "@opentelemetry/sdk-node": "^0.39.1" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^7.32.0", 25 | "eslint-config-airbnb-base": "^14.2.0", 26 | "eslint-config-prettier": "^6.12.0", 27 | "eslint-plugin-import": "^2.22.1", 28 | "eslint-plugin-prettier": "^3.1.4", 29 | "nodemon": "^2.0.4", 30 | "prettier": "^2.8.7" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /_Resources/_service_template/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | // Define your RESTful routes here 5 | router.get("/", (req, res) => { 6 | // Return a JSON response with a 'hello world' message 7 | res.json({ msg: "hello world" }); 8 | }); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /_Resources/_support/items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "sku": "001", "name": "Bug Repellent Spray for Developers", "price": 19.99 }, 3 | { "sku": "002", "name": "Microservice Jigsaw Puzzles", "price": 14.99 }, 4 | { "sku": "003", "name": "\"Decompose Monolith\" Hammer", "price": 9.99 }, 5 | { "sku": "004", "name": "Load Balancer Scale", "price": 24.99 }, 6 | { "sku": "005", "name": "Fault Isolation T-Shirts", "price": 29.99 }, 7 | { "sku": "006", "name": "Distributed System Domino Sets", "price": 39.99 }, 8 | { "sku": "007", "name": "Microservice Magic 8 Ball", "price": 14.99 }, 9 | { "sku": "008", "name": "API Gateway Keyring", "price": 9.99 }, 10 | { "sku": "009", "name": "Container Ornaments", "price": 14.99 }, 11 | { "sku": "010", "name": "\"Database Sharding\" Glassware", "price": 19.99 }, 12 | { "sku": "011", "name": "Serverless Stress Balls", "price": 12.99 }, 13 | { "sku": "012", "name": "Replica Set Twinsies Outfit", "price": 24.99 }, 14 | { "sku": "013", "name": "Scalability Elastic Bands", "price": 4.99 }, 15 | { "sku": "014", "name": "Circuit Breaker Switch Toys", "price": 19.99 }, 16 | { "sku": "015", "name": "Rate Limiter Hourglass", "price": 24.99 } 17 | ] 18 | -------------------------------------------------------------------------------- /_Resources/_support/snippets.md: -------------------------------------------------------------------------------- 1 | # Snippets 2 | This file contains snippets for you to copy paste 3 | 4 | ## Cloning the Exercise Files from GitHub 5 | Please note, that your repository url is different if you decided to fork the repo. 6 | In this case, please use the URL as shown under the <> Code button on the forked repository page. 7 | 8 | ```bash 9 | git clone --bare git@github.com:/LinkedInLearning/nodejs-microservices-4403064.git .git 10 | git config --bool core.bare false 11 | git reset --hard 12 | git branch 13 | ``` 14 | 15 | ## Start MongoDB in Docker 16 | Run `docker pull mongo` when doing it the first time followed by `docker run --name mongodb -p 37017:27017 -d mongo`. 17 | 18 | ## Start Redis in Docker 19 | Run `docker pull redis` followed by `docker run --name redis -p 7379:6379 -d redis` 20 | 21 | ### Install and run Redis Commander 22 | Make sure you have installed it with `npm install -g redis-commander` then run `redis-commander --redis-port=7379`. 23 | 24 | ## Start Jaeger in Docker 25 | ```sh 26 | docker run --name jaeger \ 27 | -e COLLECTOR_OTLP_ENABLED=true \ 28 | -p 16686:16686 \ 29 | -p 4317:4317 \ 30 | -p 4318:4318 \ 31 | -d jaegertracing/all-in-one:1.45 32 | ``` 33 | UI: http://localhost:16686 34 | 35 | ## Start RabbitMQ in Docker 36 | `docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 -d rabbitmq:3.11-management` 37 | 38 | Management interface: http://localhost:15672/ user: guest, password: guest 39 | 40 | -------------------------------------------------------------------------------- /_Resources/_support/test-apis.http: -------------------------------------------------------------------------------- 1 | // Testing the catalog-service - make sure to update the port 2 | @catalog-service-port = 56800 3 | 4 | ## 5 | GET http://localhost:{{catalog-service-port}}/items 6 | 7 | ### 8 | # @prompt item-id 9 | GET http://localhost:{{catalog-service-port}}/items/{{item-id}} 10 | 11 | ### 12 | # @prompt item-sku 13 | # @prompt item-name 14 | # @prompt item-price 15 | POST http://localhost:{{catalog-service-port}}/items 16 | content-type: application/json 17 | 18 | { 19 | "sku": {{item-sku}}, 20 | "name": "{{item-name}}", 21 | "price": {{item-price}} 22 | } 23 | 24 | // Updating an existing item 25 | ### 26 | # @prompt item-id 27 | # @prompt new-price 28 | PUT http://localhost:{{catalog-service-port}}/items/{{item-id}} 29 | content-type: application/json 30 | 31 | { 32 | "price": {{new-price}} 33 | } 34 | 35 | 36 | // Registering services 37 | ### 38 | PUT http://localhost:3080/register/myservice/1.1.0/3000 39 | 40 | ### 41 | PUT http://localhost:3080/register/myservice/2.0.1/3001 42 | 43 | ### 44 | PUT http://localhost:3080/register/myservice/1.1.1/3002 45 | 46 | ### 47 | PUT http://localhost:3080/register/myservice/1.2.1/3003 48 | 49 | ### 50 | PUT http://localhost:3080/register/myotherservice/0.0.1/5003 51 | 52 | 53 | // Querying the registry 54 | ### 55 | GET http://localhost:3080/find/myservice/1 56 | 57 | ### 58 | GET http://localhost:3080/find/myservice/2 59 | 60 | 61 | // This shows you how you get random services because of the wildcard version - try it a few times 62 | ### 63 | GET http://localhost:3080/find/myservice/* 64 | 65 | ### 66 | GET http://localhost:3080/find/myotherservice/* 67 | 68 | 69 | // Unregistering a service 70 | ### 71 | DELETE http://localhost:3080/register/myotherservice/0.0.1/5003 72 | 73 | 74 | // Test if it's gone 75 | ### 76 | GET http://localhost:3080/find/myotherservice/* 77 | -------------------------------------------------------------------------------- /_Resources/cart-service/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["airbnb-base", "prettier"], 8 | "plugins": ["prettier", "import"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "no-console": 0, 18 | "no-unused-vars": 1, 19 | "no-underscore-dangle": 0, 20 | "no-undef": 1, 21 | "import/no-extraneous-dependencies": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /_Resources/cart-service/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .DS_STORe -------------------------------------------------------------------------------- /_Resources/cart-service/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "trailingComma": "none" 5 | } -------------------------------------------------------------------------------- /_Resources/cart-service/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const app = express(); 4 | const morgan = require("morgan"); 5 | const routes = require("./routes"); 6 | const config = require("./config"); 7 | 8 | // Middleware to parse JSON request bodies 9 | app.use(express.json()); 10 | 11 | // Middleware to log HTTP requests 12 | app.use(morgan("tiny")); 13 | 14 | // Mount the router 15 | app.use("/", routes); 16 | 17 | // Error handling middleware 18 | app.use((err, req, res, next) => { 19 | const status = err.status || 500; 20 | const message = err.message || "Internal Server Error"; 21 | // You can also log the error to a file or console 22 | console.error(err); 23 | 24 | res.status(status).json({ 25 | error: { 26 | message, 27 | status 28 | } 29 | }); 30 | }); 31 | 32 | module.exports = app; 33 | -------------------------------------------------------------------------------- /_Resources/cart-service/bin/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable import/order */ 3 | 4 | // Import necessary dependencies 5 | // HTTP server functionality 6 | const config = require("../config"); // Configuration settings 7 | 8 | // eslint-disable-next-line no-unused-vars 9 | const tracing = require("../lib/tracing")( 10 | `${config.serviceName}:${config.serviceVersion}` 11 | ); 12 | 13 | const http = require("http"); 14 | const axios = require("axios"); 15 | 16 | // const connectToMongoose = require("../lib/mongooseConnection"); // Function to connect to MongoDB 17 | const connectToRedis = require("../lib/redisConnection"); // Function to connect to Redis 18 | 19 | // Prepare the Redis client to connect to later 20 | // This has to come before `app` is initiated because sessions depend on it 21 | config.redis.client = connectToRedis(config.redis.options); 22 | 23 | const app = require("../app"); // Express application 24 | 25 | // Create the HTTP server with the express app 26 | const server = http.createServer(app); 27 | 28 | // Attach error and listening handlers to the server 29 | server.on("listening", () => { 30 | const addr = server.address(); 31 | const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; 32 | 33 | const registerService = () => 34 | axios 35 | .put( 36 | `http://localhost:3080/register/${config.serviceName}/${ 37 | config.serviceVersion 38 | }/${server.address().port}` 39 | ) 40 | .catch((err) => console.error(err)); 41 | const unregisterService = () => 42 | axios 43 | .delete( 44 | `http://localhost:3080/register/${config.serviceName}/${ 45 | config.serviceVersion 46 | }/${server.address().port}` 47 | ) 48 | .catch((err) => console.error(err)); 49 | 50 | registerService(); 51 | const interval = setInterval(registerService, 15000); 52 | const cleanup = async () => { 53 | let clean = false; 54 | if (!clean) { 55 | clean = true; 56 | clearInterval(interval); 57 | await unregisterService(); 58 | } 59 | }; 60 | 61 | process.on("uncaughtException", async () => { 62 | await cleanup(); 63 | process.exit(0); 64 | }); 65 | 66 | process.on("SIGINT", async () => { 67 | await cleanup(); 68 | process.exit(0); 69 | }); 70 | 71 | process.on("SIGTERM", async () => { 72 | await cleanup(); 73 | process.exit(0); 74 | }); 75 | 76 | console.info( 77 | `${config.serviceName}:${config.serviceVersion} listening on ${bind}` 78 | ); 79 | }); 80 | 81 | // Start the server 82 | 83 | config.redis.client.connect().then(() => server.listen(0)); 84 | -------------------------------------------------------------------------------- /_Resources/cart-service/config.js: -------------------------------------------------------------------------------- 1 | const pkg = require("./package.json"); 2 | 3 | module.exports = { 4 | serviceName: pkg.name, 5 | serviceVersion: pkg.version, 6 | mongodb: { 7 | url: "mongodb://localhost:37017/shopper" 8 | }, 9 | redis: { 10 | options: { 11 | url: "redis://localhost:7379" 12 | }, 13 | client: null 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /_Resources/cart-service/lib/CartService.js: -------------------------------------------------------------------------------- 1 | const config = require("../config"); 2 | 3 | /** @module CartService */ 4 | 5 | /** 6 | * Service class for managing a user's cart 7 | */ 8 | class CartService { 9 | static key(userId) { 10 | return `shopper_cart:${userId}`; 11 | } 12 | 13 | static client() { 14 | return config.redis.client; 15 | } 16 | 17 | /** 18 | * Add an item to the user's cart 19 | * @param {string} itemId - The ID of the item to add 20 | * @returns {Promise} - A promise that resolves to the new quantity of 21 | * the item in the cart 22 | */ 23 | static async add(userId, itemId) { 24 | return this.client().HINCRBY(this.key(userId), itemId, 1); 25 | } 26 | 27 | /** 28 | * Get all items in the user's cart 29 | * @returns {Promise} - A promise that resolves to an object containing 30 | * the cart items and their quantities 31 | */ 32 | static async getAll(userId) { 33 | return this.client().HGETALL(this.key(userId)); 34 | } 35 | 36 | /** 37 | * Remove an item from the user's cart 38 | * @param {string} itemId - The ID of the item to remove 39 | * @returns {Promise} - A promise that resolves to the number of items 40 | * removed (1 if the item was removed, 0 if the item was not in the cart) 41 | */ 42 | static async remove(userId, itemId) { 43 | return this.client().HDEL(this.key(userId), itemId); 44 | } 45 | } 46 | 47 | module.exports = CartService; 48 | -------------------------------------------------------------------------------- /_Resources/cart-service/lib/mongooseConnection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const connectToMongoose = async (connectionString) => { 4 | try { 5 | await mongoose.connect(connectionString); 6 | 7 | console.log("Connected to MongoDB"); 8 | } catch (error) { 9 | console.error("Error connecting to Mongoose:", error); 10 | process.exit(1); 11 | } 12 | }; 13 | 14 | module.exports = connectToMongoose; 15 | -------------------------------------------------------------------------------- /_Resources/cart-service/lib/redisConnection.js: -------------------------------------------------------------------------------- 1 | const redis = require('@redis/client'); 2 | 3 | const connectToRedis = (options) => { 4 | const client = redis.createClient(options); 5 | 6 | client.on('connect', () => { 7 | console.log('Connected to Redis'); 8 | }); 9 | 10 | client.on('error', (error) => { 11 | console.error('Error connecting to Redis:', error); 12 | process.exit(1); 13 | }); 14 | 15 | return client; 16 | }; 17 | 18 | module.exports = connectToRedis; 19 | -------------------------------------------------------------------------------- /_Resources/cart-service/lib/tracing.js: -------------------------------------------------------------------------------- 1 | const { node } = require("@opentelemetry/sdk-node"); 2 | 3 | const { NodeTracerProvider } = node; 4 | 5 | const opentelemetry = require("@opentelemetry/api"); 6 | const { registerInstrumentations } = require("@opentelemetry/instrumentation"); 7 | const { 8 | getNodeAutoInstrumentations 9 | } = require("@opentelemetry/auto-instrumentations-node"); 10 | const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace-base"); 11 | const { Resource } = require("@opentelemetry/resources"); 12 | const { 13 | SemanticResourceAttributes 14 | } = require("@opentelemetry/semantic-conventions"); 15 | const { 16 | OTLPTraceExporter 17 | } = require("@opentelemetry/exporter-trace-otlp-http"); 18 | 19 | const sdks = []; 20 | 21 | const exporter = new OTLPTraceExporter(); 22 | 23 | module.exports = (serviceName) => { 24 | if (sdks[serviceName]) return sdks[serviceName]; 25 | 26 | const provider = new NodeTracerProvider({ 27 | resource: new Resource({ 28 | [SemanticResourceAttributes.SERVICE_NAME]: serviceName 29 | }) 30 | }); 31 | 32 | provider.addSpanProcessor( 33 | new BatchSpanProcessor(exporter, { 34 | // The maximum queue size. After the size is reached spans are dropped. 35 | maxQueueSize: 1000, 36 | // The interval between two consecutive exports 37 | scheduledDelayMillis: 30000 38 | }) 39 | ); 40 | provider.register(); 41 | registerInstrumentations({ 42 | instrumentations: getNodeAutoInstrumentations({ 43 | "@opentelemetry/instrumentation-fs": { 44 | enabled: false 45 | } 46 | }) 47 | }); 48 | 49 | return opentelemetry.trace.getTracer(serviceName); 50 | }; 51 | -------------------------------------------------------------------------------- /_Resources/cart-service/models/Item.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ItemSchema = mongoose.Schema({ 4 | sku: {type: Number, required: true, index: {unique: true}}, 5 | name: {type: String, required: true}, 6 | price: {type: Number, required: true}, 7 | }, { 8 | timestamps: true, 9 | }); 10 | 11 | module.exports = mongoose.model('Item', ItemSchema); -------------------------------------------------------------------------------- /_Resources/cart-service/models/Order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const orderItemSchema = new mongoose.Schema({ 4 | sku: Number, 5 | qty: Number, 6 | name: String, 7 | price: Number, 8 | }); 9 | 10 | const orderSchema = new mongoose.Schema({ 11 | user: { 12 | type: mongoose.Schema.Types.ObjectId, 13 | ref: "User", 14 | }, 15 | email: String, 16 | status: String, 17 | items: [orderItemSchema], 18 | }); 19 | 20 | const Order = mongoose.model("Order", orderSchema); 21 | const OrderItem = mongoose.model("OrderItem", orderItemSchema); 22 | 23 | 24 | module.exports = { Order, OrderItem }; -------------------------------------------------------------------------------- /_Resources/cart-service/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const crypto = require("crypto"); 3 | 4 | const UserSchema = mongoose.Schema( 5 | { 6 | email: { 7 | // Trim and lowercase 8 | type: String, 9 | required: true, 10 | index: { unique: true }, 11 | lowercase: true, 12 | trim: true 13 | }, 14 | password: { 15 | type: String, 16 | required: true, 17 | trim: true 18 | }, 19 | salt: { 20 | type: String 21 | } 22 | }, 23 | { timestamps: true } 24 | ); 25 | 26 | UserSchema.pre("save", function preSave(next) { 27 | const user = this; 28 | 29 | if (user.isModified("password")) { 30 | // Generating a random salt for each user 31 | return crypto.randomBytes(16, (err, salt) => { 32 | if (err) return next(err); 33 | 34 | const saltHex = salt.toString("hex"); 35 | user.salt = saltHex; 36 | 37 | // Using PBKDF2 to hash the password 38 | return crypto.pbkdf2( 39 | user.password, 40 | salt, 41 | 100000, 42 | 64, 43 | "sha512", 44 | (pkerr, derivedKey) => { 45 | if (pkerr) return next(pkerr); 46 | user.password = derivedKey.toString("hex"); 47 | return next(); 48 | } 49 | ); 50 | }); 51 | } 52 | return next(); 53 | }); 54 | 55 | UserSchema.methods.comparePassword = function comparePassword( 56 | candidatePassword, 57 | callback 58 | ) { 59 | // Using PBKDF2 to hash the candidate password and then compare it with the stored hash 60 | crypto.pbkdf2( 61 | candidatePassword, 62 | this.salt, 63 | 100000, 64 | 64, 65 | "sha512", 66 | (err, derivedKey) => { 67 | if (err) return callback(err); 68 | return callback(null, derivedKey.toString("hex") === this.password); 69 | } 70 | ); 71 | }; 72 | 73 | module.exports = mongoose.model("User", UserSchema); 74 | -------------------------------------------------------------------------------- /_Resources/cart-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cart-service", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "", 6 | "scripts": { 7 | "start": "node ./bin/start", 8 | "dev": "nodemon ./bin/start" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@opentelemetry/api": "^1.4.1", 14 | "@opentelemetry/auto-instrumentations-node": "^0.37.0", 15 | "@opentelemetry/exporter-trace-otlp-http": "^0.39.1", 16 | "@opentelemetry/sdk-metrics": "^1.13.0", 17 | "@opentelemetry/sdk-node": "^0.39.1", 18 | "@redis/client": "^1.5.7", 19 | "axios": "^1.4.0", 20 | "express": "^4.18.2", 21 | "got": "^12.6.0", 22 | "mongoose": "^7.1.2", 23 | "morgan": "^1.10.0" 24 | }, 25 | "devDependencies": { 26 | "eslint": "^7.32.0", 27 | "eslint-config-airbnb-base": "^14.2.0", 28 | "eslint-config-prettier": "^6.12.0", 29 | "eslint-plugin-import": "^2.22.1", 30 | "eslint-plugin-prettier": "^3.1.4", 31 | "nodemon": "^2.0.4", 32 | "prettier": "^2.8.7" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /_Resources/cart-service/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const CartService = require("../lib/CartService"); 3 | 4 | const router = express.Router(); 5 | 6 | router.get("/items/:userId", async (req, res) => { 7 | try { 8 | const items = await CartService.getAll(req.params.userId); 9 | 10 | return res.json(items); 11 | } catch (error) { 12 | console.error(error); 13 | return res.status(500).send("General error"); 14 | } 15 | }); 16 | 17 | router.post("/items/:userId", async (req, res) => { 18 | try { 19 | await CartService.add(req.params.userId, req.body.itemId); 20 | return res.status(204).send(); 21 | } catch (error) { 22 | console.error(error); 23 | return res.status(500).send("General error"); 24 | } 25 | }); 26 | 27 | router.delete("/items/:userId/:itemId", async (req, res) => { 28 | try { 29 | await CartService.remove(req.params.userId, req.params.itemId); 30 | res.status(204).send(); 31 | } catch (error) { 32 | console.error(error); 33 | return res.status(500).send("General error"); 34 | } 35 | }); 36 | module.exports = router; 37 | -------------------------------------------------------------------------------- /_Resources/order-service/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["airbnb-base", "prettier"], 8 | "plugins": ["prettier", "import"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "no-console": 0, 18 | "no-unused-vars": 0, 19 | "no-underscore-dangle": 0, 20 | "no-undef": 1, 21 | "import/no-extraneous-dependencies": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /_Resources/order-service/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .DS_STORe -------------------------------------------------------------------------------- /_Resources/order-service/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "trailingComma": "none" 5 | } -------------------------------------------------------------------------------- /_Resources/order-service/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const app = express(); 4 | const morgan = require("morgan"); 5 | const routes = require("./routes"); 6 | const config = require("./config"); 7 | 8 | // Middleware to parse JSON request bodies 9 | app.use(express.json()); 10 | 11 | // Middleware to log HTTP requests 12 | app.use(morgan("tiny")); 13 | 14 | // Mount the router 15 | app.use("/", routes); 16 | 17 | // Error handling middleware 18 | app.use((err, req, res, next) => { 19 | const status = err.status || 500; 20 | const message = err.message || "Internal Server Error"; 21 | // You can also log the error to a file or console 22 | console.error(err); 23 | 24 | res.status(status).json({ 25 | error: { 26 | message, 27 | status 28 | } 29 | }); 30 | }); 31 | 32 | module.exports = app; 33 | -------------------------------------------------------------------------------- /_Resources/order-service/bin/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable import/order */ 3 | 4 | // Import necessary dependencies 5 | // HTTP server functionality 6 | const config = require("../config"); // Configuration settings 7 | 8 | // eslint-disable-next-line no-unused-vars 9 | const tracing = require("../lib/tracing")( 10 | `${config.serviceName}:${config.serviceVersion}` 11 | ); 12 | // Import necessary dependencies 13 | // HTTP server functionality 14 | 15 | const http = require("http"); 16 | const axios = require("axios"); 17 | 18 | const connectToMongoose = require("../lib/mongooseConnection"); // Function to connect to MongoDB 19 | // const connectToRedis = require("../lib/redisConnection"); // Function to connect to Redis 20 | 21 | // Prepare the Redis client to connect to later 22 | // This has to come before `app` is initiated because sessions depend on it 23 | // config.redis.client = connectToRedis(config.redis.options); 24 | 25 | const app = require("../app"); // Express application 26 | 27 | // Create the HTTP server with the express app 28 | const server = http.createServer(app); 29 | 30 | // Attach error and listening handlers to the server 31 | server.on("listening", () => { 32 | const addr = server.address(); 33 | const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; 34 | 35 | const registerService = () => 36 | axios 37 | .put( 38 | `http://localhost:3080/register/${config.serviceName}/${ 39 | config.serviceVersion 40 | }/${server.address().port}` 41 | ) 42 | .catch((err) => console.error(err)); 43 | const unregisterService = () => 44 | axios 45 | .delete( 46 | `http://localhost:3080/register/${config.serviceName}/${ 47 | config.serviceVersion 48 | }/${server.address().port}` 49 | ) 50 | .catch((err) => console.error(err)); 51 | 52 | registerService(); 53 | const interval = setInterval(registerService, 15000); 54 | const cleanup = async () => { 55 | let clean = false; 56 | if (!clean) { 57 | clean = true; 58 | clearInterval(interval); 59 | await unregisterService(); 60 | } 61 | }; 62 | 63 | process.on("uncaughtException", async () => { 64 | await cleanup(); 65 | process.exit(0); 66 | }); 67 | 68 | process.on("SIGINT", async () => { 69 | await cleanup(); 70 | process.exit(0); 71 | }); 72 | 73 | process.on("SIGTERM", async () => { 74 | await cleanup(); 75 | process.exit(0); 76 | }); 77 | 78 | console.info( 79 | `${config.serviceName}:${config.serviceVersion} listening on ${bind}` 80 | ); 81 | }); 82 | 83 | // Start the server 84 | 85 | connectToMongoose(config.mongodb.url).then(() => server.listen(0)); 86 | -------------------------------------------------------------------------------- /_Resources/order-service/config.js: -------------------------------------------------------------------------------- 1 | const pkg = require("./package.json"); 2 | 3 | module.exports = { 4 | serviceName: pkg.name, 5 | serviceVersion: pkg.version, 6 | mongodb: { 7 | url: "mongodb://localhost:37017/shopper" 8 | }, 9 | redis: { 10 | options: { 11 | url: "redis://localhost:7379" 12 | }, 13 | client: null 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /_Resources/order-service/lib/OrderService.js: -------------------------------------------------------------------------------- 1 | /** @module OrderService */ 2 | 3 | // Import the Order and User models from mongoose 4 | const { Order } = require("../models/Order"); 5 | 6 | /** 7 | * Service class for managing orders 8 | */ 9 | class OrderService { 10 | /** 11 | * Create a new order 12 | * @param {Object} user - The user who is creating the order 13 | * @param {Array} items - The items in the order 14 | * @returns {Promise} - A promise that resolves to the new order 15 | */ 16 | static async create(userId, email, items) { 17 | const order = new Order({ 18 | userId, 19 | email, 20 | status: "Not Shipped", 21 | items 22 | }); 23 | 24 | await order.save(); 25 | 26 | return order; 27 | } 28 | 29 | /** 30 | * Get all orders 31 | * @returns {Promise} - A promise that resolves to an array of orders 32 | */ 33 | static async getAll() { 34 | return Order.find().populate("items"); 35 | } 36 | 37 | /** 38 | * Update the status of an order 39 | * @param {string} orderId - The ID of the order to update 40 | * @param {string} status - The new status 41 | * @returns {Promise} - A promise that resolves to the updated 42 | * order, or null if no order was found 43 | */ 44 | static async setStatus(orderId, status) { 45 | return Order.findByIdAndUpdate(orderId, { status }, { new: true }); 46 | } 47 | } 48 | 49 | module.exports = OrderService; 50 | -------------------------------------------------------------------------------- /_Resources/order-service/lib/mongooseConnection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const connectToMongoose = async (connectionString) => { 4 | try { 5 | await mongoose.connect(connectionString); 6 | 7 | console.log("Connected to MongoDB"); 8 | } catch (error) { 9 | console.error("Error connecting to Mongoose:", error); 10 | process.exit(1); 11 | } 12 | }; 13 | 14 | module.exports = connectToMongoose; 15 | -------------------------------------------------------------------------------- /_Resources/order-service/lib/redisConnection.js: -------------------------------------------------------------------------------- 1 | const redis = require('@redis/client'); 2 | 3 | const connectToRedis = (options) => { 4 | const client = redis.createClient(options); 5 | 6 | client.on('connect', () => { 7 | console.log('Connected to Redis'); 8 | }); 9 | 10 | client.on('error', (error) => { 11 | console.error('Error connecting to Redis:', error); 12 | process.exit(1); 13 | }); 14 | 15 | return client; 16 | }; 17 | 18 | module.exports = connectToRedis; 19 | -------------------------------------------------------------------------------- /_Resources/order-service/lib/tracing.js: -------------------------------------------------------------------------------- 1 | const { node } = require("@opentelemetry/sdk-node"); 2 | 3 | const { NodeTracerProvider } = node; 4 | 5 | const opentelemetry = require("@opentelemetry/api"); 6 | const { registerInstrumentations } = require("@opentelemetry/instrumentation"); 7 | const { 8 | getNodeAutoInstrumentations 9 | } = require("@opentelemetry/auto-instrumentations-node"); 10 | const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace-base"); 11 | const { Resource } = require("@opentelemetry/resources"); 12 | const { 13 | SemanticResourceAttributes 14 | } = require("@opentelemetry/semantic-conventions"); 15 | const { 16 | OTLPTraceExporter 17 | } = require("@opentelemetry/exporter-trace-otlp-http"); 18 | 19 | const sdks = []; 20 | 21 | const exporter = new OTLPTraceExporter(); 22 | 23 | module.exports = (serviceName) => { 24 | if (sdks[serviceName]) return sdks[serviceName]; 25 | 26 | const provider = new NodeTracerProvider({ 27 | resource: new Resource({ 28 | [SemanticResourceAttributes.SERVICE_NAME]: serviceName 29 | }) 30 | }); 31 | 32 | provider.addSpanProcessor( 33 | new BatchSpanProcessor(exporter, { 34 | // The maximum queue size. After the size is reached spans are dropped. 35 | maxQueueSize: 1000, 36 | // The interval between two consecutive exports 37 | scheduledDelayMillis: 30000 38 | }) 39 | ); 40 | provider.register(); 41 | registerInstrumentations({ 42 | instrumentations: getNodeAutoInstrumentations({ 43 | "@opentelemetry/instrumentation-fs": { 44 | enabled: false 45 | } 46 | }) 47 | }); 48 | 49 | return opentelemetry.trace.getTracer(serviceName); 50 | }; 51 | -------------------------------------------------------------------------------- /_Resources/order-service/models/Order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const orderItemSchema = new mongoose.Schema({ 4 | sku: Number, 5 | qty: Number, 6 | name: String, 7 | price: Number 8 | }); 9 | 10 | const orderSchema = new mongoose.Schema({ 11 | userId: { 12 | type: mongoose.Schema.Types.ObjectId 13 | }, 14 | email: String, 15 | status: String, 16 | items: [orderItemSchema] 17 | }); 18 | 19 | const Order = mongoose.model("Order", orderSchema); 20 | const OrderItem = mongoose.model("OrderItem", orderItemSchema); 21 | 22 | module.exports = { Order, OrderItem }; 23 | -------------------------------------------------------------------------------- /_Resources/order-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "order-service", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "", 6 | "scripts": { 7 | "start": "node ./bin/start", 8 | "dev": "nodemon ./bin/start" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^1.4.0", 14 | "express": "^4.18.2", 15 | "got": "^12.6.0", 16 | "mongoose": "^7.1.1", 17 | "morgan": "^1.10.0", 18 | "@opentelemetry/api": "^1.4.1", 19 | "@opentelemetry/auto-instrumentations-node": "^0.37.0", 20 | "@opentelemetry/exporter-trace-otlp-http": "^0.39.1", 21 | "@opentelemetry/sdk-metrics": "^1.13.0", 22 | "@opentelemetry/sdk-node": "^0.39.1" 23 | }, 24 | "devDependencies": { 25 | "eslint": "^7.32.0", 26 | "eslint-config-airbnb-base": "^14.2.0", 27 | "eslint-config-prettier": "^6.12.0", 28 | "eslint-plugin-import": "^2.22.1", 29 | "eslint-plugin-prettier": "^3.1.4", 30 | "nodemon": "^2.0.4", 31 | "prettier": "^2.8.7" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /_Resources/order-service/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const OrderService = require("../lib/OrderService"); 3 | 4 | const router = express.Router(); 5 | 6 | function createJson(order) { 7 | return { 8 | id: order.id, 9 | userId: order.userId, 10 | email: order.email, 11 | sku: order.sku, 12 | status: order.status, 13 | items: order.items 14 | }; 15 | } 16 | 17 | router.get("/orders", async (req, res) => { 18 | try { 19 | const orders = await OrderService.getAll(); 20 | return res.json(orders.map(createJson)); 21 | } catch (error) { 22 | console.error(error); 23 | throw error; 24 | } 25 | }); 26 | 27 | router.post("/orders", async (req, res) => { 28 | try { 29 | const order = await OrderService.create( 30 | req.body.userId, 31 | req.body.email, 32 | req.body.items 33 | ); 34 | if (!order) { 35 | res.status(404).send("Error creating order"); 36 | } else { 37 | res.json(createJson(order)); 38 | } 39 | } catch (error) { 40 | console.error(error); 41 | res.status(500).send("General error"); 42 | } 43 | }); 44 | 45 | router.put("/orders/:id", async (req, res) => { 46 | try { 47 | const updatedOrder = await OrderService.setStatus( 48 | req.params.id, 49 | req.body.status 50 | ); 51 | if (!updatedOrder) return res.status(404).send("Order not found"); 52 | return res.json(createJson(updatedOrder)); 53 | } catch (error) { 54 | console.error(error); 55 | return res.status(500).send("General error"); 56 | } 57 | }); 58 | 59 | module.exports = router; 60 | -------------------------------------------------------------------------------- /_Resources/registry-service/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["airbnb-base", "prettier"], 8 | "plugins": ["prettier", "import"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "no-console": 0, 18 | "no-unused-vars": 0, 19 | "no-underscore-dangle": 0, 20 | "import/no-extraneous-dependencies": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /_Resources/registry-service/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .DS_STORe -------------------------------------------------------------------------------- /_Resources/registry-service/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "trailingComma": "none" 5 | } -------------------------------------------------------------------------------- /_Resources/registry-service/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const app = express(); 4 | const morgan = require("morgan"); 5 | const routes = require("./routes"); 6 | const config = require("./config"); 7 | 8 | // Middleware to parse JSON request bodies 9 | app.use(express.json()); 10 | 11 | // Middleware to log HTTP requests 12 | app.use(morgan("tiny")); 13 | 14 | // Mount the router 15 | app.use("/", routes); 16 | // Error handling middleware 17 | app.use((err, req, res, next) => { 18 | const status = err.status || 500; 19 | const message = err.message || "Internal Server Error"; 20 | // You can also log the error to a file or console 21 | console.error(err); 22 | 23 | res.status(status).json({ 24 | error: { 25 | message, 26 | status 27 | } 28 | }); 29 | }); 30 | module.exports = app; 31 | -------------------------------------------------------------------------------- /_Resources/registry-service/bin/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable import/order */ 3 | 4 | // Import necessary dependencies 5 | // HTTP server functionality 6 | const config = require("../config"); // Configuration settings 7 | 8 | // eslint-disable-next-line no-unused-vars 9 | const tracing = require("../lib/tracing")( 10 | `${config.serviceName}:${config.serviceVersion}` 11 | ); 12 | 13 | // Import necessary dependencies 14 | const http = require("http"); // HTTP server functionality 15 | 16 | const app = require("../app"); // Express application 17 | 18 | // Create the HTTP server with the express app 19 | const server = http.createServer(app); 20 | 21 | // Attach error and listening handlers to the server 22 | server.on("listening", () => { 23 | const addr = server.address(); 24 | const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; 25 | console.info( 26 | `${config.serviceName}:${config.serviceVersion} listening on ${bind}` 27 | ); 28 | }); 29 | 30 | // Start the server 31 | server.listen(3080); 32 | -------------------------------------------------------------------------------- /_Resources/registry-service/config.js: -------------------------------------------------------------------------------- 1 | const pkg = require("./package.json"); 2 | 3 | module.exports = { 4 | serviceName: pkg.name, 5 | serviceVersion: pkg.version, 6 | mongodb: { 7 | url: "mongodb://localhost:37017/shopper" 8 | }, 9 | redis: { 10 | options: { 11 | url: "redis://localhost:7379" 12 | }, 13 | client: null 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /_Resources/registry-service/lib/tracing.js: -------------------------------------------------------------------------------- 1 | const { node } = require("@opentelemetry/sdk-node"); 2 | 3 | const { NodeTracerProvider } = node; 4 | 5 | const opentelemetry = require("@opentelemetry/api"); 6 | const { registerInstrumentations } = require("@opentelemetry/instrumentation"); 7 | const { 8 | getNodeAutoInstrumentations 9 | } = require("@opentelemetry/auto-instrumentations-node"); 10 | const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace-base"); 11 | const { Resource } = require("@opentelemetry/resources"); 12 | const { 13 | SemanticResourceAttributes 14 | } = require("@opentelemetry/semantic-conventions"); 15 | const { 16 | OTLPTraceExporter 17 | } = require("@opentelemetry/exporter-trace-otlp-http"); 18 | 19 | const sdks = []; 20 | 21 | const exporter = new OTLPTraceExporter(); 22 | 23 | module.exports = (serviceName) => { 24 | if (sdks[serviceName]) return sdks[serviceName]; 25 | 26 | const provider = new NodeTracerProvider({ 27 | resource: new Resource({ 28 | [SemanticResourceAttributes.SERVICE_NAME]: serviceName 29 | }) 30 | }); 31 | 32 | provider.addSpanProcessor( 33 | new BatchSpanProcessor(exporter, { 34 | // The maximum queue size. After the size is reached spans are dropped. 35 | maxQueueSize: 1000, 36 | // The interval between two consecutive exports 37 | scheduledDelayMillis: 30000 38 | }) 39 | ); 40 | provider.register(); 41 | registerInstrumentations({ 42 | instrumentations: getNodeAutoInstrumentations({ 43 | "@opentelemetry/instrumentation-fs": { 44 | enabled: false 45 | } 46 | }) 47 | }); 48 | 49 | return opentelemetry.trace.getTracer(serviceName); 50 | }; 51 | -------------------------------------------------------------------------------- /_Resources/registry-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "registry-service", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "", 6 | "scripts": { 7 | "start": "node ./bin/start", 8 | "dev": "nodemon ./bin/start" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.18.2", 14 | "morgan": "^1.10.0", 15 | "semver": "^7.5.1", 16 | "@opentelemetry/api": "^1.4.1", 17 | "@opentelemetry/auto-instrumentations-node": "^0.37.0", 18 | "@opentelemetry/exporter-trace-otlp-http": "^0.39.1", 19 | "@opentelemetry/sdk-metrics": "^1.13.0", 20 | "@opentelemetry/sdk-node": "^0.39.1" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^7.32.0", 24 | "eslint-config-airbnb-base": "^14.2.0", 25 | "eslint-config-prettier": "^6.12.0", 26 | "eslint-plugin-import": "^2.22.1", 27 | "eslint-plugin-prettier": "^3.1.4", 28 | "nodemon": "^2.0.4", 29 | "prettier": "^2.8.7" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /_Resources/registry-service/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const router = express.Router(); 4 | 5 | module.exports = router; -------------------------------------------------------------------------------- /_Resources/user-service/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["airbnb-base", "prettier"], 8 | "plugins": ["prettier", "import"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "no-console": 0, 18 | "no-unused-vars": 0, 19 | "no-underscore-dangle": 0, 20 | "no-undef": 1, 21 | "import/no-extraneous-dependencies": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /_Resources/user-service/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .DS_STORe -------------------------------------------------------------------------------- /_Resources/user-service/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "trailingComma": "none" 5 | } -------------------------------------------------------------------------------- /_Resources/user-service/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const app = express(); 4 | const morgan = require("morgan"); 5 | const routes = require("./routes"); 6 | const config = require("./config"); 7 | 8 | // Middleware to parse JSON request bodies 9 | app.use(express.json()); 10 | 11 | // Middleware to log HTTP requests 12 | app.use(morgan("tiny")); 13 | 14 | // Mount the router 15 | app.use("/", routes); 16 | 17 | // Error handling middleware 18 | app.use((err, req, res, next) => { 19 | const status = err.status || 500; 20 | const message = err.message || "Internal Server Error"; 21 | // You can also log the error to a file or console 22 | console.error(err); 23 | 24 | res.status(status).json({ 25 | error: { 26 | message, 27 | status 28 | } 29 | }); 30 | }); 31 | 32 | module.exports = app; 33 | -------------------------------------------------------------------------------- /_Resources/user-service/bin/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable import/order */ 3 | 4 | // Import necessary dependencies 5 | // HTTP server functionality 6 | const config = require("../config"); // Configuration settings 7 | 8 | // eslint-disable-next-line no-unused-vars 9 | const tracing = require("../lib/tracing")( 10 | `${config.serviceName}:${config.serviceVersion}` 11 | ); 12 | // Import necessary dependencies 13 | // HTTP server functionality 14 | 15 | const http = require("http"); 16 | const axios = require("axios"); 17 | 18 | const connectToMongoose = require("../lib/mongooseConnection"); // Function to connect to MongoDB 19 | // const connectToRedis = require("../lib/redisConnection"); // Function to connect to Redis 20 | 21 | // Prepare the Redis client to connect to later 22 | // This has to come before `app` is initiated because sessions depend on it 23 | // config.redis.client = connectToRedis(config.redis.options); 24 | 25 | const app = require("../app"); // Express application 26 | 27 | // Create the HTTP server with the express app 28 | const server = http.createServer(app); 29 | 30 | // Attach error and listening handlers to the server 31 | server.on("listening", () => { 32 | const addr = server.address(); 33 | const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; 34 | 35 | const registerService = () => 36 | axios 37 | .put( 38 | `http://localhost:3080/register/${config.serviceName}/${ 39 | config.serviceVersion 40 | }/${server.address().port}` 41 | ) 42 | .catch((err) => console.error(err)); 43 | const unregisterService = () => 44 | axios 45 | .delete( 46 | `http://localhost:3080/register/${config.serviceName}/${ 47 | config.serviceVersion 48 | }/${server.address().port}` 49 | ) 50 | .catch((err) => console.error(err)); 51 | 52 | registerService(); 53 | const interval = setInterval(registerService, 15000); 54 | const cleanup = async () => { 55 | let clean = false; 56 | if (!clean) { 57 | clean = true; 58 | clearInterval(interval); 59 | await unregisterService(); 60 | } 61 | }; 62 | 63 | process.on("uncaughtException", async () => { 64 | await cleanup(); 65 | process.exit(0); 66 | }); 67 | 68 | process.on("SIGINT", async () => { 69 | await cleanup(); 70 | process.exit(0); 71 | }); 72 | 73 | process.on("SIGTERM", async () => { 74 | await cleanup(); 75 | process.exit(0); 76 | }); 77 | 78 | console.info( 79 | `${config.serviceName}:${config.serviceVersion} listening on ${bind}` 80 | ); 81 | }); 82 | 83 | // Start the server 84 | 85 | connectToMongoose(config.mongodb.url).then(() => server.listen(0)); 86 | -------------------------------------------------------------------------------- /_Resources/user-service/config.js: -------------------------------------------------------------------------------- 1 | const pkg = require("./package.json"); 2 | 3 | module.exports = { 4 | serviceName: pkg.name, 5 | serviceVersion: pkg.version, 6 | mongodb: { 7 | url: "mongodb://localhost:37017/shopper" 8 | }, 9 | redis: { 10 | options: { 11 | url: "redis://localhost:7379" 12 | }, 13 | client: null 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /_Resources/user-service/lib/UserService.js: -------------------------------------------------------------------------------- 1 | /** @module UserService */ 2 | 3 | // Import the User model from mongoose 4 | const UserModel = require("../models/User"); 5 | 6 | /** 7 | * Service class for managing users 8 | */ 9 | class UserService { 10 | /** 11 | * Get all users 12 | * @returns {Promise} - A promise that resolves to an array of users 13 | */ 14 | static async getAll() { 15 | return UserModel.find({}).sort({ createdAt: -1 }); 16 | } 17 | 18 | /** 19 | * Get a user by ID 20 | * @param {string} userId - The ID of the user to retrieve 21 | * @returns {Promise} - A promise that resolves to the user, or 22 | * null if no user was found 23 | */ 24 | static async getOne(userId) { 25 | return UserModel.findById(userId).exec(); 26 | } 27 | 28 | /** 29 | * Create a new user 30 | * @param {Object} data - The data for the new user 31 | * @returns {Promise} - A promise that resolves to the new user 32 | */ 33 | static async create(data) { 34 | const user = new UserModel(data); 35 | return user.save(); 36 | } 37 | 38 | /** 39 | * Authenticate a user 40 | * @param {string} email - The email address 41 | * @param {string} password - The email password 42 | * @returns {Promise} - A promise that either returns the authenticated user or false 43 | */ 44 | static async authenticate(email, password) { 45 | const maybeUser = await UserModel.findOne({ email }); 46 | if (!maybeUser) return false; 47 | const validPassword = await maybeUser.comparePassword(password); 48 | if (!validPassword) return false; 49 | return maybeUser; 50 | } 51 | 52 | /** 53 | * Update a user's data 54 | * @param {string} userId - The ID of the user to update 55 | * @param {Object} data - The new data for the user 56 | * @returns {Promise} - A promise that resolves to the updated user 57 | */ 58 | static async update(userId, data) { 59 | // Fetch the user first 60 | const user = await UserModel.findById(userId); 61 | user.email = data.email; 62 | user.isAdmin = data.isAdmin; 63 | 64 | // Only set the password if it was modified 65 | if (data.password) { 66 | user.password = data.password; 67 | } 68 | 69 | return user.save(); 70 | } 71 | 72 | /** 73 | * Remove a user 74 | * @param {string} userId - The ID of the user to remove 75 | * @returns {Promise} - A promise that resolves to the result of the 76 | * delete operation 77 | */ 78 | static async remove(userId) { 79 | return UserModel.deleteOne({ _id: userId }).exec(); 80 | } 81 | } 82 | 83 | module.exports = UserService; 84 | -------------------------------------------------------------------------------- /_Resources/user-service/lib/mongooseConnection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const connectToMongoose = async (connectionString) => { 4 | try { 5 | await mongoose.connect(connectionString); 6 | 7 | console.log("Connected to MongoDB"); 8 | } catch (error) { 9 | console.error("Error connecting to Mongoose:", error); 10 | process.exit(1); 11 | } 12 | }; 13 | 14 | module.exports = connectToMongoose; 15 | -------------------------------------------------------------------------------- /_Resources/user-service/lib/redisConnection.js: -------------------------------------------------------------------------------- 1 | const redis = require('@redis/client'); 2 | 3 | const connectToRedis = (options) => { 4 | const client = redis.createClient(options); 5 | 6 | client.on('connect', () => { 7 | console.log('Connected to Redis'); 8 | }); 9 | 10 | client.on('error', (error) => { 11 | console.error('Error connecting to Redis:', error); 12 | process.exit(1); 13 | }); 14 | 15 | return client; 16 | }; 17 | 18 | module.exports = connectToRedis; 19 | -------------------------------------------------------------------------------- /_Resources/user-service/lib/tracing.js: -------------------------------------------------------------------------------- 1 | const { node } = require("@opentelemetry/sdk-node"); 2 | 3 | const { NodeTracerProvider } = node; 4 | 5 | const opentelemetry = require("@opentelemetry/api"); 6 | const { registerInstrumentations } = require("@opentelemetry/instrumentation"); 7 | const { 8 | getNodeAutoInstrumentations 9 | } = require("@opentelemetry/auto-instrumentations-node"); 10 | const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace-base"); 11 | const { Resource } = require("@opentelemetry/resources"); 12 | const { 13 | SemanticResourceAttributes 14 | } = require("@opentelemetry/semantic-conventions"); 15 | const { 16 | OTLPTraceExporter 17 | } = require("@opentelemetry/exporter-trace-otlp-http"); 18 | 19 | const sdks = []; 20 | 21 | const exporter = new OTLPTraceExporter(); 22 | 23 | module.exports = (serviceName) => { 24 | if (sdks[serviceName]) return sdks[serviceName]; 25 | 26 | const provider = new NodeTracerProvider({ 27 | resource: new Resource({ 28 | [SemanticResourceAttributes.SERVICE_NAME]: serviceName 29 | }) 30 | }); 31 | 32 | provider.addSpanProcessor( 33 | new BatchSpanProcessor(exporter, { 34 | // The maximum queue size. After the size is reached spans are dropped. 35 | maxQueueSize: 1000, 36 | // The interval between two consecutive exports 37 | scheduledDelayMillis: 30000 38 | }) 39 | ); 40 | provider.register(); 41 | registerInstrumentations({ 42 | instrumentations: getNodeAutoInstrumentations({ 43 | "@opentelemetry/instrumentation-fs": { 44 | enabled: false 45 | } 46 | }) 47 | }); 48 | 49 | return opentelemetry.trace.getTracer(serviceName); 50 | }; 51 | -------------------------------------------------------------------------------- /_Resources/user-service/models/Item.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ItemSchema = mongoose.Schema({ 4 | sku: {type: Number, required: true, index: {unique: true}}, 5 | name: {type: String, required: true}, 6 | price: {type: Number, required: true}, 7 | }, { 8 | timestamps: true, 9 | }); 10 | 11 | module.exports = mongoose.model('Item', ItemSchema); -------------------------------------------------------------------------------- /_Resources/user-service/models/Order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const orderItemSchema = new mongoose.Schema({ 4 | sku: Number, 5 | qty: Number, 6 | name: String, 7 | price: Number, 8 | }); 9 | 10 | const orderSchema = new mongoose.Schema({ 11 | user: { 12 | type: mongoose.Schema.Types.ObjectId, 13 | ref: "User", 14 | }, 15 | email: String, 16 | status: String, 17 | items: [orderItemSchema], 18 | }); 19 | 20 | const Order = mongoose.model("Order", orderSchema); 21 | const OrderItem = mongoose.model("OrderItem", orderItemSchema); 22 | 23 | 24 | module.exports = { Order, OrderItem }; -------------------------------------------------------------------------------- /_Resources/user-service/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const crypto = require("crypto"); 3 | 4 | const UserSchema = mongoose.Schema( 5 | { 6 | email: { 7 | // Trim and lowercase 8 | type: String, 9 | required: true, 10 | index: { unique: true }, 11 | lowercase: true, 12 | trim: true 13 | }, 14 | password: { 15 | type: String, 16 | required: true, 17 | trim: true 18 | }, 19 | isAdmin: { 20 | type: Boolean, 21 | default: false 22 | }, 23 | salt: { 24 | type: String 25 | } 26 | }, 27 | { timestamps: true } 28 | ); 29 | 30 | UserSchema.pre("save", function preSave(next) { 31 | const user = this; 32 | 33 | if (user.isModified("password")) { 34 | // Generating a random salt for each user 35 | return crypto.randomBytes(16, (err, salt) => { 36 | if (err) return next(err); 37 | 38 | const saltHex = salt.toString("hex"); 39 | user.salt = saltHex; 40 | 41 | // Using PBKDF2 to hash the password 42 | return crypto.pbkdf2( 43 | user.password, 44 | salt, 45 | 100000, 46 | 64, 47 | "sha512", 48 | (pkerr, derivedKey) => { 49 | if (pkerr) return next(pkerr); 50 | user.password = derivedKey.toString("hex"); 51 | return next(); 52 | } 53 | ); 54 | }); 55 | } 56 | return next(); 57 | }); 58 | 59 | UserSchema.methods.comparePassword = async function comparePassword( 60 | candidatePassword 61 | ) { 62 | return new Promise((resolve, reject) => { 63 | // Using PBKDF2 to hash the candidate password and then compare it with the stored hash 64 | crypto.pbkdf2( 65 | candidatePassword, 66 | Buffer.from(this.salt, "hex"), 67 | 100000, 68 | 64, 69 | "sha512", 70 | (err, derivedKey) => { 71 | if (err) reject(err); 72 | else resolve(derivedKey.toString("hex") === this.password); 73 | } 74 | ); 75 | }); 76 | }; 77 | 78 | module.exports = mongoose.model("User", UserSchema); 79 | -------------------------------------------------------------------------------- /_Resources/user-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-service", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "", 6 | "scripts": { 7 | "start": "node ./bin/start", 8 | "dev": "nodemon ./bin/start" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^1.4.0", 14 | "express": "^4.18.2", 15 | "got": "^12.6.0", 16 | "mongoose": "^7.1.1", 17 | "morgan": "^1.10.0", 18 | "@opentelemetry/api": "^1.4.1", 19 | "@opentelemetry/auto-instrumentations-node": "^0.37.0", 20 | "@opentelemetry/exporter-trace-otlp-http": "^0.39.1", 21 | "@opentelemetry/sdk-metrics": "^1.13.0", 22 | "@opentelemetry/sdk-node": "^0.39.1" 23 | }, 24 | "devDependencies": { 25 | "eslint": "^7.32.0", 26 | "eslint-config-airbnb-base": "^14.2.0", 27 | "eslint-config-prettier": "^6.12.0", 28 | "eslint-plugin-import": "^2.22.1", 29 | "eslint-plugin-prettier": "^3.1.4", 30 | "nodemon": "^2.0.4", 31 | "prettier": "^2.8.7" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /_Resources/user-service/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const UserService = require("../lib/UserService"); 3 | 4 | const router = express.Router(); 5 | 6 | function createJson(user) { 7 | return { id: user.id, email: user.email, isAdmin: user.isAdmin }; 8 | } 9 | 10 | router.get("/users", async (req, res) => { 11 | try { 12 | const users = await UserService.getAll(); 13 | return res.json(users.map(createJson)); 14 | } catch (error) { 15 | console.error(error); 16 | throw error; 17 | } 18 | }); 19 | 20 | router.get("/users/:id", async (req, res) => { 21 | try { 22 | const user = await UserService.getOne(req.params.id); 23 | if (!user) { 24 | res.status(404).send("User not found"); 25 | } else { 26 | res.json(createJson(user)); 27 | } 28 | } catch (error) { 29 | console.error(error); 30 | res.status(500).send("General error"); 31 | } 32 | }); 33 | 34 | router.post("/users", async (req, res) => { 35 | try { 36 | const newUser = await UserService.create(req.body); 37 | res.json(createJson(newUser)); 38 | } catch (error) { 39 | console.error(error); 40 | res.status(500).send("General error"); 41 | } 42 | }); 43 | 44 | router.put("/users/:id", async (req, res) => { 45 | try { 46 | const updatedUser = await UserService.update(req.params.id, req.body); 47 | if (!updatedUser) return res.status(404).send("User not found"); 48 | res.json(createJson(updatedUser)); 49 | } catch (error) { 50 | console.error(error); 51 | res.status(500).send("General error"); 52 | } 53 | }); 54 | 55 | router.post("/users/authenticate", async (req, res) => { 56 | try { 57 | const authUser = await UserService.authenticate( 58 | req.body.email, 59 | req.body.password 60 | ); 61 | if (!authUser) return res.status(403).send("User not found"); 62 | return res.json(createJson(authUser)); 63 | } catch (error) { 64 | console.error(error); 65 | res.status(500).send("General error"); 66 | } 67 | }); 68 | 69 | router.delete("/users/:id", async (req, res) => { 70 | try { 71 | const deletionResult = await UserService.remove(req.params.id); 72 | if (deletionResult.deletedCount === 0) { 73 | res.status(404).send("User not found"); 74 | } else { 75 | res.status(204).send(); 76 | } 77 | } catch (error) { 78 | console.error(error); 79 | res.status(500).send("General error"); 80 | } 81 | }); 82 | module.exports = router; 83 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInLearning/nodejs-microservices-4403064/10a30bb719e0126d1248d81c64a54c2a61a9a1f0/favicon.ico -------------------------------------------------------------------------------- /workspace/microservices/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInLearning/nodejs-microservices-4403064/10a30bb719e0126d1248d81c64a54c2a61a9a1f0/workspace/microservices/.gitkeep -------------------------------------------------------------------------------- /workspace/shopper/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["airbnb-base", "prettier"], 8 | "plugins": ["prettier", "import"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "no-console": 0, 18 | "no-unused-vars": 1, 19 | "no-underscore-dangle": 0, 20 | "no-undef": 1, 21 | "import/no-extraneous-dependencies": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /workspace/shopper/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .DS_STORe -------------------------------------------------------------------------------- /workspace/shopper/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "trailingComma": "none" 5 | } -------------------------------------------------------------------------------- /workspace/shopper/client/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 5rem; 3 | } -------------------------------------------------------------------------------- /workspace/shopper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopper", 3 | "version": "0.0.0", 4 | "private": true, 5 | "author": "Daniel Khan ", 6 | "scripts": { 7 | "start": "node ./server/bin/start", 8 | "dev": "nodemon ./server/bin/start" 9 | }, 10 | "dependencies": { 11 | "@redis/client": "^1.5.7", 12 | "axios": "^1.4.0", 13 | "body-parser": "~1.20.2", 14 | "connect-redis": "^7.1.0", 15 | "cookie-parser": "~1.4.6", 16 | "express": "~4.18.2", 17 | "express-session": "^1.17.3", 18 | "mongoose": "^7.1.1", 19 | "morgan": "^1.10.0", 20 | "pug": "~3.0.2", 21 | "redis": "^4.6.6", 22 | "@opentelemetry/api": "^1.4.1", 23 | "@opentelemetry/auto-instrumentations-node": "^0.37.0", 24 | "@opentelemetry/exporter-trace-otlp-http": "^0.39.1", 25 | "@opentelemetry/sdk-metrics": "^1.13.0", 26 | "@opentelemetry/sdk-node": "^0.39.1" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^8.40.0", 30 | "eslint-config-airbnb-base": "^15.0.0", 31 | "eslint-config-prettier": "^8.8.0", 32 | "eslint-plugin-import": "^2.27.5", 33 | "eslint-plugin-prettier": "^4.2.1", 34 | "nodemon": "^2.0.22", 35 | "prettier": "^2.8.8" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /workspace/shopper/server/app.js: -------------------------------------------------------------------------------- 1 | // Import necessary dependencies 2 | const express = require("express"); 3 | const path = require("path"); 4 | const bodyParser = require("body-parser"); 5 | const session = require("express-session"); 6 | const morgan = require("morgan"); 7 | const RedisStore = require("connect-redis").default; 8 | const { assignTemplateVariables } = require("./lib/middlewares"); 9 | const routeHandler = require("./routes"); 10 | 11 | const config = require("./config"); 12 | 13 | // Initialize express application 14 | const app = express(); 15 | 16 | // Set up view engine (Pug in this case) and views directory 17 | app.set("view engine", "pug"); 18 | app.set("views", path.join(__dirname, "views")); 19 | 20 | // Set up middleware 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | app.use(morgan("tiny")); // Log HTTP requests 24 | app.use(express.static(path.join(__dirname, "../client"))); // Serve static files 25 | 26 | // Initialize Redis session store 27 | const redisStore = new RedisStore({ 28 | client: config.redis.client, 29 | prefix: "shopper_session:" 30 | }); 31 | 32 | // Set up session middleware 33 | app.use( 34 | session({ 35 | store: redisStore, 36 | secret: "CHANGE ME!", 37 | resave: false, 38 | saveUninitialized: false 39 | }) 40 | ); 41 | 42 | // Ignore requests for favicon and robots.txt 43 | app.get("/favicon.ico", (req, res) => res.status(204)); 44 | app.get("/robots.txt", (req, res) => res.status(204)); 45 | 46 | // Middleware to add 'global' template variables 47 | app.use(assignTemplateVariables); 48 | 49 | // Set up routes 50 | app.use("/", routeHandler); 51 | 52 | // Error handlers 53 | app.use((req, res, next) => { 54 | const err = new Error(`Not Found (${req.url})`); 55 | err.status = 404; 56 | next(err); 57 | }); 58 | 59 | app.use((err, req, res) => { 60 | res.locals.message = err.message; 61 | res.locals.error = req.app.get("env") === "development" ? err : {}; 62 | res.status(err.status || 500); 63 | res.render("error"); 64 | }); 65 | 66 | // Export the express application 67 | module.exports = app; 68 | -------------------------------------------------------------------------------- /workspace/shopper/server/bin/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable import/order */ 4 | 5 | // Import necessary dependencies 6 | // HTTP server functionality 7 | const config = require("../config"); // Configuration settings 8 | 9 | // eslint-disable-next-line no-unused-vars 10 | const tracing = require("../lib/tracing")( 11 | `${config.serviceName}:${config.serviceVersion}` 12 | ); 13 | // Import necessary dependencies 14 | const http = require("http"); // HTTP server functionality 15 | 16 | const connectToMongoose = require("../lib/mongooseConnection"); // Function to connect to MongoDB 17 | const connectToRedis = require("../lib/redisConnection"); // Function to connect to Redis 18 | 19 | // Prepare the Redis client to connect to later 20 | // This has to come before `app` is initiated because sessions depend on it 21 | config.redis.client = connectToRedis(config.redis.options); 22 | 23 | const app = require("../app"); // Express application 24 | 25 | // Set the port from the environment variable or use 3000 as default 26 | const port = process.env.PORT || "3000"; 27 | 28 | // Create the HTTP server with the express app 29 | const server = http.createServer(app); 30 | 31 | // Attach a listening handler 32 | server.on("listening", () => { 33 | const addr = server.address(); 34 | const bind = typeof addr === "string" ? `pipe ${addr}` : `port ${addr.port}`; 35 | console.info( 36 | `${config.serviceName}:${config.serviceVersion} listening on ${bind}` 37 | ); 38 | }); 39 | 40 | // Connect to Redis and MongoDB before starting the server 41 | config.redis.client.connect().then(() => { 42 | connectToMongoose(config.mongodb.url).then(() => server.listen(port)); 43 | }); 44 | -------------------------------------------------------------------------------- /workspace/shopper/server/config/index.js: -------------------------------------------------------------------------------- 1 | // Import the package.json file to grab the package name and the version 2 | const pkg = require("../../package.json"); 3 | 4 | // Export a configuration object 5 | module.exports = { 6 | // Use the name field from package.json as the application name 7 | serviceName: pkg.name, 8 | serviceVersion: pkg.version, 9 | 10 | registry: { 11 | url: "http://localhost:3080", 12 | version: "*" 13 | }, 14 | 15 | // MongoDB configuration 16 | mongodb: { 17 | // Connection URL for the MongoDB server 18 | url: "mongodb://localhost:37017/shopper" 19 | }, 20 | 21 | // Redis configuration 22 | redis: { 23 | // Connection options for the Redis server 24 | options: { 25 | // Connection URL for the Redis server 26 | url: "redis://localhost:7379" 27 | }, 28 | // Placeholder for the Redis client, to be connected elsewhere 29 | client: null 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /workspace/shopper/server/lib/middlewares.js: -------------------------------------------------------------------------------- 1 | const UserService = require("../services/UserService"); 2 | const CartService = require("../services/CartService"); 3 | const config = require("../config"); 4 | 5 | module.exports.assignTemplateVariables = async (req, res, next) => { 6 | res.locals.applicationName = config.applicationName; 7 | 8 | // Flash messaging setup 9 | if (!req.session.messages) req.session.messages = []; 10 | res.locals.messages = req.session.messages; 11 | 12 | // Fetch user and cart info if user is logged in 13 | if (req.session.userId) { 14 | try { 15 | res.locals.currentUser = await UserService.getOne(req.session.userId); 16 | const { userId } = req.session; 17 | 18 | let cartCount = 0; 19 | const cartContents = await CartService.getAll(userId); 20 | if (cartContents) { 21 | Object.keys(cartContents).forEach((itemId) => { 22 | cartCount += parseInt(cartContents[itemId], 10); 23 | }); 24 | } 25 | res.locals.cartCount = cartCount; 26 | } catch (error) { 27 | return next(error); 28 | } 29 | } 30 | return next(); 31 | }; 32 | 33 | module.exports.requireAdmin = (req, res, next) => { 34 | if (!res.locals.currentUser || !res.locals.currentUser.isAdmin) { 35 | req.session.messages.push({ 36 | type: "danger", 37 | text: "Access denied!" 38 | }); 39 | return res.redirect("/"); 40 | } 41 | 42 | return next(); 43 | }; 44 | -------------------------------------------------------------------------------- /workspace/shopper/server/lib/mongooseConnection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const connectToMongoose = async (connectionString) => { 4 | try { 5 | await mongoose.connect(connectionString); 6 | 7 | console.log("Connected to MongoDB"); 8 | } catch (error) { 9 | console.error("Error connecting to Mongoose:", error); 10 | process.exit(1); 11 | } 12 | }; 13 | 14 | module.exports = connectToMongoose; 15 | -------------------------------------------------------------------------------- /workspace/shopper/server/lib/redisConnection.js: -------------------------------------------------------------------------------- 1 | const redis = require('@redis/client'); 2 | 3 | const connectToRedis = (options) => { 4 | const client = redis.createClient(options); 5 | 6 | client.on('connect', () => { 7 | console.log('Connected to Redis'); 8 | }); 9 | 10 | client.on('error', (error) => { 11 | console.error('Error connecting to Redis:', error); 12 | process.exit(1); 13 | }); 14 | 15 | return client; 16 | }; 17 | 18 | module.exports = connectToRedis; 19 | -------------------------------------------------------------------------------- /workspace/shopper/server/lib/tracing.js: -------------------------------------------------------------------------------- 1 | const { node } = require("@opentelemetry/sdk-node"); 2 | 3 | const { NodeTracerProvider } = node; 4 | 5 | const opentelemetry = require("@opentelemetry/api"); 6 | const { registerInstrumentations } = require("@opentelemetry/instrumentation"); 7 | const { 8 | getNodeAutoInstrumentations 9 | } = require("@opentelemetry/auto-instrumentations-node"); 10 | const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace-base"); 11 | const { Resource } = require("@opentelemetry/resources"); 12 | const { 13 | SemanticResourceAttributes 14 | } = require("@opentelemetry/semantic-conventions"); 15 | const { 16 | OTLPTraceExporter 17 | } = require("@opentelemetry/exporter-trace-otlp-http"); 18 | 19 | const sdks = []; 20 | 21 | const exporter = new OTLPTraceExporter(); 22 | 23 | module.exports = (serviceName) => { 24 | if (sdks[serviceName]) return sdks[serviceName]; 25 | 26 | const provider = new NodeTracerProvider({ 27 | resource: new Resource({ 28 | [SemanticResourceAttributes.SERVICE_NAME]: serviceName 29 | }) 30 | }); 31 | 32 | provider.addSpanProcessor( 33 | new BatchSpanProcessor(exporter, { 34 | // The maximum queue size. After the size is reached spans are dropped. 35 | maxQueueSize: 1000, 36 | // The interval between two consecutive exports 37 | scheduledDelayMillis: 30000 38 | }) 39 | ); 40 | provider.register(); 41 | registerInstrumentations({ 42 | instrumentations: getNodeAutoInstrumentations({ 43 | "@opentelemetry/instrumentation-fs": { 44 | enabled: false 45 | } 46 | }) 47 | }); 48 | 49 | return opentelemetry.trace.getTracer(serviceName); 50 | }; 51 | -------------------------------------------------------------------------------- /workspace/shopper/server/models/Item.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ItemSchema = mongoose.Schema({ 4 | sku: {type: Number, required: true, index: {unique: true}}, 5 | name: {type: String, required: true}, 6 | price: {type: Number, required: true}, 7 | }, { 8 | timestamps: true, 9 | }); 10 | 11 | module.exports = mongoose.model('Item', ItemSchema); -------------------------------------------------------------------------------- /workspace/shopper/server/models/Order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const orderItemSchema = new mongoose.Schema({ 4 | sku: Number, 5 | qty: Number, 6 | name: String, 7 | price: Number 8 | }); 9 | 10 | const orderSchema = new mongoose.Schema({ 11 | userId: { 12 | type: mongoose.Schema.Types.ObjectId 13 | }, 14 | email: String, 15 | status: String, 16 | items: [orderItemSchema] 17 | }); 18 | 19 | const Order = mongoose.model("Order", orderSchema); 20 | const OrderItem = mongoose.model("OrderItem", orderItemSchema); 21 | 22 | module.exports = { Order, OrderItem }; 23 | -------------------------------------------------------------------------------- /workspace/shopper/server/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const crypto = require("crypto"); 3 | 4 | const UserSchema = mongoose.Schema( 5 | { 6 | email: { 7 | // Trim and lowercase 8 | type: String, 9 | required: true, 10 | index: { unique: true }, 11 | lowercase: true, 12 | trim: true 13 | }, 14 | password: { 15 | type: String, 16 | required: true, 17 | trim: true 18 | }, 19 | isAdmin: { 20 | type: Boolean, 21 | default: false 22 | }, 23 | salt: { 24 | type: String 25 | } 26 | }, 27 | { timestamps: true } 28 | ); 29 | 30 | UserSchema.pre("save", function preSave(next) { 31 | const user = this; 32 | 33 | if (user.isModified("password")) { 34 | // Generating a random salt for each user 35 | return crypto.randomBytes(16, (err, salt) => { 36 | if (err) return next(err); 37 | 38 | const saltHex = salt.toString("hex"); 39 | user.salt = saltHex; 40 | 41 | // Using PBKDF2 to hash the password 42 | return crypto.pbkdf2( 43 | user.password, 44 | salt, 45 | 100000, 46 | 64, 47 | "sha512", 48 | (pkerr, derivedKey) => { 49 | if (pkerr) return next(pkerr); 50 | user.password = derivedKey.toString("hex"); 51 | return next(); 52 | } 53 | ); 54 | }); 55 | } 56 | return next(); 57 | }); 58 | 59 | UserSchema.methods.comparePassword = async function comparePassword( 60 | candidatePassword 61 | ) { 62 | return new Promise((resolve, reject) => { 63 | // Using PBKDF2 to hash the candidate password and then compare it with the stored hash 64 | crypto.pbkdf2( 65 | candidatePassword, 66 | Buffer.from(this.salt, "hex"), 67 | 100000, 68 | 64, 69 | "sha512", 70 | (err, derivedKey) => { 71 | if (err) reject(err); 72 | else resolve(derivedKey.toString("hex") === this.password); 73 | } 74 | ); 75 | }); 76 | }; 77 | 78 | module.exports = mongoose.model("User", UserSchema); 79 | -------------------------------------------------------------------------------- /workspace/shopper/server/routes/admin/item/index.js: -------------------------------------------------------------------------------- 1 | // Import necessary modules 2 | const express = require("express"); 3 | const CatalogService = require("../../../services/CatalogService"); 4 | 5 | // Create a new Express router 6 | const router = express.Router(); 7 | 8 | // Route for getting item(s) 9 | router.get("/:itemId?", async (req, res, next) => { 10 | try { 11 | // Get all items 12 | const items = await CatalogService.getAll(); 13 | 14 | let item = null; 15 | 16 | // If an itemId was provided, get that specific item 17 | if (req.params.itemId) { 18 | item = await CatalogService.getOne(req.params.itemId); 19 | } 20 | 21 | // Render the page with the items data 22 | return res.render("admin/item", { items, item }); 23 | } catch (err) { 24 | // Forward any errors to the error handler 25 | return next(err); 26 | } 27 | }); 28 | 29 | // Route for creating or updating an item 30 | router.post("/", async (req, res) => { 31 | // Extract and clean up data from the request body 32 | const sku = req.body.sku.trim(); 33 | const name = req.body.name.trim(); 34 | const price = req.body.price.trim(); 35 | 36 | // Validate the data 37 | if (!sku || !name || !price) { 38 | req.session.messages.push({ 39 | type: "warning", 40 | text: "Please enter SKU, name and price!" 41 | }); 42 | return res.redirect("/admin/item"); 43 | } 44 | 45 | try { 46 | // If there was no existing item we now want to create a new item object 47 | if (!req.body.itemId) { 48 | await CatalogService.create({ sku, name, price }); 49 | } else { 50 | // Update an existing item 51 | const itemData = { sku, name, price }; 52 | await CatalogService.update(req.body.itemId, itemData); 53 | } 54 | 55 | // Provide feedback 56 | req.session.messages.push({ 57 | type: "success", 58 | text: `The item was ${ 59 | req.body.itemId ? "updated" : "created" 60 | } successfully!` 61 | }); 62 | return res.redirect("/admin/item"); 63 | } catch (err) { 64 | // Error handling 65 | req.session.messages.push({ 66 | type: "danger", 67 | text: "There was an error while saving the item!" 68 | }); 69 | console.error(err); 70 | return res.redirect("/admin/item"); 71 | } 72 | }); 73 | 74 | // Route for deleting an item 75 | router.get("/delete/:itemId", async (req, res) => { 76 | try { 77 | // Remove the item 78 | await CatalogService.remove(req.params.itemId); 79 | 80 | // Provide feedback 81 | req.session.messages.push({ 82 | type: "success", 83 | text: "The item was successfully deleted!" 84 | }); 85 | return res.redirect("/admin/item"); 86 | } catch (err) { 87 | // Error handling 88 | req.session.messages.push({ 89 | type: "danger", 90 | text: "There was an error while deleting the item!" 91 | }); 92 | console.error(err); 93 | return res.redirect("/admin/item"); 94 | } 95 | }); 96 | 97 | // Export the router 98 | module.exports = router; 99 | -------------------------------------------------------------------------------- /workspace/shopper/server/routes/admin/orders/index.js: -------------------------------------------------------------------------------- 1 | // Import necessary modules 2 | const express = require("express"); 3 | const OrderService = require("../../../services/OrderService"); 4 | 5 | // Create a new Express router 6 | const router = express.Router(); 7 | 8 | // Route for getting all orders 9 | router.get("/", async (req, res, next) => { 10 | try { 11 | // Use the OrderService to get all orders 12 | const orders = await OrderService.getAll(); 13 | 14 | // Render the admin orders page with the orders data 15 | return res.render("admin/orders", { orders }); 16 | } catch (err) { 17 | // Push an error message to the session 18 | req.session.messages.push({ 19 | type: "danger", 20 | text: "There was an error fetching the orders" 21 | }); 22 | // Log the error and forward it to the error handler 23 | console.error(err); 24 | return next(err); 25 | } 26 | }); 27 | 28 | // Route for setting an order's status to "Shipped" 29 | router.get("/setshipped/:orderId", async (req, res) => { 30 | try { 31 | // Use the OrderService to update the status of an order 32 | await OrderService.setStatus(req.params.orderId, "Shipped"); 33 | 34 | // Push a success message to the session 35 | req.session.messages.push({ 36 | type: "success", 37 | text: "Status updated" 38 | }); 39 | 40 | // Redirect back to the admin orders page 41 | return res.redirect("/admin/orders"); 42 | } catch (err) { 43 | // Push an error message to the session 44 | req.session.messages.push({ 45 | type: "danger", 46 | text: "There was an error updating the order" 47 | }); 48 | // Log the error and redirect back to the admin orders page 49 | console.error(err); 50 | return res.redirect("/admin/orders"); 51 | } 52 | }); 53 | 54 | // Export the router 55 | module.exports = router; 56 | -------------------------------------------------------------------------------- /workspace/shopper/server/routes/admin/user/index.js: -------------------------------------------------------------------------------- 1 | // Import necessary modules 2 | const express = require("express"); 3 | const UserService = require("../../../services/UserService"); 4 | 5 | // Create a new Express router 6 | const router = express.Router(); 7 | 8 | // Route for getting all users or a specific user by ID 9 | router.get("/:userId?", async (req, res, next) => { 10 | try { 11 | const users = await UserService.getAll(); // Get all users 12 | 13 | let user = null; 14 | // The optional userId param was passed 15 | if (req.params.userId) { 16 | user = await UserService.getOne(req.params.userId); // Get specific user 17 | } 18 | 19 | // Render the user page with all users or the specific user 20 | return res.render("admin/user", { 21 | users, 22 | user 23 | }); 24 | } catch (err) { 25 | // Forward the error to the error handler 26 | return next(err); 27 | } 28 | }); 29 | 30 | // Route for saving or updating a user 31 | router.post("/", async (req, res) => { 32 | const email = req.body.email.trim(); 33 | const password = req.body.password.trim(); 34 | const { isAdmin } = req.body; 35 | 36 | // Check if email and password are provided 37 | if (!email || (!password && !req.body.userId)) { 38 | req.session.messages.push({ 39 | type: "warning", 40 | text: "Please enter email address and password!" 41 | }); 42 | return res.redirect("/admin/user"); 43 | } 44 | 45 | try { 46 | // Create or update a user 47 | if (!req.body.userId) { 48 | await UserService.create({ email, password, isAdmin }); // Create new user 49 | } else { 50 | const userData = { 51 | email, 52 | isAdmin, 53 | password 54 | }; 55 | 56 | // Check if password is provided for updating user 57 | if (password) { 58 | userData.password = password; 59 | } 60 | 61 | await UserService.update(req.body.userId, userData); // Update user 62 | } 63 | 64 | req.session.messages.push({ 65 | type: "success", 66 | text: `The user was ${ 67 | req.body.userId ? "updated" : "created" 68 | } successfully!` 69 | }); 70 | 71 | // Redirect to the user page 72 | return res.redirect("/admin/user"); 73 | } catch (err) { 74 | // Log the error and redirect to the user page 75 | req.session.messages.push({ 76 | type: "danger", 77 | text: "There was an error while saving the user!" 78 | }); 79 | console.error(err); 80 | return res.redirect("/admin/user"); 81 | } 82 | }); 83 | 84 | // Route for deleting a user 85 | router.get("/delete/:userId", async (req, res) => { 86 | try { 87 | await UserService.remove(req.params.userId); // Remove the user 88 | } catch (err) { 89 | // Log the error and redirect to the user page 90 | req.session.messages.push({ 91 | type: "danger", 92 | text: "There was an error while deleting the user!" 93 | }); 94 | console.error(err); 95 | return res.redirect("/admin/user"); 96 | } 97 | 98 | // Let the user know that everything went fine 99 | req.session.messages.push({ 100 | type: "success", 101 | text: "The user was successfully deleted!" 102 | }); 103 | return res.redirect("/admin/user"); 104 | }); 105 | 106 | // Route for impersonating a user (switching the current user to another) 107 | router.get("/impersonate/:userId", (req, res) => { 108 | req.session.userId = req.params.userId; // Set session's userId to the provided one 109 | req.session.messages.push({ 110 | type: "success", 111 | text: "User successfully switched" 112 | }); 113 | return res.redirect("/admin/user"); 114 | }); 115 | // Export the router 116 | module.exports = router; 117 | -------------------------------------------------------------------------------- /workspace/shopper/server/routes/cart/index.js: -------------------------------------------------------------------------------- 1 | // Import required modules 2 | const express = require("express"); 3 | 4 | const CatalogService = require("../../services/CatalogService"); 5 | const CartService = require("../../services/CartService"); 6 | const OrderService = require("../../services/OrderService"); 7 | 8 | // Instantiate a new Express router 9 | const router = express.Router(); 10 | 11 | // Define a route for getting the cart 12 | router.get("/", async (req, res) => { 13 | // Check if a user is logged in 14 | if (!res.locals.currentUser) { 15 | req.session.messages.push({ 16 | type: "warning", 17 | text: "Please log in first" 18 | }); 19 | return res.redirect("/shop"); 20 | } 21 | 22 | // Retrieve all items in the user's cart 23 | const userId = res.locals.currentUser.id; 24 | const cartItems = await CartService.getAll(userId); 25 | 26 | let items = []; 27 | if (cartItems) { 28 | // Map over the cart items and fetch their details 29 | items = await Promise.all( 30 | Object.keys(cartItems).map(async (itemId) => { 31 | const item = await CatalogService.getOne(itemId); 32 | if (!item) { 33 | CartService.remove(userId, itemId); 34 | return null; 35 | } 36 | // Add the quantity of each item to its details 37 | item.quantity = cartItems[itemId]; 38 | return item; 39 | }) 40 | ); 41 | } 42 | 43 | // Render the cart view with the items 44 | return res.render("cart", { items: items.filter((item) => item) }); 45 | }); 46 | 47 | // Define a route for removing an item from the cart 48 | router.get("/remove/:itemId", async (req, res) => { 49 | // Check if a user is logged in 50 | if (!res.locals.currentUser) { 51 | req.session.messages.push({ 52 | type: "warning", 53 | text: "Please log in first" 54 | }); 55 | return res.redirect("/shop"); 56 | } 57 | 58 | try { 59 | // Remove the specified item from the cart 60 | const userId = res.locals.currentUser.id; 61 | await CartService.remove(userId, req.params.itemId); 62 | 63 | // Show a success message 64 | req.session.messages.push({ 65 | type: "success", 66 | text: "The item was removed from the your cart" 67 | }); 68 | } catch (err) { 69 | // Show an error message and log the error 70 | req.session.messages.push({ 71 | type: "danger", 72 | text: "There was an error removing the item from your cart" 73 | }); 74 | console.error(err); 75 | return res.redirect("/cart"); 76 | } 77 | 78 | // Redirect the user back to the cart 79 | return res.redirect("/cart"); 80 | }); 81 | 82 | // Define a route for buying all items in the cart 83 | router.get("/buy", async (req, res) => { 84 | // Check if a user is logged in 85 | if (!res.locals.currentUser) { 86 | req.session.messages.push({ 87 | type: "warning", 88 | text: "Please log in first" 89 | }); 90 | return res.redirect("/shop"); 91 | } 92 | 93 | try { 94 | const userId = res.locals.currentUser.id; 95 | const user = res.locals.currentUser; 96 | 97 | // Retrieve all items in the user's cart 98 | const cartItems = await CartService.getAll(userId); 99 | 100 | // Throw an error if the cart is empty 101 | if (!cartItems) { 102 | throw new Error("No items were found in your cart"); 103 | } 104 | 105 | const cartPromises = []; 106 | 107 | // Map over the cart items, fetch their details, and prepare promises 108 | // to clear the cart after the purchase 109 | const items = await Promise.all( 110 | Object.keys(cartItems).map(async (key) => { 111 | const item = await CatalogService.getOne(key); 112 | 113 | // Add a promise to remove this item from the cart to the list of promises 114 | cartPromises.push(CartService.remove(userId, item.id)); 115 | 116 | // Return an object with the item's details for the order 117 | return { 118 | sku: item.sku, 119 | qty: cartItems[key], 120 | price: item.price, 121 | name: item.name 122 | }; 123 | }) 124 | ); 125 | 126 | // Create the order 127 | await OrderService.create(user.id, user.email, items); 128 | 129 | // Execute all promises to remove each item from the cart 130 | await Promise.all(cartPromises) 131 | .then(() => { 132 | // Show a success message 133 | req.session.messages.push({ 134 | type: "success", 135 | text: "Thank you for your business" 136 | }); 137 | }) 138 | .catch((error) => { 139 | // Log any errors 140 | console.error("Error occurred while running tasks:", error); 141 | }); 142 | 143 | // Redirect the user to the home page 144 | return res.redirect("/"); 145 | } catch (err) { 146 | console.error(err); 147 | // Show an error message and log the error 148 | req.session.messages.push({ 149 | type: "danger", 150 | text: "There was an error finishing your order" 151 | }); 152 | 153 | // Redirect the user back to the cart 154 | return res.redirect("/cart"); 155 | } 156 | }); 157 | 158 | // Export the router 159 | module.exports = router; 160 | -------------------------------------------------------------------------------- /workspace/shopper/server/routes/index.js: -------------------------------------------------------------------------------- 1 | // Import the required Express module 2 | const express = require("express"); 3 | 4 | // Import the different routes modules 5 | const userAdminRoute = require("./admin/user"); 6 | const itemAdminRoute = require("./admin/item"); 7 | const orderAdminRoute = require("./admin/orders"); 8 | const shopRoute = require("./shop"); 9 | const cartRoute = require("./cart"); 10 | const userRoute = require("./user"); 11 | 12 | const { requireAdmin } = require("../lib/middlewares"); 13 | 14 | // Instantiate a new Express Router 15 | const router = express.Router(); 16 | 17 | // Define a GET route for the root path ("/") of the application 18 | router.get("/", (req, res) => { 19 | // Render the 'index' view when this route is accessed 20 | res.render("index"); 21 | }); 22 | 23 | // Mount the shop and cart routes to their respective paths 24 | router.use("/shop", shopRoute); 25 | router.use("/cart", cartRoute); 26 | 27 | // Mount the admin routes to their respective paths 28 | // Please note, in real world applications these routes should be protected by authentication and authorization middlewares 29 | router.use("/admin/user", userAdminRoute); 30 | router.use("/admin/item", requireAdmin, itemAdminRoute); 31 | router.use("/admin/orders", requireAdmin, orderAdminRoute); 32 | router.use("/user", userRoute); 33 | 34 | // Export the router to be used in the main application 35 | module.exports = router; 36 | -------------------------------------------------------------------------------- /workspace/shopper/server/routes/shop/index.js: -------------------------------------------------------------------------------- 1 | // Required modules and services are imported 2 | const express = require("express"); 3 | const CatalogService = require("../../services/CatalogService"); 4 | const CartService = require("../../services/CartService"); 5 | 6 | // Express router is instantiated 7 | const router = express.Router(); 8 | 9 | // Route to render all items in the catalog 10 | router.get("/", async (req, res) => { 11 | try { 12 | // Get all items from the catalog 13 | const items = await CatalogService.getAll(); 14 | // Render the 'shop' view and pass in the items 15 | res.render("shop", { items }); 16 | } catch (err) { 17 | req.session.messages.push({ 18 | type: "danger", 19 | text: "There was an error loading the shop catalog." 20 | }); 21 | console.error(err); 22 | } 23 | }); 24 | 25 | // Route to add an item to the cart 26 | router.get("/tocart/:itemId", async (req, res) => { 27 | // Check if the user is logged in 28 | if (!res.locals.currentUser) { 29 | // If not, add a warning message and redirect to the shop page 30 | req.session.messages.push({ 31 | type: "warning", 32 | text: "Please log in first" 33 | }); 34 | return res.redirect("/shop"); 35 | } 36 | 37 | // If the user is logged in, attempt to add the item to the cart 38 | try { 39 | // Add the item to the cart 40 | const userId = res.locals.currentUser.id; 41 | await CartService.add(userId, req.params.itemId); 42 | // Add a success message 43 | req.session.messages.push({ 44 | type: "success", 45 | text: "The item was added to your cart" 46 | }); 47 | } catch (err) { 48 | // If an error occurs, add an error message, log the error, and redirect to the shop page 49 | req.session.messages.push({ 50 | type: "danger", 51 | text: "There was an error adding the item to your cart" 52 | }); 53 | console.error(err); 54 | } 55 | 56 | // Redirect to the shop page 57 | return res.redirect("/shop"); 58 | }); 59 | 60 | // Export the router 61 | module.exports = router; 62 | -------------------------------------------------------------------------------- /workspace/shopper/server/routes/user/index.js: -------------------------------------------------------------------------------- 1 | // Required modules and services are imported 2 | const express = require("express"); 3 | const UserService = require("../../services/UserService"); 4 | 5 | // Express router is instantiated 6 | const router = express.Router(); 7 | 8 | // Route to render all items in the catalog 9 | router.post("/login", async (req, res) => { 10 | const authUser = await UserService.authenticate( 11 | req.body.email, 12 | req.body.password 13 | ); 14 | 15 | if (authUser && authUser.id) { 16 | req.session.userId = authUser.id; 17 | req.session.messages.push({ 18 | type: "success", 19 | text: "You have been logged in!" 20 | }); 21 | return res.redirect("/"); 22 | } 23 | 24 | req.session.messages.push({ 25 | type: "danger", 26 | text: "Invalid email address or password!" 27 | }); 28 | return res.redirect("/"); 29 | }); 30 | 31 | router.get("/logout", (req, res) => { 32 | req.session.userId = null; 33 | req.session.messages.push({ 34 | type: "success", 35 | text: "You have been logged out!" 36 | }); 37 | return res.redirect("/"); 38 | }); 39 | 40 | // Export the router 41 | module.exports = router; 42 | -------------------------------------------------------------------------------- /workspace/shopper/server/services/CartService.js: -------------------------------------------------------------------------------- 1 | const config = require("../config"); 2 | 3 | /** @module CartService */ 4 | 5 | /** 6 | * Service class for managing a user's cart 7 | */ 8 | class CartService { 9 | static key(userId) { 10 | return `shopper_cart:${userId}`; 11 | } 12 | 13 | static client() { 14 | return config.redis.client; 15 | } 16 | 17 | /** 18 | * Add an item to the user's cart 19 | * @param {string} itemId - The ID of the item to add 20 | * @returns {Promise} - A promise that resolves to the new quantity of 21 | * the item in the cart 22 | */ 23 | static async add(userId, itemId) { 24 | return this.client().HINCRBY(this.key(userId), itemId, 1); 25 | } 26 | 27 | /** 28 | * Get all items in the user's cart 29 | * @returns {Promise} - A promise that resolves to an object containing 30 | * the cart items and their quantities 31 | */ 32 | static async getAll(userId) { 33 | return this.client().HGETALL(this.key(userId)); 34 | } 35 | 36 | /** 37 | * Remove an item from the user's cart 38 | * @param {string} itemId - The ID of the item to remove 39 | * @returns {Promise} - A promise that resolves to the number of items 40 | * removed (1 if the item was removed, 0 if the item was not in the cart) 41 | */ 42 | static async remove(userId, itemId) { 43 | return this.client().HDEL(this.key(userId), itemId); 44 | } 45 | } 46 | 47 | module.exports = CartService; 48 | -------------------------------------------------------------------------------- /workspace/shopper/server/services/CatalogService.js: -------------------------------------------------------------------------------- 1 | /** @module CatalogService */ 2 | 3 | // Import the Item model from mongoose 4 | const ItemModel = require("../models/Item"); 5 | 6 | /** 7 | * Service class for interacting with the Item catalog 8 | */ 9 | class CatalogService { 10 | /** 11 | * Get all items from the database, sorted in descending order by creation time 12 | * @returns {Promise} - A promise that resolves to an array of Items 13 | */ 14 | static async getAll() { 15 | return ItemModel.find({}).sort({ createdAt: -1 }).exec(); 16 | } 17 | 18 | /** 19 | * Get a single item from the database 20 | * @param {string} itemId - The id of the item to retrieve 21 | * @returns {Promise} - A promise that resolves to an Item object 22 | */ 23 | static async getOne(itemId) { 24 | return ItemModel.findById(itemId).exec(); 25 | } 26 | 27 | /** 28 | * Create a new item in the database 29 | * @param {Object} data - The data for the new item 30 | * @returns {Promise} - A promise that resolves to the new Item object 31 | */ 32 | static async create(data) { 33 | const item = new ItemModel(data); 34 | return item.save(); 35 | } 36 | 37 | /** 38 | * Update an existing item in the database 39 | * @param {string} itemId - The id of the item to update 40 | * @param {Object} data - The new data for the item 41 | * @returns {Promise} - A promise that resolves to the updated Item object, or null if no item was found 42 | */ 43 | static async update(itemId, data) { 44 | return ItemModel.findByIdAndUpdate(itemId, data, { new: true }).exec(); 45 | } 46 | 47 | /** 48 | * Remove an item from the database 49 | * @param {string} itemId - The id of the item to remove 50 | * @returns {Promise} - A promise that resolves to the deletion result 51 | */ 52 | static async remove(itemId) { 53 | return ItemModel.deleteOne({ _id: itemId }).exec(); 54 | } 55 | } 56 | 57 | module.exports = CatalogService; 58 | -------------------------------------------------------------------------------- /workspace/shopper/server/services/OrderService.js: -------------------------------------------------------------------------------- 1 | /** @module OrderService */ 2 | 3 | // Import the Order and User models from mongoose 4 | const { Order, User } = require("../models/Order"); 5 | 6 | /** 7 | * Service class for managing orders 8 | */ 9 | class OrderService { 10 | /** 11 | * Create a new order 12 | * @param {Object} user - The user who is creating the order 13 | * @param {Array} items - The items in the order 14 | * @returns {Promise} - A promise that resolves to the new order 15 | */ 16 | static async create(userId, email, items) { 17 | const order = new Order({ 18 | userId, 19 | email, 20 | status: "Not Shipped", 21 | items 22 | }); 23 | 24 | await order.save(); 25 | 26 | return order; 27 | } 28 | 29 | /** 30 | * Get all orders 31 | * @returns {Promise} - A promise that resolves to an array of orders 32 | */ 33 | static async getAll() { 34 | return Order.find().populate("items"); 35 | } 36 | 37 | /** 38 | * Update the status of an order 39 | * @param {string} orderId - The ID of the order to update 40 | * @param {string} status - The new status 41 | * @returns {Promise} - A promise that resolves to the updated 42 | * order, or null if no order was found 43 | */ 44 | static async setStatus(orderId, status) { 45 | return Order.findByIdAndUpdate(orderId, { status }, { new: true }); 46 | } 47 | } 48 | 49 | module.exports = OrderService; 50 | -------------------------------------------------------------------------------- /workspace/shopper/server/services/UserService.js: -------------------------------------------------------------------------------- 1 | /** @module UserService */ 2 | 3 | // Import the User model from mongoose 4 | const UserModel = require("../models/User"); 5 | 6 | /** 7 | * Service class for managing users 8 | */ 9 | class UserService { 10 | /** 11 | * Get all users 12 | * @returns {Promise} - A promise that resolves to an array of users 13 | */ 14 | static async getAll() { 15 | return UserModel.find({}).sort({ createdAt: -1 }); 16 | } 17 | 18 | /** 19 | * Get a user by ID 20 | * @param {string} userId - The ID of the user to retrieve 21 | * @returns {Promise} - A promise that resolves to the user, or 22 | * null if no user was found 23 | */ 24 | static async getOne(userId) { 25 | return UserModel.findById(userId).exec(); 26 | } 27 | 28 | /** 29 | * Create a new user 30 | * @param {Object} data - The data for the new user 31 | * @returns {Promise} - A promise that resolves to the new user 32 | */ 33 | static async create(data) { 34 | const user = new UserModel(data); 35 | return user.save(); 36 | } 37 | 38 | /** 39 | * Authenticate a user 40 | * @param {string} email - The email address 41 | * @param {string} password - The email password 42 | * @returns {Promise} - A promise that either returns the authenticated user or false 43 | */ 44 | static async authenticate(email, password) { 45 | const maybeUser = await UserModel.findOne({ email }); 46 | if (!maybeUser) return false; 47 | const validPassword = await maybeUser.comparePassword(password); 48 | if (!validPassword) return false; 49 | return maybeUser; 50 | } 51 | 52 | /** 53 | * Update a user's data 54 | * @param {string} userId - The ID of the user to update 55 | * @param {Object} data - The new data for the user 56 | * @returns {Promise} - A promise that resolves to the updated user 57 | */ 58 | static async update(userId, data) { 59 | // Fetch the user first 60 | const user = await UserModel.findById(userId); 61 | user.email = data.email; 62 | user.isAdmin = data.isAdmin; 63 | 64 | // Only set the password if it was modified 65 | if (data.password) { 66 | user.password = data.password; 67 | } 68 | 69 | return user.save(); 70 | } 71 | 72 | /** 73 | * Remove a user 74 | * @param {string} userId - The ID of the user to remove 75 | * @returns {Promise} - A promise that resolves to the result of the 76 | * delete operation 77 | */ 78 | static async remove(userId) { 79 | return UserModel.deleteOne({ _id: userId }).exec(); 80 | } 81 | } 82 | 83 | module.exports = UserService; 84 | -------------------------------------------------------------------------------- /workspace/shopper/server/views/admin/item.pug: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | block content 4 | .row 5 | .col-md-12 6 | h1 7 | | Manage Items 8 | 9 | if items && items.length 10 | table.table.table-bordered.table-striped 11 | thead 12 | th 13 | | SKU 14 | 15 | th 16 | | Name 17 | 18 | th 19 | | Price 20 | 21 | th 22 | | Action 23 | 24 | tbody 25 | each item in items 26 | tr 27 | td=item.sku 28 | td=item.name 29 | td=item.price 30 | td 31 | a.m-1.btn.btn-primary(href='/admin/item/' + item.id, role='button') 32 | | Edit item 33 | a.m-1.btn.btn-danger(href='/admin/item/delete/' + item.id, role='button') 34 | | Delete item 35 | else 36 | p.lead 37 | | No items found 38 | .row 39 | .col-md-12 40 | .card 41 | .card-header 42 | if(!item) 43 | | Create item 44 | else 45 | | Edit item 46 | 47 | .card-block 48 | form(method='POST', action='/admin/item' autocomplete='off') 49 | input(type='hidden', name='itemId', value=item ? item.id : '') 50 | 51 | .form-group 52 | label.form-control-label(for='sku') 53 | | SKU: 54 | input#sku.form-control(type='number', name='sku', autocomplete='off', value=item ? item.sku : '') 55 | 56 | .form-group 57 | label.form-control-label(for='name') 58 | | Name: 59 | input#name.form-control(type='text', name='name', autocomplete='off', value=item ? item.name : '') 60 | 61 | .form-group 62 | label.form-control-label(for='price') 63 | | Price: 64 | input#price.form-control(type='text', name='price', autocomplete='off', value=item ? item.price : '') 65 | 66 | button.m-1.btn.btn-primary(type='submit') 67 | | Submit 68 | 69 | a.m-1.btn.btn-secondary(href='/admin/item', role='button') 70 | | Reset -------------------------------------------------------------------------------- /workspace/shopper/server/views/admin/orders.pug: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | block content 4 | .row 5 | .col-md-12 6 | h1 7 | | All Orders 8 | 9 | 10 | if orders && orders.length > 0 11 | table.table.table-bordered 12 | thead 13 | th 14 | | # 15 | 16 | 17 | th 18 | | Status 19 | 20 | th 21 | | Email 22 | 23 | th 24 | | Date 25 | 26 | th 27 | | Items 28 | 29 | th 30 | | Action 31 | tbody 32 | each order in orders 33 | tr 34 | td=order.id 35 | td=order.status 36 | td=order.email 37 | td=order.createdAt 38 | 39 | td 40 | table.table.table-bordered 41 | thead 42 | th 43 | | SKU 44 | 45 | th 46 | | Quantity 47 | 48 | th 49 | | Price 50 | 51 | tbody 52 | each item in order.items 53 | tr 54 | td=item.sku 55 | td=item.qty 56 | td=item.price 57 | 58 | td 59 | if order.status !== 'Shipped' 60 | a.m-1.btn.btn-danger(href='/admin/orders/setshipped/' + order.id, role='button') 61 | | Set Shipped 62 | else 63 | a.m-1.btn.btn-danger.disabled(href='/admin/orders/setshipped/' + order.id, role='button') 64 | | Order Shipped 65 | 66 | 67 | else 68 | p.lead 69 | | Nothing in here. -------------------------------------------------------------------------------- /workspace/shopper/server/views/admin/user.pug: -------------------------------------------------------------------------------- 1 | extends ../layout 2 | 3 | block content 4 | .row 5 | .col-md-12 6 | h1 7 | | Manage Users 8 | 9 | if users && users.length 10 | 11 | table.table.table-bordered.table-striped 12 | thead 13 | th 14 | | Email 15 | th 16 | | Admin Status 17 | th 18 | | Action 19 | 20 | tbody 21 | each user in users 22 | tr 23 | td 24 | | #{user.email} 25 | td 26 | | #{user.isAdmin ? 'Admin' : 'User'} 27 | td 28 | a.m-1.btn.btn-primary(href='/admin/user/' + user.id, role='button') 29 | | Edit User 30 | a.m-1.btn.btn-danger(href='/admin/user/delete/' + user.id, role='button') 31 | | Delete User 32 | else 33 | p.lead 34 | | No users found 35 | 36 | .row 37 | .col-md-12 38 | .card 39 | .card-header 40 | if(!user) 41 | | Create User 42 | else 43 | | Edit User 44 | 45 | .card-block 46 | form(method='POST', action='/admin/user' autocomplete='off') 47 | input(type='hidden', name='userId', value=user ? user.id : '') 48 | .form-group 49 | label.form-control-label(for='email') 50 | | Email Address: 51 | input#email.form-control(type='email', name='email', autocomplete='off', value=user ? user.email : '') 52 | 53 | .form-group 54 | label.form-control-label(for='password') 55 | | Password: 56 | input#password.form-control(type='password', name='password', autocomplete='off') 57 | 58 | // New form group for isAdmin checkbox 59 | .form-group 60 | label.form-control-label(for='isAdmin') 61 | | Set as admin:  62 | input#isAdmin(type='checkbox', name='isAdmin', value='true', checked=(user && user.isAdmin)) 63 | 64 | button.m-1.btn.btn-primary(type='submit') 65 | | Submit 66 | 67 | a.m-1.btn.btn-secondary(href='/admin/user', role='button') 68 | | Reset 69 | -------------------------------------------------------------------------------- /workspace/shopper/server/views/cart.pug: -------------------------------------------------------------------------------- 1 | extends ./layout 2 | 3 | block content 4 | .row 5 | .col-md-12 6 | h1 7 | | Your Cart 8 | 9 | 10 | if items && items.length > 0 11 | table.table.table-bordered.table-striped 12 | thead 13 | th 14 | | SKU 15 | 16 | th 17 | | Name 18 | 19 | th 20 | | Price 21 | 22 | th 23 | | Quantity 24 | 25 | th 26 | | Action 27 | tbody 28 | each item in items 29 | tr 30 | td=item.sku 31 | td=item.name 32 | td=item.price 33 | td=item.quantity 34 | td 35 | a.m-1.btn.btn-danger(href='/cart/remove/' + item.id, role='button') 36 | | Remove from Cart 37 | 38 | 39 | tfoot 40 | tr 41 | td(colspan=5) 42 | a.m-1.btn.btn-primary.btn-block(href='/cart/buy', role='button') 43 | | Buy Now 44 | else 45 | p.lead 46 | | Nothing in here. -------------------------------------------------------------------------------- /workspace/shopper/server/views/index.pug: -------------------------------------------------------------------------------- 1 | extends ./layout 2 | 3 | block content 4 | .row 5 | .col-md-12 6 | h1 7 | | Welcome to #{applicationName} 8 | p.lead 9 | | This is a playground application for the Node Microservices course on LinkedIn Learning. -------------------------------------------------------------------------------- /workspace/shopper/server/views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | meta(name='viewport', content='width=device-width, initial-scale=1, shrink-to-fit=no') 6 | meta(name='description', content='') 7 | meta(name='author', content='') 8 | 9 | title #{applicationName} 10 | // Bootstrap core CSS 11 | link(rel='stylesheet', href='//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css', integrity='sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ', crossorigin='anonymous') 12 | link(rel='stylesheet', href='/css/site.css') 13 | 14 | body 15 | nav.navbar.navbar-toggleable-md.navbar-inverse.bg-inverse.fixed-top 16 | button.navbar-toggler.navbar-toggler-right(type='button', data-toggle='collapse', data-target='#navbarsExampleDefault', aria-controls='navbarsExampleDefault', aria-expanded='false', aria-label='Toggle navigation') 17 | span.navbar-toggler-icon 18 | 19 | .container 20 | a.navbar-brand(href='#') #{applicationName} 21 | #navbarsExampleDefault.collapse.navbar-collapse 22 | ul.navbar-nav.mr-auto 23 | li.nav-item.active 24 | a.nav-link(href='/') 25 | | Home 26 | 27 | li.nav-item 28 | a.nav-link(href='/shop') Shop 29 | 30 | if(currentUser && currentUser.isAdmin) 31 | li.nav-item 32 | a.nav-link(href='/admin/item') Manage Items 33 | 34 | li.nav-item 35 | a.nav-link(href='/admin/orders') Manage Orders 36 | 37 | ul.navbar-nav.ml-auto 38 | 39 | if !currentUser 40 | 41 | li.nav-item 42 | a.nav-link(href='#', data-toggle='modal', data-target='#loginModal') Login 43 | li.nav-item 44 | a.nav-link(href='/admin/user') Manage Users (Dev Only) 45 | else 46 | 47 | li.nav-item 48 | a.nav-link(href='#') 49 | | Logged in as #{currentUser.email} 50 | li.nav-item 51 | a.nav-link(href='/user/logout') Logout 52 | 53 | if cartCount 54 | li.nav-item 55 | a.nav-link(href='/cart') 56 | if(cartCount > 1) 57 | | #{cartCount} items in cart 58 | else 59 | | #{cartCount} item in cart 60 | li.nav-item 61 | a.nav-link(href='/admin/user') Manage Users (Dev Only) 62 | // Login modal 63 | #loginModal.modal.fade(tabindex='-1', role='dialog', aria-labelledby='loginModalLabel', aria-hidden='true') 64 | .modal-dialog(role='document') 65 | .modal-content 66 | .modal-header 67 | h5#loginModalLabel.modal-title Login 68 | button.close(type='button', data-dismiss='modal', aria-label='Close') 69 | span(aria-hidden='true') × 70 | .modal-body 71 | form(method="POST", action="/user/login") 72 | .form-group 73 | label(for='email') Email address 74 | input#email.form-control(type='email', name="email" placeholder='Enter email') 75 | .form-group 76 | label(for='password') Password 77 | input#password.form-control(type='password', name="password", placeholder='Password') 78 | .modal-footer 79 | button.btn.btn-secondary(type='button', data-dismiss='modal') Close 80 | button.btn.btn-primary(type='submit') Login 81 | 82 | .container 83 | 84 | while messages.length > 0 85 | - const message =messages.pop(); 86 | .alert(class='alert-dismissible fade show alert-' + message.type) 87 | button.close(type='button', data-dismiss='alert', aria-label='Close') 88 | span(aria-hidden='true') × 89 | | #{message.text} 90 | 91 | block content 92 | 93 | 94 | script(src='//code.jquery.com/jquery-3.1.1.slim.min.js', integrity='sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n', crossorigin='anonymous') 95 | script(src='//cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js', integrity='sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb', crossorigin='anonymous') 96 | script(src='//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js', integrity='sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn', crossorigin='anonymous') 97 | -------------------------------------------------------------------------------- /workspace/shopper/server/views/shop.pug: -------------------------------------------------------------------------------- 1 | extends ./layout 2 | 3 | block content 4 | .row 5 | .col-md-12 6 | h1 7 | | A very basic Shop 8 | 9 | if items && items.length 10 | table.table.table-bordered.table-striped 11 | thead 12 | th 13 | | SKU 14 | 15 | th 16 | | Name 17 | 18 | th 19 | | Price 20 | 21 | th 22 | | Action 23 | tbody 24 | each item in items 25 | tr 26 | td=item.sku 27 | td=item.name 28 | td=item.price 29 | td 30 | a.m-1.btn.btn-primary(href='/shop/tocart/' + item.id, role='button') 31 | | Add to Cart 32 | else 33 | p.lead 34 | | Nothing to see here yet --------------------------------------------------------------------------------