├── .gitignore ├── README.md ├── __Episodes ├── Part_5 │ └── nodejs_microservice_rpc_communication-master.zip ├── Part_6 │ ├── CD_Prod_Workflow.yml │ ├── CD_QA_Workflow.yml │ ├── CI_Workflow.yml │ └── part_6_nodejs_microservice.zip ├── Part_7 │ └── Part_7.zip └── Part_8 │ └── Part_8.zip ├── customer ├── .env.dev ├── Dockerfile ├── package-lock.json ├── package.json └── src │ ├── api │ ├── app-events.js │ ├── customer.js │ ├── index.js │ └── middlewares │ │ └── auth.js │ ├── config │ └── index.js │ ├── database │ ├── connection.js │ ├── index.js │ ├── models │ │ ├── Address.js │ │ ├── Customer.js │ │ └── index.js │ └── repository │ │ └── customer-repository.js │ ├── express-app.js │ ├── index.js │ ├── sampledata.json │ ├── services │ ├── customer-service.js │ └── customer-service.test.js │ └── utils │ ├── app-errors.js │ ├── error-handler.js │ └── index.js ├── docker-compose.yml ├── gateway ├── index.js └── package.json ├── products ├── .env.dev ├── Dockerfile ├── index.js ├── package-lock.json ├── package.json └── src │ ├── api │ ├── app-events.js │ ├── index.js │ ├── middlewares │ │ └── auth.js │ └── products.js │ ├── config │ └── index.js │ ├── database │ ├── connection.js │ ├── index.js │ ├── models │ │ ├── Product.js │ │ └── index.js │ └── repository │ │ └── product-repository.js │ ├── express-app.js │ ├── index.js │ ├── sampledata.json │ ├── services │ ├── product-service.js │ └── product-service.test.js │ └── utils │ ├── app-errors.js │ ├── error-handler.js │ └── index.js ├── proxy ├── Dockerfile └── nginx.conf └── shopping ├── .env.dev ├── Dockerfile ├── index.js ├── package-lock.json ├── package.json └── src ├── api ├── app-events.js ├── index.js ├── middlewares │ └── auth.js └── shopping.js ├── config └── index.js ├── database ├── connection.js ├── index.js ├── models │ ├── Cart.js │ ├── Order.js │ └── index.js └── repository │ └── shopping-repository.js ├── express-app.js ├── index.js ├── sampledata.json ├── services ├── shopping-service.js └── shopping-service.test.js └── utils ├── app-errors.js ├── error-handler.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | **/.env 3 | **/.DS_Store 4 | **/node_modules 5 | db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeJS Microservice 2 | NodeJS Microservice Architecture Example with realtime project 3 | 4 | 5 | ## Monolithic version link: 6 | 7 | [Grocery Online Shopping App Monolithic](https://github.com/codergogoi/Grocery_Online_Shopping_App) 8 | 9 | ============================ 10 |
11 | What you can learn from this repository? 12 |
13 | https://youtu.be/EXDkgjU8DDU 14 |
15 |
16 |
17 | 18 | This is a practical source code of the NodeJS Microservice tutorial serise. Where we have split up a monolithic application into Microservices Architecture. The main goal of this repository is to provide an overview how the microservices architecture is working with nodejs and what is the complexity we need to resolve to achieve the outcome from an Monolithic architecture. 19 | 20 | 21 | ============================ 22 |
23 | This repository is published for educational purpose only. If the concept of the business logic matching with any project belongs to any organization it may be a co-incident. The main purpose of this repository is only to educate people by contributing practical knowledge. 24 |
25 | 26 | ## Frontend Repository: 27 | 28 | https://github.com/codergogoi/microservice-frontend 29 | 30 | ## POSTMAN Collection 31 |
32 | https://github.com/codergogoi/Grocery_Online_Shopping_App/blob/master/online_shopping_monolithic/Microservices%20Tutorial.postman_collection.json 33 | -------------------------------------------------------------------------------- /__Episodes/Part_5/nodejs_microservice_rpc_communication-master.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codergogoi/nodejs_microservice/5e56802ea8935e86ba663b6a24ae4327ebcf689e/__Episodes/Part_5/nodejs_microservice_rpc_communication-master.zip -------------------------------------------------------------------------------- /__Episodes/Part_6/CD_Prod_Workflow.yml: -------------------------------------------------------------------------------- 1 | name: Deploy on Production 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | deploy_on_prod: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout Source Code 13 | uses: actions/checkout@v2 14 | 15 | - name: Create customer Env file 16 | working-directory: ./customer 17 | run: | 18 | touch .env 19 | echo APP_SECRET=${{ secrets.PROD_APP_SECRET }} >> .env 20 | echo MONGODB_URI=${{ secrets.PROD_CUSTOMER_DB_URL }} >> .env 21 | echo MSG_QUEUE_URL=${{ secrets.PROD_MSG_QUEUE_URL }} >> .env 22 | echo EXCHANGE_NAME=ONLINE_STORE >> .env 23 | echo PORT=8001 >> .env 24 | cat .env 25 | - name: Create Products Env file 26 | working-directory: ./products 27 | run: | 28 | touch .env 29 | echo APP_SECRET=${{ secrets.PROD_APP_SECRET }} >> .env 30 | echo MONGODB_URI=${{ secrets.PROD_PRODUCTS_DB_URL }} >> .env 31 | echo MSG_QUEUE_URL=${{ secrets.PROD_MSG_QUEUE_URL }} >> .env 32 | echo EXCHANGE_NAME=ONLINE_STORE >> .env 33 | echo PORT=8002 >> .env 34 | cat .env 35 | 36 | - name: Create shopping Env file 37 | working-directory: ./shopping 38 | run: | 39 | touch .env 40 | echo APP_SECRET=${{ secrets.PROD_APP_SECRET }} >> .env 41 | echo MONGODB_URI=${{ secrets.PROD_SHOPPING_DB_URL }} >> .env 42 | echo MSG_QUEUE_URL=${{ secrets.PROD_MSG_QUEUE_URL }} >> .env 43 | echo EXCHANGE_NAME=ONLINE_STORE >> .env 44 | echo PORT=8003 >> .env 45 | cat .env 46 | 47 | - name: Generate deployment package 48 | run: | 49 | zip -r deploy.zip . -x '*.git' 50 | 51 | - name: Deploy on Elastic beanstalk PROD Env 52 | uses: einaregilsson/beanstalk-deploy@v20 53 | with: 54 | aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }} 55 | aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 56 | application_name: youtube-ms-prod 57 | environment_name: Youtubemsprod-env 58 | version_label: "ver-${{ github.sha }}" 59 | region: eu-central-1 60 | deployment_package: deploy.zip 61 | -------------------------------------------------------------------------------- /__Episodes/Part_6/CD_QA_Workflow.yml: -------------------------------------------------------------------------------- 1 | name: Deploy on QA 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | deploy_on_qa: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout Source Code 15 | uses: actions/checkout@v2 16 | 17 | - name: Create customer Env file 18 | working-directory: ./customer 19 | run: | 20 | touch .env 21 | echo APP_SECRET=${{ secrets.QA_APP_SECRET }} >> .env 22 | echo MONGODB_URI=${{ secrets.QA_CUSTOMER_DB_URL }} >> .env 23 | echo MSG_QUEUE_URL=${{ secrets.QA_MSG_QUEUE_URL }} >> .env 24 | echo EXCHANGE_NAME=ONLINE_STORE >> .env 25 | echo PORT=8001 >> .env 26 | cat .env 27 | 28 | - name: Create Products Env file 29 | working-directory: ./products 30 | run: | 31 | touch .env 32 | echo APP_SECRET=${{ secrets.QA_APP_SECRET }} >> .env 33 | echo MONGODB_URI=${{ secrets.QA_PRODUCTS_DB_URL }} >> .env 34 | echo MSG_QUEUE_URL=${{ secrets.QA_MSG_QUEUE_URL }} >> .env 35 | echo EXCHANGE_NAME=ONLINE_STORE >> .env 36 | echo PORT=8002 >> .env 37 | cat .env 38 | 39 | - name: Create shopping Env file 40 | working-directory: ./shopping 41 | run: | 42 | touch .env 43 | echo APP_SECRET=${{ secrets.QA_APP_SECRET }} >> .env 44 | echo MONGODB_URI=${{ secrets.QA_SHOPPING_DB_URL }} >> .env 45 | echo MSG_QUEUE_URL=${{ secrets.QA_MSG_QUEUE_URL }} >> .env 46 | echo EXCHANGE_NAME=ONLINE_STORE >> .env 47 | echo PORT=8003 >> .env 48 | cat .env 49 | 50 | - name: Generate deployment package 51 | run: | 52 | zip -r deploy.zip . -x '*.git' 53 | 54 | - name: Deploy on Elastic beanstalk QA Env 55 | uses: einaregilsson/beanstalk-deploy@v20 56 | with: 57 | aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }} 58 | aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 59 | application_name: youtube-ms 60 | environment_name: Youtubems-env 61 | version_label: "ver-${{ github.sha }}" 62 | region: eu-central-1 63 | deployment_package: deploy.zip 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /__Episodes/Part_6/CI_Workflow.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | branches: ["main"] 6 | 7 | jobs: 8 | ci_verification: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [14.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Test Customer Service 23 | working-directory: ./customer 24 | run: | 25 | npm ci 26 | npm test 27 | 28 | - name: Test Products Service 29 | working-directory: ./products 30 | run: | 31 | npm ci 32 | npm test 33 | 34 | - name: Test Shopping Service 35 | working-directory: ./shopping 36 | run: | 37 | npm ci 38 | npm test 39 | -------------------------------------------------------------------------------- /__Episodes/Part_6/part_6_nodejs_microservice.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codergogoi/nodejs_microservice/5e56802ea8935e86ba663b6a24ae4327ebcf689e/__Episodes/Part_6/part_6_nodejs_microservice.zip -------------------------------------------------------------------------------- /__Episodes/Part_7/Part_7.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codergogoi/nodejs_microservice/5e56802ea8935e86ba663b6a24ae4327ebcf689e/__Episodes/Part_7/Part_7.zip -------------------------------------------------------------------------------- /__Episodes/Part_8/Part_8.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codergogoi/nodejs_microservice/5e56802ea8935e86ba663b6a24ae4327ebcf689e/__Episodes/Part_8/Part_8.zip -------------------------------------------------------------------------------- /customer/.env.dev: -------------------------------------------------------------------------------- 1 | APP_SECRET ='jg_youtube_tutorial' 2 | 3 | # Mongo DB 4 | MONGODB_URI='mongodb://nosql-db/msytt_customer' 5 | 6 | MSG_QUEUE_URL='amqp://rabbitmq:5672' 7 | 8 | EXCHANGE_NAME='ONLINE_STORE' 9 | 10 | # Port 11 | PORT=8001 12 | 13 | 14 | -------------------------------------------------------------------------------- /customer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /app/customer 4 | 5 | COPY package.json . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 8001 12 | 13 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /customer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "customer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=prod node src/index.js", 8 | "dev": "NODE_ENV=dev nodemon src/index.js", 9 | "test": "jest" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "amqplib": "^0.8.0", 16 | "axios": "^0.21.1", 17 | "bcrypt": "^5.0.1", 18 | "cors": "^2.8.5", 19 | "dotenv": "^8.6.0", 20 | "express": "^4.17.1", 21 | "jsonwebtoken": "^8.5.1", 22 | "mongoose": "^5.12.3", 23 | "nodemon": "^2.0.7" 24 | }, 25 | "devDependencies": { 26 | "jest": "^29.0.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /customer/src/api/app-events.js: -------------------------------------------------------------------------------- 1 | const CustomerService = require("../services/customer-service"); 2 | 3 | module.exports = (app) => { 4 | 5 | const service = new CustomerService(); 6 | app.use('/app-events',async (req,res,next) => { 7 | 8 | const { payload } = req.body; 9 | 10 | //handle subscribe events 11 | service.SubscribeEvents(payload); 12 | 13 | console.log("============= Shopping ================"); 14 | console.log(payload); 15 | res.json(payload); 16 | 17 | }); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /customer/src/api/customer.js: -------------------------------------------------------------------------------- 1 | const CustomerService = require('../services/customer-service'); 2 | const UserAuth = require('./middlewares/auth'); 3 | const { SubscribeMessage } = require('../utils'); 4 | 5 | 6 | module.exports = (app, channel) => { 7 | 8 | const service = new CustomerService(); 9 | 10 | // To listen 11 | SubscribeMessage(channel, service); 12 | 13 | 14 | app.post('/signup', async (req,res,next) => { 15 | const { email, password, phone } = req.body; 16 | const { data } = await service.SignUp({ email, password, phone}); 17 | res.json(data); 18 | 19 | }); 20 | 21 | app.post('/login', async (req,res,next) => { 22 | 23 | const { email, password } = req.body; 24 | 25 | const { data } = await service.SignIn({ email, password}); 26 | 27 | res.json(data); 28 | 29 | }); 30 | 31 | app.post('/address', UserAuth, async (req,res,next) => { 32 | 33 | const { _id } = req.user; 34 | 35 | 36 | const { street, postalCode, city,country } = req.body; 37 | 38 | const { data } = await service.AddNewAddress( _id ,{ street, postalCode, city,country}); 39 | 40 | res.json(data); 41 | 42 | }); 43 | 44 | 45 | app.get('/profile', UserAuth ,async (req,res,next) => { 46 | 47 | const { _id } = req.user; 48 | const { data } = await service.GetProfile({ _id }); 49 | res.json(data); 50 | }); 51 | 52 | 53 | app.get('/shoping-details', UserAuth, async (req,res,next) => { 54 | const { _id } = req.user; 55 | const { data } = await service.GetShopingDetails(_id); 56 | 57 | return res.json(data); 58 | }); 59 | 60 | app.get('/wishlist', UserAuth, async (req,res,next) => { 61 | const { _id } = req.user; 62 | const { data } = await service.GetWishList( _id); 63 | return res.status(200).json(data); 64 | }); 65 | 66 | app.get('/whoami', (req,res,next) => { 67 | return res.status(200).json({msg: '/customer : I am Customer Service'}) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /customer/src/api/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | customer: require('./customer'), 4 | appEvents: require('./app-events') 5 | } 6 | -------------------------------------------------------------------------------- /customer/src/api/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const { ValidateSignature } = require('../../utils'); 2 | 3 | module.exports = async (req,res,next) => { 4 | 5 | const isAuthorized = await ValidateSignature(req); 6 | 7 | if(isAuthorized){ 8 | return next(); 9 | } 10 | return res.status(403).json({message: 'Not Authorized'}) 11 | } -------------------------------------------------------------------------------- /customer/src/config/index.js: -------------------------------------------------------------------------------- 1 | const dotEnv = require("dotenv"); 2 | 3 | if (process.env.NODE_ENV !== "prod") { 4 | const configFile = `./.env.${process.env.NODE_ENV}`; 5 | dotEnv.config({ path: configFile }); 6 | } else { 7 | dotEnv.config(); 8 | } 9 | 10 | module.exports = { 11 | PORT: process.env.PORT, 12 | DB_URL: process.env.MONGODB_URI, 13 | APP_SECRET: process.env.APP_SECRET, 14 | EXCHANGE_NAME: process.env.EXCHANGE_NAME, 15 | MSG_QUEUE_URL: process.env.MSG_QUEUE_URL, 16 | CUSTOMER_SERVICE: "customer_service", 17 | SHOPPING_SERVICE: "shopping_service", 18 | }; 19 | -------------------------------------------------------------------------------- /customer/src/database/connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { DB_URL } = require('../config'); 3 | 4 | module.exports = async() => { 5 | 6 | try { 7 | await mongoose.connect(DB_URL, { 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | useCreateIndex: true 11 | }); 12 | console.log('Db Connected'); 13 | 14 | } catch (error) { 15 | console.error('Error ============ ON DB Connection') 16 | console.log(error); 17 | } 18 | 19 | }; 20 | 21 | 22 | -------------------------------------------------------------------------------- /customer/src/database/index.js: -------------------------------------------------------------------------------- 1 | // database related modules 2 | module.exports = { 3 | databaseConnection: require('./connection'), 4 | CustomerRepository: require('./repository/customer-repository'), 5 | } -------------------------------------------------------------------------------- /customer/src/database/models/Address.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const AddressSchema = new Schema({ 6 | street: String, 7 | postalCode: String, 8 | city: String, 9 | country: String 10 | }); 11 | 12 | module.exports = mongoose.model('address', AddressSchema); -------------------------------------------------------------------------------- /customer/src/database/models/Customer.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const CustomerSchema = new Schema({ 6 | email: String, 7 | password: String, 8 | salt: String, 9 | phone: String, 10 | address:[ 11 | { type: Schema.Types.ObjectId, ref: 'address', require: true } 12 | ], 13 | cart: [ 14 | { 15 | product: { 16 | _id: { type: String, require: true}, 17 | name: { type: String}, 18 | banner: { type: String}, 19 | price: { type: Number}, 20 | }, 21 | unit: { type: Number, require: true} 22 | } 23 | ], 24 | wishlist:[ 25 | { 26 | _id: { type: String, require: true }, 27 | name: { type: String }, 28 | description: { type: String }, 29 | banner: { type: String }, 30 | avalable: { type: Boolean }, 31 | price: { type: Number }, 32 | } 33 | ], 34 | orders: [ 35 | { 36 | _id: {type: String, required: true}, 37 | amount: { type: String}, 38 | date: {type: Date, default: Date.now()} 39 | } 40 | ] 41 | },{ 42 | toJSON: { 43 | transform(doc, ret){ 44 | delete ret.password; 45 | delete ret.salt; 46 | delete ret.__v; 47 | } 48 | }, 49 | timestamps: true 50 | }); 51 | 52 | module.exports = mongoose.model('customer', CustomerSchema); 53 | -------------------------------------------------------------------------------- /customer/src/database/models/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | CustomerModel: require('./Customer'), 3 | AddressModel: require('./Address') 4 | } -------------------------------------------------------------------------------- /customer/src/database/repository/customer-repository.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { CustomerModel, AddressModel } = require('../models'); 3 | 4 | //Dealing with data base operations 5 | class CustomerRepository { 6 | 7 | async CreateCustomer({ email, password, phone, salt }){ 8 | 9 | const customer = new CustomerModel({ 10 | email, 11 | password, 12 | salt, 13 | phone, 14 | address: [] 15 | }) 16 | 17 | const customerResult = await customer.save(); 18 | return customerResult; 19 | } 20 | 21 | async CreateAddress({ _id, street, postalCode, city, country}){ 22 | 23 | const profile = await CustomerModel.findById(_id); 24 | 25 | if(profile){ 26 | 27 | const newAddress = new AddressModel({ 28 | street, 29 | postalCode, 30 | city, 31 | country 32 | }) 33 | 34 | await newAddress.save(); 35 | 36 | profile.address.push(newAddress); 37 | } 38 | 39 | return await profile.save(); 40 | } 41 | 42 | async FindCustomer({ email }){ 43 | const existingCustomer = await CustomerModel.findOne({ email: email }); 44 | return existingCustomer; 45 | } 46 | 47 | async FindCustomerById({ id }){ 48 | 49 | const existingCustomer = await CustomerModel.findById(id).populate('address'); 50 | // existingCustomer.cart = []; 51 | // existingCustomer.orders = []; 52 | // existingCustomer.wishlist = []; 53 | 54 | // await existingCustomer.save(); 55 | return existingCustomer; 56 | } 57 | 58 | async Wishlist(customerId){ 59 | 60 | const profile = await CustomerModel.findById(customerId).populate('wishlist'); 61 | 62 | return profile.wishlist; 63 | } 64 | 65 | async AddWishlistItem(customerId, { _id, name, desc, price, available, banner}){ 66 | 67 | const product = { 68 | _id, name, desc, price, available, banner 69 | }; 70 | 71 | const profile = await CustomerModel.findById(customerId).populate('wishlist'); 72 | 73 | if(profile){ 74 | 75 | let wishlist = profile.wishlist; 76 | 77 | if(wishlist.length > 0){ 78 | let isExist = false; 79 | wishlist.map(item => { 80 | if(item._id.toString() === product._id.toString()){ 81 | const index = wishlist.indexOf(item); 82 | wishlist.splice(index,1); 83 | isExist = true; 84 | } 85 | }); 86 | 87 | if(!isExist){ 88 | wishlist.push(product); 89 | } 90 | 91 | }else{ 92 | wishlist.push(product); 93 | } 94 | 95 | profile.wishlist = wishlist; 96 | } 97 | 98 | const profileResult = await profile.save(); 99 | 100 | return profileResult.wishlist; 101 | 102 | } 103 | 104 | 105 | async AddCartItem(customerId, { _id, name, price, banner},qty, isRemove){ 106 | 107 | 108 | const profile = await CustomerModel.findById(customerId).populate('cart'); 109 | 110 | 111 | if(profile){ 112 | 113 | const cartItem = { 114 | product: { _id, name, price, banner }, 115 | unit: qty, 116 | }; 117 | 118 | let cartItems = profile.cart; 119 | 120 | if(cartItems.length > 0){ 121 | let isExist = false; 122 | cartItems.map(item => { 123 | if(item.product._id.toString() === _id.toString()){ 124 | 125 | if(isRemove){ 126 | cartItems.splice(cartItems.indexOf(item), 1); 127 | }else{ 128 | item.unit = qty; 129 | } 130 | isExist = true; 131 | } 132 | }); 133 | 134 | if(!isExist){ 135 | cartItems.push(cartItem); 136 | } 137 | }else{ 138 | cartItems.push(cartItem); 139 | } 140 | 141 | profile.cart = cartItems; 142 | 143 | return await profile.save(); 144 | } 145 | 146 | throw new Error('Unable to add to cart!'); 147 | } 148 | 149 | 150 | 151 | async AddOrderToProfile(customerId, order){ 152 | 153 | const profile = await CustomerModel.findById(customerId); 154 | 155 | if(profile){ 156 | 157 | if(profile.orders == undefined){ 158 | profile.orders = [] 159 | } 160 | profile.orders.push(order); 161 | 162 | profile.cart = []; 163 | 164 | const profileResult = await profile.save(); 165 | 166 | return profileResult; 167 | } 168 | 169 | throw new Error('Unable to add to order!'); 170 | } 171 | 172 | 173 | 174 | 175 | } 176 | 177 | module.exports = CustomerRepository; 178 | -------------------------------------------------------------------------------- /customer/src/express-app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const { customer, appEvents } = require('./api'); 4 | const { CreateChannel, SubscribeMessage } = require('./utils') 5 | 6 | module.exports = async (app) => { 7 | 8 | app.use(express.json()); 9 | app.use(cors()); 10 | app.use(express.static(__dirname + '/public')) 11 | 12 | //api 13 | // appEvents(app); 14 | 15 | const channel = await CreateChannel() 16 | 17 | 18 | customer(app, channel); 19 | // error handling 20 | 21 | } 22 | -------------------------------------------------------------------------------- /customer/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { PORT } = require('./config'); 3 | const { databaseConnection } = require('./database'); 4 | const expressApp = require('./express-app'); 5 | const { CreateChannel } = require('./utils') 6 | 7 | const StartServer = async() => { 8 | 9 | const app = express(); 10 | 11 | await databaseConnection(); 12 | 13 | const channel = await CreateChannel() 14 | 15 | await expressApp(app, channel); 16 | 17 | 18 | app.listen(PORT, () => { 19 | console.log(`listening to port ${PORT}`); 20 | }) 21 | .on('error', (err) => { 22 | console.log(err); 23 | process.exit(); 24 | }) 25 | .on('close', () => { 26 | channel.close(); 27 | }) 28 | 29 | 30 | } 31 | 32 | StartServer(); 33 | -------------------------------------------------------------------------------- /customer/src/sampledata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"alphonso mango", 4 | "desc":"great Quality of Mango", 5 | "type":"fruits", 6 | "banner":"http://codergogoi.com/youtube/images/alphonso.jpeg", 7 | "unit":1, 8 | "price":300, 9 | "available":true, 10 | "suplier":"Golden seed firming" 11 | }, 12 | { 13 | "name":"Apples", 14 | "desc":"great Quality of Apple", 15 | "type":"fruits", 16 | "banner":"http://codergogoi.com/youtube/images/apples.jpeg", 17 | "unit":1, 18 | "price":140, 19 | "available":true, 20 | "suplier":"Golden seed firming" 21 | }, 22 | { 23 | "name":"Kesar Mango", 24 | "desc":"great Quality of Mango", 25 | "type":"fruits", 26 | "banner":"http://codergogoi.com/youtube/images/kesar.jpeg", 27 | "unit":1, 28 | "price":170, 29 | "available":true, 30 | "suplier":"Golden seed firming" 31 | }, 32 | { 33 | "name":"Langra Mango", 34 | "desc":"great Quality of Mango", 35 | "type":"fruits", 36 | "banner":"http://codergogoi.com/youtube/images/langra.jpeg", 37 | "unit":1, 38 | "price":280, 39 | "available":true, 40 | "suplier":"Golden seed firming" 41 | }, 42 | { 43 | "name":"Broccoli", 44 | "desc":"great Quality of Fresh Vegetable", 45 | "type":"vegetables", 46 | "banner":"http://codergogoi.com/youtube/images/broccoli.jpeg", 47 | "unit":1, 48 | "price":280, 49 | "available":true, 50 | "suplier":"Golden seed firming" 51 | }, 52 | { 53 | "name":"Cauliflower", 54 | "desc":"great Quality of Fresh Vegetable", 55 | "type":"vegetables", 56 | "banner":"http://codergogoi.com/youtube/images/cauliflower.jpeg", 57 | "unit":1, 58 | "price":280, 59 | "available":true, 60 | "suplier":"Golden seed firming" 61 | }, 62 | { 63 | "name":"Olive Oil", 64 | "desc":"great Quality of Oil", 65 | "type":"oils", 66 | "banner":"http://codergogoi.com/youtube/images/oliveoil.jpg", 67 | "unit":1, 68 | "price":400, 69 | "available":true, 70 | "suplier":"Golden seed firming" 71 | }, 72 | { 73 | "name":"Olive Oil", 74 | "desc":"great Quality of Oil", 75 | "type":"oils", 76 | "banner":"http://codergogoi.com/youtube/images/potatos.jpeg", 77 | "unit":1, 78 | "price":400, 79 | "available":true, 80 | "suplier":"Golden seed firming" 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /customer/src/services/customer-service.js: -------------------------------------------------------------------------------- 1 | const { CustomerRepository } = require("../database"); 2 | const { FormateData, GeneratePassword, GenerateSalt, GenerateSignature, ValidatePassword } = require('../utils'); 3 | 4 | // All Business logic will be here 5 | class CustomerService { 6 | 7 | constructor(){ 8 | this.repository = new CustomerRepository(); 9 | } 10 | 11 | async SignIn(userInputs){ 12 | 13 | const { email, password } = userInputs; 14 | 15 | const existingCustomer = await this.repository.FindCustomer({ email}); 16 | 17 | if(existingCustomer){ 18 | 19 | const validPassword = await ValidatePassword(password, existingCustomer.password, existingCustomer.salt); 20 | if(validPassword){ 21 | const token = await GenerateSignature({ email: existingCustomer.email, _id: existingCustomer._id}); 22 | return FormateData({id: existingCustomer._id, token }); 23 | } 24 | } 25 | 26 | return FormateData(null); 27 | } 28 | 29 | async SignUp(userInputs){ 30 | 31 | const { email, password, phone } = userInputs; 32 | 33 | // create salt 34 | let salt = await GenerateSalt(); 35 | 36 | let userPassword = await GeneratePassword(password, salt); 37 | 38 | const existingCustomer = await this.repository.CreateCustomer({ email, password: userPassword, phone, salt}); 39 | 40 | const token = await GenerateSignature({ email: email, _id: existingCustomer._id}); 41 | return FormateData({id: existingCustomer._id, token }); 42 | 43 | } 44 | 45 | async AddNewAddress(_id,userInputs){ 46 | 47 | const { street, postalCode, city,country} = userInputs; 48 | 49 | const addressResult = await this.repository.CreateAddress({ _id, street, postalCode, city,country}) 50 | 51 | return FormateData(addressResult); 52 | } 53 | 54 | async GetProfile(id){ 55 | 56 | const existingCustomer = await this.repository.FindCustomerById({id}); 57 | return FormateData(existingCustomer); 58 | } 59 | 60 | async GetShopingDetails(id){ 61 | 62 | const existingCustomer = await this.repository.FindCustomerById({id}); 63 | 64 | if(existingCustomer){ 65 | // const orders = await this.shopingRepository.Orders(id); 66 | return FormateData(existingCustomer); 67 | } 68 | return FormateData({ msg: 'Error'}); 69 | } 70 | 71 | async GetWishList(customerId){ 72 | const wishListItems = await this.repository.Wishlist(customerId); 73 | return FormateData(wishListItems); 74 | } 75 | 76 | async AddToWishlist(customerId, product){ 77 | const wishlistResult = await this.repository.AddWishlistItem(customerId, product); 78 | return FormateData(wishlistResult); 79 | } 80 | 81 | async ManageCart(customerId, product, qty, isRemove){ 82 | const cartResult = await this.repository.AddCartItem(customerId, product, qty, isRemove); 83 | return FormateData(cartResult); 84 | } 85 | 86 | async ManageOrder(customerId, order){ 87 | const orderResult = await this.repository.AddOrderToProfile(customerId, order); 88 | return FormateData(orderResult); 89 | } 90 | 91 | async SubscribeEvents(payload){ 92 | 93 | console.log('Triggering.... Customer Events') 94 | 95 | payload = JSON.parse(payload) 96 | 97 | const { event, data } = payload; 98 | 99 | const { userId, product, order, qty } = data; 100 | 101 | switch(event){ 102 | case 'ADD_TO_WISHLIST': 103 | case 'REMOVE_FROM_WISHLIST': 104 | this.AddToWishlist(userId,product) 105 | break; 106 | case 'ADD_TO_CART': 107 | this.ManageCart(userId,product, qty, false); 108 | break; 109 | case 'REMOVE_FROM_CART': 110 | this.ManageCart(userId,product,qty, true); 111 | break; 112 | case 'CREATE_ORDER': 113 | this.ManageOrder(userId,order); 114 | break; 115 | default: 116 | break; 117 | } 118 | 119 | } 120 | 121 | } 122 | 123 | module.exports = CustomerService; 124 | -------------------------------------------------------------------------------- /customer/src/services/customer-service.test.js: -------------------------------------------------------------------------------- 1 | // which service it is 2 | describe("CustomerService", () => { 3 | // Which function 4 | describe("SignIn", () => { 5 | // Which Scenario we are testing 6 | test("validate user inputs", () => {}); 7 | 8 | test("Validate response", async () => {}); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /customer/src/utils/app-errors.js: -------------------------------------------------------------------------------- 1 | const STATUS_CODES = { 2 | OK: 200, 3 | BAD_REQUEST: 400, 4 | UN_AUTHORISED: 403, 5 | NOT_FOUND: 404, 6 | INTERNAL_ERROR: 500, 7 | } 8 | 9 | class AppError extends Error { 10 | constructor(name,statusCode,description, isOperational, errorStack, logingErrorResponse){ 11 | super(description); 12 | Object.setPrototypeOf(this,new.target.prototype); 13 | this.name = name; 14 | this.statusCode = statusCode; 15 | this.isOperational = isOperational 16 | this.errorStack = errorStack; 17 | this.logError = logingErrorResponse; 18 | Error.captureStackTrace(this); 19 | } 20 | } 21 | 22 | //api Specific Errors 23 | class APIError extends AppError { 24 | constructor(name, statusCode = STATUS_CODES.INTERNAL_ERROR, description ='Internal Server Error',isOperational = true,){ 25 | super(name,statusCode,description,isOperational); 26 | } 27 | } 28 | 29 | //400 30 | class BadRequestError extends AppError { 31 | constructor(description = 'Bad request',logingErrorResponse){ 32 | super('NOT FOUND', STATUS_CODES.BAD_REQUEST,description,true, false, logingErrorResponse); 33 | } 34 | } 35 | 36 | //400 37 | class ValidationError extends AppError { 38 | constructor(description = 'Validation Error', errorStack){ 39 | super('BAD REQUEST', STATUS_CODES.BAD_REQUEST,description,true, errorStack); 40 | } 41 | } 42 | 43 | 44 | module.exports = { 45 | AppError, 46 | APIError, 47 | BadRequestError, 48 | ValidationError, 49 | STATUS_CODES, 50 | } 51 | -------------------------------------------------------------------------------- /customer/src/utils/error-handler.js: -------------------------------------------------------------------------------- 1 | const { createLogger, transports } = require('winston'); 2 | const { AppError } = require('./app-errors'); 3 | 4 | 5 | const LogErrors = createLogger({ 6 | transports: [ 7 | new transports.Console(), 8 | new transports.File({ filename: 'app_error.log' }) 9 | ] 10 | }); 11 | 12 | 13 | class ErrorLogger { 14 | constructor(){} 15 | async logError(err){ 16 | console.log('==================== Start Error Logger ==============='); 17 | LogErrors.log({ 18 | private: true, 19 | level: 'error', 20 | message: `${new Date()}-${JSON.stringify(err)}` 21 | }); 22 | console.log('==================== End Error Logger ==============='); 23 | // log error with Logger plugins 24 | 25 | return false; 26 | } 27 | 28 | isTrustError(error){ 29 | if(error instanceof AppError){ 30 | return error.isOperational; 31 | }else{ 32 | return false; 33 | } 34 | } 35 | } 36 | 37 | const ErrorHandler = async(err,req,res,next) => { 38 | 39 | const errorLogger = new ErrorLogger(); 40 | 41 | process.on('uncaughtException', (reason, promise) => { 42 | console.log(reason, 'UNHANDLED'); 43 | throw reason; // need to take care 44 | }) 45 | 46 | process.on('uncaughtException', (error) => { 47 | errorLogger.logError(error); 48 | if(errorLogger.isTrustError(err)){ 49 | //process exist // need restart 50 | } 51 | }) 52 | 53 | // console.log(err.description, '-------> DESCRIPTION') 54 | // console.log(err.message, '-------> MESSAGE') 55 | // console.log(err.name, '-------> NAME') 56 | if(err){ 57 | await errorLogger.logError(err); 58 | if(errorLogger.isTrustError(err)){ 59 | if(err.errorStack){ 60 | const errorDescription = err.errorStack; 61 | return res.status(err.statusCode).json({'message': errorDescription}) 62 | } 63 | return res.status(err.statusCode).json({'message': err.message }) 64 | }else{ 65 | //process exit // terriablly wrong with flow need restart 66 | } 67 | return res.status(err.statusCode).json({'message': err.message}) 68 | } 69 | next(); 70 | } 71 | 72 | module.exports = ErrorHandler; -------------------------------------------------------------------------------- /customer/src/utils/index.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcrypt"); 2 | const jwt = require("jsonwebtoken"); 3 | const amqplib = require("amqplib"); 4 | 5 | const { 6 | APP_SECRET, 7 | EXCHANGE_NAME, 8 | CUSTOMER_SERVICE, 9 | MSG_QUEUE_URL, 10 | } = require("../config"); 11 | 12 | //Utility functions 13 | module.exports.GenerateSalt = async () => { 14 | return await bcrypt.genSalt(); 15 | }; 16 | 17 | module.exports.GeneratePassword = async (password, salt) => { 18 | return await bcrypt.hash(password, salt); 19 | }; 20 | 21 | module.exports.ValidatePassword = async ( 22 | enteredPassword, 23 | savedPassword, 24 | salt 25 | ) => { 26 | return (await this.GeneratePassword(enteredPassword, salt)) === savedPassword; 27 | }; 28 | 29 | module.exports.GenerateSignature = async (payload) => { 30 | try { 31 | return await jwt.sign(payload, APP_SECRET, { expiresIn: "30d" }); 32 | } catch (error) { 33 | console.log(error); 34 | return error; 35 | } 36 | }; 37 | 38 | module.exports.ValidateSignature = async (req) => { 39 | try { 40 | const signature = req.get("Authorization"); 41 | console.log(signature); 42 | const payload = await jwt.verify(signature.split(" ")[1], APP_SECRET); 43 | req.user = payload; 44 | return true; 45 | } catch (error) { 46 | console.log(error); 47 | return false; 48 | } 49 | }; 50 | 51 | module.exports.FormateData = (data) => { 52 | if (data) { 53 | return { data }; 54 | } else { 55 | throw new Error("Data Not found!"); 56 | } 57 | }; 58 | 59 | //Message Broker 60 | module.exports.CreateChannel = async () => { 61 | try { 62 | const connection = await amqplib.connect(MSG_QUEUE_URL); 63 | const channel = await connection.createChannel(); 64 | await channel.assertQueue(EXCHANGE_NAME, "direct", { durable: true }); 65 | return channel; 66 | } catch (err) { 67 | throw err; 68 | } 69 | }; 70 | 71 | module.exports.PublishMessage = (channel, service, msg) => { 72 | channel.publish(EXCHANGE_NAME, service, Buffer.from(msg)); 73 | console.log("Sent: ", msg); 74 | }; 75 | 76 | module.exports.SubscribeMessage = async (channel, service) => { 77 | await channel.assertExchange(EXCHANGE_NAME, "direct", { durable: true }); 78 | const q = await channel.assertQueue("", { exclusive: true }); 79 | console.log(` Waiting for messages in queue: ${q.queue}`); 80 | 81 | channel.bindQueue(q.queue, EXCHANGE_NAME, CUSTOMER_SERVICE); 82 | 83 | channel.consume( 84 | q.queue, 85 | (msg) => { 86 | if (msg.content) { 87 | console.log("the message is:", msg.content.toString()); 88 | service.SubscribeEvents(msg.content.toString()); 89 | } 90 | console.log("[X] received"); 91 | }, 92 | { 93 | noAck: true, 94 | } 95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | nosql-db: 4 | image: mvertes/alpine-mongo 5 | ports: 6 | - "27018:27017" 7 | container_name: nosql-db 8 | volumes: 9 | - ./db/:/data/db 10 | 11 | rabbitmq: 12 | image: rabbitmq:alpine 13 | container_name: rabbitmq 14 | ports: 15 | - '5672:5672' 16 | 17 | products: 18 | build: 19 | dockerfile: Dockerfile 20 | context: ./products 21 | container_name: products 22 | ports: 23 | - "8002:8002" 24 | restart: always 25 | depends_on: 26 | - "nosql-db" 27 | - "rabbitmq" 28 | volumes: 29 | - .:/app 30 | - /app/products/node_modules 31 | 32 | env_file: 33 | - ./products/.env.dev 34 | shopping: 35 | build: 36 | dockerfile: Dockerfile 37 | context: ./shopping 38 | container_name: shopping 39 | ports: 40 | - "8003:8003" 41 | restart: always 42 | depends_on: 43 | - "nosql-db" 44 | - "rabbitmq" 45 | volumes: 46 | - .:/app 47 | - /app/shopping/node_modules 48 | env_file: 49 | - ./shopping/.env.dev 50 | customer: 51 | build: 52 | dockerfile: Dockerfile 53 | context: ./customer 54 | container_name: customer 55 | ports: 56 | - "8001:8001" 57 | restart: always 58 | depends_on: 59 | - "nosql-db" 60 | - "rabbitmq" 61 | volumes: 62 | - .:/app/ 63 | - /app/customer/node_modules 64 | env_file: 65 | - ./customer/.env.dev 66 | nginx-proxy: 67 | build: 68 | dockerfile: Dockerfile 69 | context: ./proxy 70 | depends_on: 71 | - products 72 | - shopping 73 | - customer 74 | ports: 75 | - 80:80 76 | -------------------------------------------------------------------------------- /gateway/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cors = require("cors"); 3 | const proxy = require("express-http-proxy"); 4 | 5 | const app = express(); 6 | 7 | app.use(cors()); 8 | app.use(express.json()); 9 | 10 | app.use("/customer", proxy("http://localhost:8001")); 11 | app.use("/shopping", proxy("http://localhost:8003")); 12 | app.use("/", proxy("http://localhost:8002")); // products 13 | 14 | app.listen(8000, () => { 15 | console.log("Gateway is Listening to Port 8000"); 16 | }); 17 | -------------------------------------------------------------------------------- /gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gateway", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "nodemon index.js" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "description": "", 12 | "dependencies": { 13 | "cors": "^2.8.5", 14 | "express": "^4.17.1", 15 | "express-http-proxy": "^1.6.2" 16 | }, 17 | "devDependencies": { 18 | "nodemon": "^2.0.12" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /products/.env.dev: -------------------------------------------------------------------------------- 1 | APP_SECRET ='jg_youtube_tutorial' 2 | 3 | # Mongo DB 4 | MONGODB_URI='mongodb://nosql-db/msytt_product' 5 | 6 | MSG_QUEUE_URL='amqp://rabbitmq:5672' 7 | 8 | EXCHANGE_NAME='ONLINE_STORE' 9 | 10 | # Port 11 | PORT=8002 12 | 13 | 14 | -------------------------------------------------------------------------------- /products/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /app/products 4 | 5 | COPY package.json . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 8002 12 | 13 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /products/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | 5 | app.use(express.json()); 6 | 7 | app.use('/', (req,res,next) => { 8 | 9 | return res.status(200).json({"msg": "Hello from Products"}) 10 | }) 11 | 12 | 13 | app.listen(8002, () => { 14 | console.log('Products is Listening to Port 8002') 15 | }) -------------------------------------------------------------------------------- /products/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "products", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=prod node src/index.js", 8 | "dev": "NODE_ENV=dev nodemon src/index.js", 9 | "test": "jest" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "amqplib": "^0.8.0", 16 | "axios": "^0.21.1", 17 | "bcryptjs": "^2.4.3", 18 | "cors": "^2.8.5", 19 | "dotenv": "^8.6.0", 20 | "express": "^4.17.1", 21 | "jsonwebtoken": "^8.5.1", 22 | "mongoose": "^5.12.3", 23 | "nodemon": "^2.0.7" 24 | }, 25 | "devDependencies": { 26 | "jest": "^29.0.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /products/src/api/app-events.js: -------------------------------------------------------------------------------- 1 | // const ShoppingService = require("../services/shopping-service"); 2 | 3 | module.exports = (app) => { 4 | 5 | // const service = new ShoppingService(); 6 | app.use('/app-events',async (req,res,next) => { 7 | 8 | const { payload } = req.body; 9 | 10 | console.log("============= Shopping ================"); 11 | console.log(payload); 12 | 13 | return res.status(200).json({ message: 'notified!'}); 14 | 15 | }); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /products/src/api/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | products: require('./products'), 4 | appEvents: require('./app-events'), 5 | } 6 | -------------------------------------------------------------------------------- /products/src/api/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const { ValidateSignature } = require('../../utils'); 2 | 3 | module.exports = async (req,res,next) => { 4 | 5 | const isAuthorized = await ValidateSignature(req); 6 | 7 | if(isAuthorized){ 8 | return next(); 9 | } 10 | return res.status(403).json({message: 'Not Authorized'}) 11 | } -------------------------------------------------------------------------------- /products/src/api/products.js: -------------------------------------------------------------------------------- 1 | const { CUSTOMER_SERVICE, SHOPPING_SERVICE } = require("../config"); 2 | const ProductService = require("../services/product-service"); 3 | const { 4 | PublishCustomerEvent, 5 | PublishShoppingEvent, 6 | PublishMessage, 7 | } = require("../utils"); 8 | const UserAuth = require("./middlewares/auth"); 9 | 10 | module.exports = (app, channel) => { 11 | const service = new ProductService(); 12 | 13 | app.post("/product/create", async (req, res, next) => { 14 | const { name, desc, type, unit, price, available, suplier, banner } = 15 | req.body; 16 | // validation 17 | const { data } = await service.CreateProduct({ 18 | name, 19 | desc, 20 | type, 21 | unit, 22 | price, 23 | available, 24 | suplier, 25 | banner, 26 | }); 27 | return res.json(data); 28 | }); 29 | 30 | app.get("/category/:type", async (req, res, next) => { 31 | const type = req.params.type; 32 | 33 | try { 34 | const { data } = await service.GetProductsByCategory(type); 35 | return res.status(200).json(data); 36 | } catch (error) { 37 | return res.status(404).json({ error }); 38 | } 39 | }); 40 | 41 | app.get("/:id", async (req, res, next) => { 42 | const productId = req.params.id; 43 | 44 | try { 45 | const { data } = await service.GetProductDescription(productId); 46 | return res.status(200).json(data); 47 | } catch (error) { 48 | return res.status(404).json({ error }); 49 | } 50 | }); 51 | 52 | app.post("/ids", async (req, res, next) => { 53 | const { ids } = req.body; 54 | const products = await service.GetSelectedProducts(ids); 55 | return res.status(200).json(products); 56 | }); 57 | 58 | app.put("/wishlist", UserAuth, async (req, res, next) => { 59 | const { _id } = req.user; 60 | 61 | const { data } = await service.GetProductPayload( 62 | _id, 63 | { productId: req.body._id }, 64 | "ADD_TO_WISHLIST" 65 | ); 66 | 67 | // PublishCustomerEvent(data); 68 | PublishMessage(channel, CUSTOMER_SERVICE, JSON.stringify(data)); 69 | 70 | res.status(200).json(data.data.product); 71 | }); 72 | 73 | app.delete("/wishlist/:id", UserAuth, async (req, res, next) => { 74 | const { _id } = req.user; 75 | const productId = req.params.id; 76 | 77 | const { data } = await service.GetProductPayload( 78 | _id, 79 | { productId }, 80 | "REMOVE_FROM_WISHLIST" 81 | ); 82 | // PublishCustomerEvent(data); 83 | PublishMessage(channel, CUSTOMER_SERVICE, JSON.stringify(data)); 84 | 85 | res.status(200).json(data.data.product); 86 | }); 87 | 88 | app.put("/cart", UserAuth, async (req, res, next) => { 89 | const { _id } = req.user; 90 | 91 | const { data } = await service.GetProductPayload( 92 | _id, 93 | { productId: req.body._id, qty: req.body.qty }, 94 | "ADD_TO_CART" 95 | ); 96 | 97 | // PublishCustomerEvent(data); 98 | // PublishShoppingEvent(data); 99 | 100 | PublishMessage(channel, CUSTOMER_SERVICE, JSON.stringify(data)); 101 | PublishMessage(channel, SHOPPING_SERVICE, JSON.stringify(data)); 102 | 103 | const response = { product: data.data.product, unit: data.data.qty }; 104 | 105 | res.status(200).json(response); 106 | }); 107 | 108 | app.delete("/cart/:id", UserAuth, async (req, res, next) => { 109 | const { _id } = req.user; 110 | const productId = req.params.id; 111 | 112 | const { data } = await service.GetProductPayload( 113 | _id, 114 | { productId }, 115 | "REMOVE_FROM_CART" 116 | ); 117 | 118 | // PublishCustomerEvent(data); 119 | // PublishShoppingEvent(data); 120 | 121 | PublishMessage(channel, CUSTOMER_SERVICE, JSON.stringify(data)); 122 | PublishMessage(channel, SHOPPING_SERVICE, JSON.stringify(data)); 123 | 124 | const response = { product: data.data.product, unit: data.data.qty }; 125 | 126 | res.status(200).json(response); 127 | }); 128 | 129 | app.get("/whoami", (req, res, next) => { 130 | return res 131 | .status(200) 132 | .json({ msg: "/ or /products : I am products Service" }); 133 | }); 134 | 135 | //get Top products and category 136 | app.get("/", async (req, res, next) => { 137 | //check validation 138 | try { 139 | const { data } = await service.GetProducts(); 140 | return res.status(200).json(data); 141 | } catch (error) { 142 | return res.status(404).json({ error }); 143 | } 144 | }); 145 | }; 146 | -------------------------------------------------------------------------------- /products/src/config/index.js: -------------------------------------------------------------------------------- 1 | const dotEnv = require("dotenv"); 2 | 3 | if (process.env.NODE_ENV !== "prod") { 4 | const configFile = `./.env.${process.env.NODE_ENV}`; 5 | dotEnv.config({ path: configFile }); 6 | } else { 7 | dotEnv.config(); 8 | } 9 | 10 | module.exports = { 11 | PORT: process.env.PORT, 12 | DB_URL: process.env.MONGODB_URI, 13 | APP_SECRET: process.env.APP_SECRET, 14 | BASE_URL: process.env.BASE_URL, 15 | EXCHANGE_NAME: process.env.EXCHANGE_NAME, 16 | MSG_QUEUE_URL: process.env.MSG_QUEUE_URL, 17 | CUSTOMER_SERVICE: "customer_service", 18 | SHOPPING_SERVICE: "shopping_service", 19 | }; 20 | -------------------------------------------------------------------------------- /products/src/database/connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const { DB_URL } = require("../config"); 3 | 4 | module.exports = async () => { 5 | try { 6 | await mongoose.connect(DB_URL, { 7 | useNewUrlParser: true, 8 | useUnifiedTopology: true, 9 | useCreateIndex: true, 10 | }); 11 | console.log("Db Connected"); 12 | } catch (error) { 13 | console.log("Error ============"); 14 | console.log(error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /products/src/database/index.js: -------------------------------------------------------------------------------- 1 | // database related modules 2 | module.exports = { 3 | databaseConnection: require('./connection'), 4 | ProductRepository: require('./repository/product-repository'), 5 | } -------------------------------------------------------------------------------- /products/src/database/models/Product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const ProductSchema = new Schema({ 6 | name: String, 7 | desc: String, 8 | banner: String, 9 | type: String, 10 | unit: Number, 11 | price: Number, 12 | available: Boolean, 13 | suplier: String 14 | }); 15 | 16 | module.exports = mongoose.model('product', ProductSchema); -------------------------------------------------------------------------------- /products/src/database/models/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ProductModel: require('./Product'), 3 | } -------------------------------------------------------------------------------- /products/src/database/repository/product-repository.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { ProductModel } = require("../models"); 3 | 4 | //Dealing with data base operations 5 | class ProductRepository { 6 | 7 | 8 | async CreateProduct({ name, desc, type, unit,price, available, suplier, banner }){ 9 | 10 | const product = new ProductModel({ 11 | name, desc, type, unit,price, available, suplier, banner 12 | }) 13 | 14 | // return await ProductModel.findByIdAndDelete('607286419f4a1007c1fa7f40'); 15 | 16 | const productResult = await product.save(); 17 | return productResult; 18 | } 19 | 20 | 21 | async Products(){ 22 | return await ProductModel.find(); 23 | } 24 | 25 | async FindById(id){ 26 | 27 | return await ProductModel.findById(id); 28 | 29 | } 30 | 31 | async FindByCategory(category){ 32 | 33 | const products = await ProductModel.find({ type: category}); 34 | 35 | return products; 36 | } 37 | 38 | async FindSelectedProducts(selectedIds){ 39 | const products = await ProductModel.find().where('_id').in(selectedIds.map(_id => _id)).exec(); 40 | return products; 41 | } 42 | 43 | } 44 | 45 | module.exports = ProductRepository; 46 | -------------------------------------------------------------------------------- /products/src/express-app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cors = require("cors"); 3 | const path = require("path"); 4 | const { products, appEvents } = require("./api"); 5 | 6 | const { CreateChannel } = require("./utils"); 7 | 8 | module.exports = async (app) => { 9 | app.use(express.json()); 10 | app.use(cors()); 11 | app.use(express.static(__dirname + "/public")); 12 | 13 | //api 14 | // appEvents(app); 15 | 16 | const channel = await CreateChannel(); 17 | products(app, channel); 18 | 19 | // error handling 20 | }; 21 | -------------------------------------------------------------------------------- /products/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { PORT } = require('./config'); 3 | const { databaseConnection } = require('./database'); 4 | const expressApp = require('./express-app'); 5 | 6 | const StartServer = async() => { 7 | 8 | const app = express(); 9 | 10 | await databaseConnection(); 11 | 12 | await expressApp(app); 13 | 14 | app.listen(PORT, () => { 15 | console.log(`listening to port ${PORT}`); 16 | }) 17 | .on('error', (err) => { 18 | console.log(err); 19 | process.exit(); 20 | }) 21 | 22 | } 23 | 24 | StartServer(); 25 | -------------------------------------------------------------------------------- /products/src/sampledata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"alphonso mango", 4 | "desc":"great Quality of Mango", 5 | "type":"fruits", 6 | "banner":"http://codergogoi.com/youtube/images/alphonso.jpeg", 7 | "unit":1, 8 | "price":300, 9 | "available":true, 10 | "suplier":"Golden seed firming" 11 | }, 12 | { 13 | "name":"Apples", 14 | "desc":"great Quality of Apple", 15 | "type":"fruits", 16 | "banner":"http://codergogoi.com/youtube/images/apples.jpeg", 17 | "unit":1, 18 | "price":140, 19 | "available":true, 20 | "suplier":"Golden seed firming" 21 | }, 22 | { 23 | "name":"Kesar Mango", 24 | "desc":"great Quality of Mango", 25 | "type":"fruits", 26 | "banner":"http://codergogoi.com/youtube/images/kesar.jpeg", 27 | "unit":1, 28 | "price":170, 29 | "available":true, 30 | "suplier":"Golden seed firming" 31 | }, 32 | { 33 | "name":"Langra Mango", 34 | "desc":"great Quality of Mango", 35 | "type":"fruits", 36 | "banner":"http://codergogoi.com/youtube/images/langra.jpeg", 37 | "unit":1, 38 | "price":280, 39 | "available":true, 40 | "suplier":"Golden seed firming" 41 | }, 42 | { 43 | "name":"Broccoli", 44 | "desc":"great Quality of Fresh Vegetable", 45 | "type":"vegetables", 46 | "banner":"http://codergogoi.com/youtube/images/broccoli.jpeg", 47 | "unit":1, 48 | "price":280, 49 | "available":true, 50 | "suplier":"Golden seed firming" 51 | }, 52 | { 53 | "name":"Cauliflower", 54 | "desc":"great Quality of Fresh Vegetable", 55 | "type":"vegetables", 56 | "banner":"http://codergogoi.com/youtube/images/cauliflower.jpeg", 57 | "unit":1, 58 | "price":280, 59 | "available":true, 60 | "suplier":"Golden seed firming" 61 | }, 62 | { 63 | "name":"Olive Oil", 64 | "desc":"great Quality of Oil", 65 | "type":"oils", 66 | "banner":"http://codergogoi.com/youtube/images/oliveoil.jpg", 67 | "unit":1, 68 | "price":400, 69 | "available":true, 70 | "suplier":"Golden seed firming" 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /products/src/services/product-service.js: -------------------------------------------------------------------------------- 1 | const { ProductRepository } = require("../database"); 2 | const { FormateData } = require("../utils"); 3 | 4 | // All Business logic will be here 5 | class ProductService { 6 | 7 | constructor(){ 8 | this.repository = new ProductRepository(); 9 | } 10 | 11 | 12 | async CreateProduct(productInputs){ 13 | 14 | const productResult = await this.repository.CreateProduct(productInputs) 15 | return FormateData(productResult); 16 | } 17 | 18 | async GetProducts(){ 19 | const products = await this.repository.Products(); 20 | 21 | let categories = {}; 22 | 23 | products.map(({ type }) => { 24 | categories[type] = type; 25 | }); 26 | 27 | return FormateData({ 28 | products, 29 | categories: Object.keys(categories) 30 | }) 31 | 32 | } 33 | 34 | async GetProductDescription(productId){ 35 | 36 | const product = await this.repository.FindById(productId); 37 | return FormateData(product) 38 | } 39 | 40 | async GetProductsByCategory(category){ 41 | 42 | const products = await this.repository.FindByCategory(category); 43 | return FormateData(products) 44 | 45 | } 46 | 47 | async GetSelectedProducts(selectedIds){ 48 | 49 | const products = await this.repository.FindSelectedProducts(selectedIds); 50 | return FormateData(products); 51 | } 52 | 53 | async GetProductPayload(userId,{ productId, qty },event){ 54 | 55 | const product = await this.repository.FindById(productId); 56 | 57 | if(product){ 58 | const payload = { 59 | event: event, 60 | data: { userId, product, qty} 61 | }; 62 | 63 | return FormateData(payload) 64 | }else{ 65 | return FormateData({error: 'No product Available'}); 66 | } 67 | 68 | } 69 | 70 | 71 | } 72 | 73 | module.exports = ProductService; 74 | -------------------------------------------------------------------------------- /products/src/services/product-service.test.js: -------------------------------------------------------------------------------- 1 | // which service it is 2 | describe("ProductService", () => { 3 | // Which function 4 | describe("CreateProduct", () => { 5 | // Which Scenario we are testing 6 | test("validate user inputs", () => {}); 7 | 8 | test("Validate response", async () => {}); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /products/src/utils/app-errors.js: -------------------------------------------------------------------------------- 1 | const STATUS_CODES = { 2 | OK: 200, 3 | BAD_REQUEST: 400, 4 | UN_AUTHORISED: 403, 5 | NOT_FOUND: 404, 6 | INTERNAL_ERROR: 500, 7 | } 8 | 9 | class AppError extends Error { 10 | constructor(name,statusCode,description, isOperational, errorStack, logingErrorResponse){ 11 | super(description); 12 | Object.setPrototypeOf(this,new.target.prototype); 13 | this.name = name; 14 | this.statusCode = statusCode; 15 | this.isOperational = isOperational 16 | this.errorStack = errorStack; 17 | this.logError = logingErrorResponse; 18 | Error.captureStackTrace(this); 19 | } 20 | } 21 | 22 | //api Specific Errors 23 | class APIError extends AppError { 24 | constructor(name, statusCode = STATUS_CODES.INTERNAL_ERROR, description ='Internal Server Error',isOperational = true,){ 25 | super(name,statusCode,description,isOperational); 26 | } 27 | } 28 | 29 | //400 30 | class BadRequestError extends AppError { 31 | constructor(description = 'Bad request',logingErrorResponse){ 32 | super('NOT FOUND', STATUS_CODES.BAD_REQUEST,description,true, false, logingErrorResponse); 33 | } 34 | } 35 | 36 | //400 37 | class ValidationError extends AppError { 38 | constructor(description = 'Validation Error', errorStack){ 39 | super('BAD REQUEST', STATUS_CODES.BAD_REQUEST,description,true, errorStack); 40 | } 41 | } 42 | 43 | 44 | module.exports = { 45 | AppError, 46 | APIError, 47 | BadRequestError, 48 | ValidationError, 49 | STATUS_CODES, 50 | } 51 | -------------------------------------------------------------------------------- /products/src/utils/error-handler.js: -------------------------------------------------------------------------------- 1 | const { createLogger, transports } = require('winston'); 2 | const { AppError } = require('./app-errors'); 3 | 4 | 5 | const LogErrors = createLogger({ 6 | transports: [ 7 | new transports.Console(), 8 | new transports.File({ filename: 'app_error.log' }) 9 | ] 10 | }); 11 | 12 | 13 | class ErrorLogger { 14 | constructor(){} 15 | async logError(err){ 16 | console.log('==================== Start Error Logger ==============='); 17 | LogErrors.log({ 18 | private: true, 19 | level: 'error', 20 | message: `${new Date()}-${JSON.stringify(err)}` 21 | }); 22 | console.log('==================== End Error Logger ==============='); 23 | // log error with Logger plugins 24 | 25 | return false; 26 | } 27 | 28 | isTrustError(error){ 29 | if(error instanceof AppError){ 30 | return error.isOperational; 31 | }else{ 32 | return false; 33 | } 34 | } 35 | } 36 | 37 | const ErrorHandler = async(err,req,res,next) => { 38 | 39 | const errorLogger = new ErrorLogger(); 40 | 41 | process.on('uncaughtException', (reason, promise) => { 42 | console.log(reason, 'UNHANDLED'); 43 | throw reason; // need to take care 44 | }) 45 | 46 | process.on('uncaughtException', (error) => { 47 | errorLogger.logError(error); 48 | if(errorLogger.isTrustError(err)){ 49 | //process exist // need restart 50 | } 51 | }) 52 | 53 | // console.log(err.description, '-------> DESCRIPTION') 54 | // console.log(err.message, '-------> MESSAGE') 55 | // console.log(err.name, '-------> NAME') 56 | if(err){ 57 | await errorLogger.logError(err); 58 | if(errorLogger.isTrustError(err)){ 59 | if(err.errorStack){ 60 | const errorDescription = err.errorStack; 61 | return res.status(err.statusCode).json({'message': errorDescription}) 62 | } 63 | return res.status(err.statusCode).json({'message': err.message }) 64 | }else{ 65 | //process exit // terriablly wrong with flow need restart 66 | } 67 | return res.status(err.statusCode).json({'message': err.message}) 68 | } 69 | next(); 70 | } 71 | 72 | module.exports = ErrorHandler; -------------------------------------------------------------------------------- /products/src/utils/index.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcryptjs"); 2 | const jwt = require("jsonwebtoken"); 3 | const axios = require("axios"); 4 | const amqplib = require("amqplib"); 5 | 6 | const { 7 | APP_SECRET, 8 | BASE_URL, 9 | EXCHANGE_NAME, 10 | MSG_QUEUE_URL, 11 | } = require("../config"); 12 | 13 | //Utility functions 14 | module.exports.GenerateSalt = async () => { 15 | return await bcrypt.genSalt(); 16 | }; 17 | 18 | module.exports.GeneratePassword = async (password, salt) => { 19 | return await bcrypt.hash(password, salt); 20 | }; 21 | 22 | module.exports.ValidatePassword = async ( 23 | enteredPassword, 24 | savedPassword, 25 | salt 26 | ) => { 27 | return (await this.GeneratePassword(enteredPassword, salt)) === savedPassword; 28 | }; 29 | 30 | module.exports.GenerateSignature = async (payload) => { 31 | try { 32 | return await jwt.sign(payload, APP_SECRET, { expiresIn: "30d" }); 33 | } catch (error) { 34 | console.log(error); 35 | return error; 36 | } 37 | }; 38 | 39 | module.exports.ValidateSignature = async (req) => { 40 | try { 41 | const signature = req.get("Authorization"); 42 | console.log(signature); 43 | const payload = await jwt.verify(signature.split(" ")[1], APP_SECRET); 44 | req.user = payload; 45 | return true; 46 | } catch (error) { 47 | console.log(error); 48 | return false; 49 | } 50 | }; 51 | 52 | module.exports.FormateData = (data) => { 53 | if (data) { 54 | return { data }; 55 | } else { 56 | throw new Error("Data Not found!"); 57 | } 58 | }; 59 | 60 | //Raise Events 61 | module.exports.PublishCustomerEvent = async (payload) => { 62 | axios.post("http://customer:8001/app-events/", { 63 | payload, 64 | }); 65 | 66 | // axios.post(`${BASE_URL}/customer/app-events/`,{ 67 | // payload 68 | // }); 69 | }; 70 | 71 | module.exports.PublishShoppingEvent = async (payload) => { 72 | // axios.post('http://gateway:8000/shopping/app-events/',{ 73 | // payload 74 | // }); 75 | 76 | axios.post(`http://shopping:8003/app-events/`, { 77 | payload, 78 | }); 79 | }; 80 | 81 | //Message Broker 82 | 83 | module.exports.CreateChannel = async () => { 84 | try { 85 | const connection = await amqplib.connect(MSG_QUEUE_URL); 86 | const channel = await connection.createChannel(); 87 | await channel.assertQueue(EXCHANGE_NAME, "direct", { durable: true }); 88 | return channel; 89 | } catch (err) { 90 | throw err; 91 | } 92 | }; 93 | 94 | module.exports.PublishMessage = (channel, service, msg) => { 95 | channel.publish(EXCHANGE_NAME, service, Buffer.from(msg)); 96 | console.log("Sent: ", msg); 97 | }; 98 | -------------------------------------------------------------------------------- /proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | RUN rm /etc/nginx/nginx.conf 4 | 5 | COPY nginx.conf /etc/nginx/nginx.conf 6 | -------------------------------------------------------------------------------- /proxy/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | 3 | events { worker_connections 1024; } 4 | 5 | http { 6 | 7 | server { 8 | 9 | listen 80; 10 | charset utf-8; 11 | 12 | location / { 13 | proxy_pass http://products:8002; 14 | proxy_http_version 1.1; 15 | proxy_set_header Upgrade $http_upgrade; 16 | proxy_set_header Connection 'upgrade'; 17 | proxy_set_header Host $host; 18 | proxy_cache_bypass $http_upgrade; 19 | } 20 | 21 | location ~ ^/shopping { 22 | rewrite ^/shopping/(.*) /$1 break; 23 | proxy_pass http://shopping:8003; 24 | proxy_http_version 1.1; 25 | proxy_set_header Upgrade $http_upgrade; 26 | proxy_set_header Connection 'upgrade'; 27 | proxy_set_header Host $host; 28 | proxy_cache_bypass $http_upgrade; 29 | } 30 | 31 | location /customer { 32 | rewrite ^/customer/(.*)$ /$1 break; 33 | proxy_pass http://customer:8001; 34 | proxy_http_version 1.1; 35 | proxy_set_header Upgrade $http_upgrade; 36 | proxy_set_header Connection 'upgrade'; 37 | proxy_set_header Host $host; 38 | proxy_cache_bypass $http_upgrade; 39 | } 40 | 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /shopping/.env.dev: -------------------------------------------------------------------------------- 1 | APP_SECRET ='jg_youtube_tutorial' 2 | 3 | APP_SECRET ='jg_youtube_tutorial' 4 | 5 | # Mongo DB 6 | MONGODB_URI='mongodb://nosql-db/msytt_shopping' 7 | 8 | MSG_QUEUE_URL='amqp://rabbitmq:5672' 9 | 10 | EXCHANGE_NAME='ONLINE_STORE' 11 | 12 | # Port 13 | PORT=8003 14 | -------------------------------------------------------------------------------- /shopping/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /app/shopping 4 | 5 | COPY package.json . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 8003 12 | 13 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /shopping/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | 5 | app.use(express.json()); 6 | 7 | app.use('/', (req,res,next) => { 8 | 9 | return res.status(200).json({"msg": "Hello from Shopping"}) 10 | }) 11 | 12 | 13 | app.listen(8003, () => { 14 | console.log('Shopping is Listening to Port 8003') 15 | }) -------------------------------------------------------------------------------- /shopping/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopping", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=prod node src/index.js", 8 | "dev": "NODE_ENV=dev nodemon src/index.js", 9 | "test": "jest" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "amqplib": "^0.8.0", 16 | "axios": "^0.21.1", 17 | "bcryptjs": "^2.4.3", 18 | "cors": "^2.8.5", 19 | "dotenv": "^8.6.0", 20 | "express": "^4.17.1", 21 | "jsonwebtoken": "^8.5.1", 22 | "mongoose": "^5.12.3", 23 | "nodemon": "^2.0.7", 24 | "uuid": "^8.3.2" 25 | }, 26 | "devDependencies": { 27 | "jest": "^29.0.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /shopping/src/api/app-events.js: -------------------------------------------------------------------------------- 1 | const ShoppingService = require("../services/shopping-service"); 2 | 3 | module.exports = (app) => { 4 | 5 | const service = new ShoppingService(); 6 | 7 | app.use('/app-events',async (req,res,next) => { 8 | 9 | const { payload } = req.body; 10 | console.log("============= Shopping ================"); 11 | 12 | console.log(payload); 13 | 14 | //handle subscribe events 15 | service.SubscribeEvents(payload); 16 | 17 | return res.status(200).json({message: 'notified!'}); 18 | 19 | }); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /shopping/src/api/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | shopping: require('./shopping'), 4 | appEvents: require('./app-events'), 5 | } 6 | -------------------------------------------------------------------------------- /shopping/src/api/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const { ValidateSignature } = require('../../utils'); 2 | 3 | module.exports = async (req,res,next) => { 4 | 5 | const isAuthorized = await ValidateSignature(req); 6 | 7 | if(isAuthorized){ 8 | return next(); 9 | } 10 | return res.status(403).json({message: 'Not Authorized'}) 11 | } -------------------------------------------------------------------------------- /shopping/src/api/shopping.js: -------------------------------------------------------------------------------- 1 | const ShoppingService = require("../services/shopping-service"); 2 | const { PublishCustomerEvent, SubscribeMessage } = require("../utils"); 3 | const UserAuth = require('./middlewares/auth'); 4 | const { CUSTOMER_SERVICE } = require('../config'); 5 | const { PublishMessage } = require('../utils') 6 | 7 | module.exports = (app, channel) => { 8 | 9 | const service = new ShoppingService(); 10 | 11 | SubscribeMessage(channel, service) 12 | 13 | app.post('/order',UserAuth, async (req,res,next) => { 14 | 15 | const { _id } = req.user; 16 | const { txnNumber } = req.body; 17 | 18 | const { data } = await service.PlaceOrder({_id, txnNumber}); 19 | 20 | const payload = await service.GetOrderPayload(_id, data, 'CREATE_ORDER') 21 | 22 | // PublishCustomerEvent(payload) 23 | PublishMessage(channel,CUSTOMER_SERVICE, JSON.stringify(payload)) 24 | 25 | res.status(200).json(data); 26 | 27 | }); 28 | 29 | app.get('/orders',UserAuth, async (req,res,next) => { 30 | 31 | const { _id } = req.user; 32 | 33 | const { data } = await service.GetOrders(_id); 34 | 35 | res.status(200).json(data); 36 | 37 | }); 38 | 39 | app.put('/cart',UserAuth, async (req,res,next) => { 40 | 41 | const { _id } = req.user; 42 | 43 | const { data } = await service.AddToCart(_id, req.body._id); 44 | 45 | res.status(200).json(data); 46 | 47 | }); 48 | 49 | app.delete('/cart/:id',UserAuth, async (req,res,next) => { 50 | 51 | const { _id } = req.user; 52 | 53 | 54 | const { data } = await service.AddToCart(_id, req.body._id); 55 | 56 | res.status(200).json(data); 57 | 58 | }); 59 | 60 | app.get('/cart', UserAuth, async (req,res,next) => { 61 | 62 | const { _id } = req.user; 63 | 64 | const { data } = await service.GetCart({ _id }); 65 | 66 | return res.status(200).json(data); 67 | }); 68 | 69 | app.get('/whoami', (req,res,next) => { 70 | return res.status(200).json({msg: '/shoping : I am Shopping Service'}) 71 | }) 72 | 73 | } 74 | -------------------------------------------------------------------------------- /shopping/src/config/index.js: -------------------------------------------------------------------------------- 1 | const dotEnv = require("dotenv"); 2 | 3 | if (process.env.NODE_ENV !== "prod") { 4 | const configFile = `./.env.${process.env.NODE_ENV}`; 5 | dotEnv.config({ path: configFile }); 6 | } else { 7 | dotEnv.config(); 8 | } 9 | 10 | module.exports = { 11 | PORT: process.env.PORT, 12 | DB_URL: process.env.MONGODB_URI, 13 | APP_SECRET: process.env.APP_SECRET, 14 | BASE_URL: process.env.BASE_URL, 15 | EXCHANGE_NAME: process.env.EXCHANGE_NAME, 16 | MSG_QUEUE_URL: process.env.MSG_QUEUE_URL, 17 | CUSTOMER_SERVICE: "customer_service", 18 | SHOPPING_SERVICE: "shopping_service", 19 | }; 20 | -------------------------------------------------------------------------------- /shopping/src/database/connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { DB_URL } = require('../config'); 3 | 4 | module.exports = async() => { 5 | 6 | try { 7 | await mongoose.connect(DB_URL, { 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | useCreateIndex: true 11 | }); 12 | console.log('Db Connected'); 13 | 14 | } catch (error) { 15 | console.log('Error ============') 16 | console.log(error); 17 | } 18 | 19 | }; 20 | 21 | 22 | -------------------------------------------------------------------------------- /shopping/src/database/index.js: -------------------------------------------------------------------------------- 1 | // database related modules 2 | module.exports = { 3 | databaseConnection: require('./connection'), 4 | ShoppingRepository: require('./repository/shopping-repository') 5 | } -------------------------------------------------------------------------------- /shopping/src/database/models/Cart.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const CartSchema = new Schema({ 6 | customerId: { type: String }, 7 | items: [ 8 | { 9 | product: { 10 | _id: { type: String, require: true}, 11 | name: { type: String }, 12 | desc: { type: String }, 13 | banner: { type: String }, 14 | type: { type: String }, 15 | unit: { type: Number }, 16 | price: { type: Number }, 17 | suplier: { type: String }, 18 | } , 19 | unit: { type: Number, require: true} 20 | } 21 | ] 22 | }); 23 | 24 | module.exports = mongoose.model('cart', CartSchema); 25 | -------------------------------------------------------------------------------- /shopping/src/database/models/Order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const OrderSchema = new Schema({ 6 | orderId: { type: String }, 7 | customerId: { type: String }, 8 | amount: { type: Number }, 9 | status: { type: String }, 10 | items: [ 11 | { 12 | product: { 13 | _id: { type: String, require: true}, 14 | name: { type: String }, 15 | desc: { type: String }, 16 | banner: { type: String }, 17 | type: { type: String }, 18 | unit: { type: Number }, 19 | price: { type: Number }, 20 | suplier: { type: String }, 21 | } , 22 | unit: { type: Number, require: true} 23 | } 24 | ] 25 | }, 26 | { 27 | toJSON: { 28 | transform(doc, ret){ 29 | delete ret.__v; 30 | } 31 | }, 32 | timestamps: true 33 | }); 34 | 35 | module.exports = mongoose.model('order', OrderSchema); 36 | -------------------------------------------------------------------------------- /shopping/src/database/models/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | OrderModel: require('./Order'), 3 | CartModel: require('./Cart') 4 | } -------------------------------------------------------------------------------- /shopping/src/database/repository/shopping-repository.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { OrderModel, CartModel } = require('../models'); 3 | const { v4: uuidv4 } = require('uuid'); 4 | 5 | //Dealing with data base operations 6 | class ShoppingRepository { 7 | 8 | async Orders(customerId){ 9 | 10 | const orders = await OrderModel.find({customerId }); 11 | 12 | return orders; 13 | 14 | } 15 | 16 | async Cart(customerId){ 17 | 18 | const cartItems = await CartModel.find({ customerId: customerId }); 19 | 20 | 21 | if(cartItems){ 22 | return cartItems; 23 | } 24 | 25 | throw new Error('Data Not found!'); 26 | } 27 | 28 | async AddCartItem(customerId,item,qty,isRemove){ 29 | 30 | // return await CartModel.deleteMany(); 31 | 32 | const cart = await CartModel.findOne({ customerId: customerId }) 33 | 34 | const { _id } = item; 35 | 36 | if(cart){ 37 | 38 | let isExist = false; 39 | 40 | let cartItems = cart.items; 41 | 42 | 43 | if(cartItems.length > 0){ 44 | 45 | cartItems.map(item => { 46 | 47 | if(item.product._id.toString() === _id.toString()){ 48 | if(isRemove){ 49 | cartItems.splice(cartItems.indexOf(item), 1); 50 | }else{ 51 | item.unit = qty; 52 | } 53 | isExist = true; 54 | } 55 | }); 56 | } 57 | 58 | if(!isExist && !isRemove){ 59 | cartItems.push({product: { ...item}, unit: qty }); 60 | } 61 | 62 | cart.items = cartItems; 63 | 64 | return await cart.save() 65 | 66 | }else{ 67 | 68 | return await CartModel.create({ 69 | customerId, 70 | items:[{product: { ...item}, unit: qty }] 71 | }) 72 | } 73 | 74 | 75 | } 76 | 77 | async CreateNewOrder(customerId, txnId){ 78 | 79 | //required to verify payment through TxnId 80 | 81 | const cart = await CartModel.findOne({ customerId: customerId }) 82 | 83 | if(cart){ 84 | 85 | let amount = 0; 86 | 87 | let cartItems = cart.items; 88 | 89 | if(cartItems.length > 0){ 90 | //process Order 91 | 92 | cartItems.map(item => { 93 | amount += parseInt(item.product.price) * parseInt(item.unit); 94 | }); 95 | 96 | const orderId = uuidv4(); 97 | 98 | const order = new OrderModel({ 99 | orderId, 100 | customerId, 101 | amount, 102 | status: 'received', 103 | items: cartItems 104 | }) 105 | 106 | cart.items = []; 107 | 108 | const orderResult = await order.save(); 109 | await cart.save(); 110 | return orderResult; 111 | 112 | 113 | } 114 | 115 | 116 | 117 | } 118 | 119 | return {} 120 | } 121 | 122 | } 123 | 124 | module.exports = ShoppingRepository; 125 | -------------------------------------------------------------------------------- /shopping/src/express-app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const path = require('path'); 4 | const { shopping, appEvents } = require('./api'); 5 | const { CreateChannel } = require('./utils') 6 | 7 | module.exports = async (app) => { 8 | 9 | app.use(express.json()); 10 | app.use(cors()); 11 | app.use(express.static(__dirname + '/public')) 12 | 13 | //api 14 | // appEvents(app); 15 | 16 | const channel = await CreateChannel() 17 | 18 | shopping(app, channel); 19 | // error handling 20 | 21 | } 22 | -------------------------------------------------------------------------------- /shopping/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { PORT } = require('./config'); 3 | const { databaseConnection } = require('./database'); 4 | const expressApp = require('./express-app'); 5 | 6 | const StartServer = async() => { 7 | 8 | const app = express(); 9 | 10 | await databaseConnection(); 11 | 12 | await expressApp(app); 13 | 14 | app.listen(PORT, () => { 15 | console.log(`listening to port ${PORT}`); 16 | }) 17 | .on('error', (err) => { 18 | console.log(err); 19 | process.exit(); 20 | }) 21 | 22 | } 23 | 24 | StartServer(); 25 | -------------------------------------------------------------------------------- /shopping/src/sampledata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"alphonso mango", 4 | "desc":"great Quality of Mango", 5 | "type":"fruits", 6 | "banner":"http://codergogoi.com/youtube/images/alphonso.jpeg", 7 | "unit":1, 8 | "price":300, 9 | "available":true, 10 | "suplier":"Golden seed firming" 11 | }, 12 | { 13 | "name":"Apples", 14 | "desc":"great Quality of Apple", 15 | "type":"fruits", 16 | "banner":"http://codergogoi.com/youtube/images/apples.jpeg", 17 | "unit":1, 18 | "price":140, 19 | "available":true, 20 | "suplier":"Golden seed firming" 21 | }, 22 | { 23 | "name":"Kesar Mango", 24 | "desc":"great Quality of Mango", 25 | "type":"fruits", 26 | "banner":"http://codergogoi.com/youtube/images/kesar.jpeg", 27 | "unit":1, 28 | "price":170, 29 | "available":true, 30 | "suplier":"Golden seed firming" 31 | }, 32 | { 33 | "name":"Langra Mango", 34 | "desc":"great Quality of Mango", 35 | "type":"fruits", 36 | "banner":"http://codergogoi.com/youtube/images/langra.jpeg", 37 | "unit":1, 38 | "price":280, 39 | "available":true, 40 | "suplier":"Golden seed firming" 41 | }, 42 | { 43 | "name":"Broccoli", 44 | "desc":"great Quality of Fresh Vegetable", 45 | "type":"vegetables", 46 | "banner":"http://codergogoi.com/youtube/images/broccoli.jpeg", 47 | "unit":1, 48 | "price":280, 49 | "available":true, 50 | "suplier":"Golden seed firming" 51 | }, 52 | { 53 | "name":"Cauliflower", 54 | "desc":"great Quality of Fresh Vegetable", 55 | "type":"vegetables", 56 | "banner":"http://codergogoi.com/youtube/images/cauliflower.jpeg", 57 | "unit":1, 58 | "price":280, 59 | "available":true, 60 | "suplier":"Golden seed firming" 61 | }, 62 | { 63 | "name":"Olive Oil", 64 | "desc":"great Quality of Oil", 65 | "type":"oils", 66 | "banner":"http://codergogoi.com/youtube/images/oliveoil.jpg", 67 | "unit":1, 68 | "price":400, 69 | "available":true, 70 | "suplier":"Golden seed firming" 71 | }, 72 | { 73 | "name":"Olive Oil", 74 | "desc":"great Quality of Oil", 75 | "type":"oils", 76 | "banner":"http://codergogoi.com/youtube/images/potatos.jpeg", 77 | "unit":1, 78 | "price":400, 79 | "available":true, 80 | "suplier":"Golden seed firming" 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /shopping/src/services/shopping-service.js: -------------------------------------------------------------------------------- 1 | const { ShoppingRepository } = require("../database"); 2 | const { FormateData } = require("../utils"); 3 | 4 | // All Business logic will be here 5 | class ShoppingService { 6 | 7 | constructor(){ 8 | this.repository = new ShoppingRepository(); 9 | } 10 | 11 | async GetCart({ _id }){ 12 | 13 | const cartItems = await this.repository.Cart(_id); 14 | return FormateData(cartItems); 15 | } 16 | 17 | 18 | async PlaceOrder(userInput){ 19 | 20 | const { _id, txnNumber } = userInput 21 | 22 | const orderResult = await this.repository.CreateNewOrder(_id, txnNumber); 23 | 24 | return FormateData(orderResult); 25 | } 26 | 27 | async GetOrders(customerId){ 28 | 29 | const orders = await this.repository.Orders(customerId); 30 | return FormateData(orders) 31 | } 32 | 33 | async GetOrderDetails({ _id,orderId }){ 34 | const orders = await this.repository.Orders(productId); 35 | return FormateData(orders) 36 | } 37 | 38 | async ManageCart(customerId, item,qty, isRemove){ 39 | 40 | const cartResult = await this.repository.AddCartItem(customerId,item,qty, isRemove); 41 | return FormateData(cartResult); 42 | } 43 | 44 | 45 | async SubscribeEvents(payload){ 46 | 47 | payload = JSON.parse(payload); 48 | const { event, data } = payload; 49 | const { userId, product, qty } = data; 50 | 51 | switch(event){ 52 | case 'ADD_TO_CART': 53 | this.ManageCart(userId,product, qty, false); 54 | break; 55 | case 'REMOVE_FROM_CART': 56 | this.ManageCart(userId,product, qty, true); 57 | break; 58 | default: 59 | break; 60 | } 61 | 62 | } 63 | 64 | 65 | async GetOrderPayload(userId,order,event){ 66 | 67 | if(order){ 68 | const payload = { 69 | event: event, 70 | data: { userId, order } 71 | }; 72 | 73 | return payload 74 | }else{ 75 | return FormateData({error: 'No Order Available'}); 76 | } 77 | 78 | } 79 | 80 | 81 | 82 | } 83 | 84 | module.exports = ShoppingService; 85 | -------------------------------------------------------------------------------- /shopping/src/services/shopping-service.test.js: -------------------------------------------------------------------------------- 1 | // which service it is 2 | describe("ShoppingService", () => { 3 | // Which function 4 | describe("PlaceOrder", () => { 5 | // Which Scenario we are testing 6 | test("validate user inputs", () => {}); 7 | 8 | test("Validate response", async () => {}); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /shopping/src/utils/app-errors.js: -------------------------------------------------------------------------------- 1 | const STATUS_CODES = { 2 | OK: 200, 3 | BAD_REQUEST: 400, 4 | UN_AUTHORISED: 403, 5 | NOT_FOUND: 404, 6 | INTERNAL_ERROR: 500, 7 | } 8 | 9 | class AppError extends Error { 10 | constructor(name,statusCode,description, isOperational, errorStack, logingErrorResponse){ 11 | super(description); 12 | Object.setPrototypeOf(this,new.target.prototype); 13 | this.name = name; 14 | this.statusCode = statusCode; 15 | this.isOperational = isOperational 16 | this.errorStack = errorStack; 17 | this.logError = logingErrorResponse; 18 | Error.captureStackTrace(this); 19 | } 20 | } 21 | 22 | //api Specific Errors 23 | class APIError extends AppError { 24 | constructor(name, statusCode = STATUS_CODES.INTERNAL_ERROR, description ='Internal Server Error',isOperational = true,){ 25 | super(name,statusCode,description,isOperational); 26 | } 27 | } 28 | 29 | //400 30 | class BadRequestError extends AppError { 31 | constructor(description = 'Bad request',logingErrorResponse){ 32 | super('NOT FOUND', STATUS_CODES.BAD_REQUEST,description,true, false, logingErrorResponse); 33 | } 34 | } 35 | 36 | //400 37 | class ValidationError extends AppError { 38 | constructor(description = 'Validation Error', errorStack){ 39 | super('BAD REQUEST', STATUS_CODES.BAD_REQUEST,description,true, errorStack); 40 | } 41 | } 42 | 43 | 44 | module.exports = { 45 | AppError, 46 | APIError, 47 | BadRequestError, 48 | ValidationError, 49 | STATUS_CODES, 50 | } 51 | -------------------------------------------------------------------------------- /shopping/src/utils/error-handler.js: -------------------------------------------------------------------------------- 1 | const { createLogger, transports } = require('winston'); 2 | const { AppError } = require('./app-errors'); 3 | 4 | 5 | const LogErrors = createLogger({ 6 | transports: [ 7 | new transports.Console(), 8 | new transports.File({ filename: 'app_error.log' }) 9 | ] 10 | }); 11 | 12 | 13 | class ErrorLogger { 14 | constructor(){} 15 | async logError(err){ 16 | console.log('==================== Start Error Logger ==============='); 17 | LogErrors.log({ 18 | private: true, 19 | level: 'error', 20 | message: `${new Date()}-${JSON.stringify(err)}` 21 | }); 22 | console.log('==================== End Error Logger ==============='); 23 | // log error with Logger plugins 24 | 25 | return false; 26 | } 27 | 28 | isTrustError(error){ 29 | if(error instanceof AppError){ 30 | return error.isOperational; 31 | }else{ 32 | return false; 33 | } 34 | } 35 | } 36 | 37 | const ErrorHandler = async(err,req,res,next) => { 38 | 39 | const errorLogger = new ErrorLogger(); 40 | 41 | process.on('uncaughtException', (reason, promise) => { 42 | console.log(reason, 'UNHANDLED'); 43 | throw reason; // need to take care 44 | }) 45 | 46 | process.on('uncaughtException', (error) => { 47 | errorLogger.logError(error); 48 | if(errorLogger.isTrustError(err)){ 49 | //process exist // need restart 50 | } 51 | }) 52 | 53 | // console.log(err.description, '-------> DESCRIPTION') 54 | // console.log(err.message, '-------> MESSAGE') 55 | // console.log(err.name, '-------> NAME') 56 | if(err){ 57 | await errorLogger.logError(err); 58 | if(errorLogger.isTrustError(err)){ 59 | if(err.errorStack){ 60 | const errorDescription = err.errorStack; 61 | return res.status(err.statusCode).json({'message': errorDescription}) 62 | } 63 | return res.status(err.statusCode).json({'message': err.message }) 64 | }else{ 65 | //process exit // terriablly wrong with flow need restart 66 | } 67 | return res.status(err.statusCode).json({'message': err.message}) 68 | } 69 | next(); 70 | } 71 | 72 | module.exports = ErrorHandler; -------------------------------------------------------------------------------- /shopping/src/utils/index.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcryptjs"); 2 | const jwt = require("jsonwebtoken"); 3 | const amqplib = require("amqplib"); 4 | 5 | const { 6 | APP_SECRET, 7 | EXCHANGE_NAME, 8 | SHOPPING_SERVICE, 9 | MSG_QUEUE_URL, 10 | } = require("../config"); 11 | 12 | //Utility functions 13 | module.exports.GenerateSalt = async () => { 14 | return await bcrypt.genSalt(); 15 | }; 16 | 17 | module.exports.GeneratePassword = async (password, salt) => { 18 | return await bcrypt.hash(password, salt); 19 | }; 20 | 21 | module.exports.ValidatePassword = async ( 22 | enteredPassword, 23 | savedPassword, 24 | salt 25 | ) => { 26 | return (await this.GeneratePassword(enteredPassword, salt)) === savedPassword; 27 | }; 28 | 29 | module.exports.GenerateSignature = async (payload) => { 30 | try { 31 | return await jwt.sign(payload, APP_SECRET, { expiresIn: "30d" }); 32 | } catch (error) { 33 | console.log(error); 34 | return error; 35 | } 36 | }; 37 | 38 | module.exports.ValidateSignature = async (req) => { 39 | try { 40 | const signature = req.get("Authorization"); 41 | console.log(signature); 42 | const payload = await jwt.verify(signature.split(" ")[1], APP_SECRET); 43 | req.user = payload; 44 | return true; 45 | } catch (error) { 46 | console.log(error); 47 | return false; 48 | } 49 | }; 50 | 51 | module.exports.FormateData = (data) => { 52 | if (data) { 53 | return { data }; 54 | } else { 55 | throw new Error("Data Not found!"); 56 | } 57 | }; 58 | 59 | //Message Broker 60 | 61 | module.exports.CreateChannel = async () => { 62 | try { 63 | const connection = await amqplib.connect(MSG_QUEUE_URL); 64 | const channel = await connection.createChannel(); 65 | await channel.assertQueue(EXCHANGE_NAME, "direct", { durable: true }); 66 | return channel; 67 | } catch (err) { 68 | throw err; 69 | } 70 | }; 71 | 72 | module.exports.PublishMessage = (channel, service, msg) => { 73 | channel.publish(EXCHANGE_NAME, service, Buffer.from(msg)); 74 | console.log("Sent: ", msg); 75 | }; 76 | 77 | module.exports.SubscribeMessage = async (channel, service) => { 78 | await channel.assertExchange(EXCHANGE_NAME, "direct", { durable: true }); 79 | const q = await channel.assertQueue("", { exclusive: true }); 80 | console.log(` Waiting for messages in queue: ${q.queue}`); 81 | 82 | channel.bindQueue(q.queue, EXCHANGE_NAME, SHOPPING_SERVICE); 83 | 84 | channel.consume( 85 | q.queue, 86 | (msg) => { 87 | if (msg.content) { 88 | console.log("the message is:", msg.content.toString()); 89 | service.SubscribeEvents(msg.content.toString()); 90 | } 91 | console.log("[X] received"); 92 | }, 93 | { 94 | noAck: true, 95 | } 96 | ); 97 | }; 98 | --------------------------------------------------------------------------------