├── .gitignore ├── LICENSE ├── README.md ├── jest-preload-env.js ├── libs └── dynamodb-lib.js ├── package.json ├── resources ├── booking-db.yml └── listing-db.yml ├── seed-data ├── insertData.js └── listings.json ├── serverless.yml ├── src ├── graphql.js ├── resolvers │ ├── index.js │ ├── mutation.js │ └── query.js └── schema.js ├── tests ├── getAllListings.test.js ├── handler.test.js └── makeABooking.test.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # vim 40 | .*.sw* 41 | Session.vim 42 | 43 | # Serverless 44 | .webpack 45 | .serverless 46 | 47 | # env 48 | env.yml 49 | .env 50 | 51 | # Jetbrains IDEs 52 | .idea 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Pimp My Book 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lunar Tour API 🌟 2 | 3 | Welcome to the Lunar Tour API that is part of [Fullstack Serverless GraphQL](https://fullstack-serverless-graphql.netlify.com/), which teaches you how to build Serverless GraphQL APIs. This API allows users to make bookings to lunar destinations, as well as view lunar holiday activies. 4 | 5 | The stack for this API is as follows: 6 | 7 | 🌟 [AWS Lambda](https://aws.amazon.com/lambda/) 8 | 9 | 🌟 [DynamoDB](https://aws.amazon.com/dynamodb/) 10 | 11 | 🌟 [Serverless Framework](https://www.serverless.com/) 12 | 13 | 🌟 [Apollo Lambda Server](https://www.apollographql.com/docs/apollo-server/) 14 | 15 | What you need to use this Repo: 16 | 17 | 🧁 An AWS Account 18 | 19 | 🧁 Serverless Framework 20 | 21 | 🧁 NodeJS v12.x or above 22 | 23 | ## Installation steps 24 | 25 | First clone the repo: 26 | 27 | ```bash 28 | git clone https://github.com/Fullstack-Serverless-GraphQL/lunar-tour-api 29 | ``` 30 | 31 | Then cd into the directory of the project: 32 | 33 | ```bash 34 | cd lunar-tour-api 35 | ``` 36 | 37 | Once you're in then install the packages using your fav package manager: 38 | 39 | ```bash 40 | yarn install 41 | ``` 42 | 43 | Now you can open playground on `localhost://4000` : 44 | 45 | ```bash 46 | sls offline 47 | ``` 48 | 49 | You should be able to run queries and mutations against the API locally now. 50 | 51 | This project was scaffolded using the [Serverless NodeJS GraphQL Starter](https://github.com/pimp-my-book/serverless-graphql-nodejs-starter) 52 | 53 | ## Links to related projects 54 | 55 | 🦚 [Lunar Tour (Vue)](https://github.com/Fullstack-Serverless-GraphQL/lunar-tour-frontend) 56 | 57 | 🦚 [Lunar Tour (React)](https://github.com/Fullstack-Serverless-GraphQL/lunar-tour-react) 58 | 59 | 🦚 [Fullstack Serverless GraphQL docs](https://github.com/Fullstack-Serverless-GraphQL/fullstack-serverless-graphql-docs) 60 | -------------------------------------------------------------------------------- /jest-preload-env.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config({ 2 | path: "./.env", 3 | }); 4 | 5 | if (process.env.NODE_ENV !== "test") { 6 | throw Error("Non-test environment"); 7 | } 8 | -------------------------------------------------------------------------------- /libs/dynamodb-lib.js: -------------------------------------------------------------------------------- 1 | import AWS from "aws-sdk"; 2 | 3 | const client = new AWS.DynamoDB.DocumentClient({ region: "us-east-1" }); 4 | 5 | export default { 6 | get: (params) => client.get(params).promise(), 7 | put: (params) => client.put(params).promise(), 8 | query: (params) => client.query(params).promise(), 9 | update: (params) => client.update(params).promise(), 10 | delete: (params) => client.delete(params).promise(), 11 | scan: (params) => client.scan(params).promise(), 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lunar-tour-api", 3 | "version": "1.1.0", 4 | "description": "A GraphQL Node.js starter for the Serverless Framework with async/await and unit test support", 5 | "main": "handler.js", 6 | "scripts": { 7 | "offline": "sls offline", 8 | "deploy": "sls deploy", 9 | "test": "serverless-bundle test" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/pimp-my-book/serverless-graphql-nodejs-starter" 16 | }, 17 | "jest": { 18 | "setupFilesAfterEnv": [ 19 | "./jest-preload-env.js" 20 | ] 21 | }, 22 | "devDependencies": { 23 | "serverless-bundle": "^1.2.5", 24 | "serverless-dotenv-plugin": "^2.1.1", 25 | "serverless-offline": "^5.3.3" 26 | }, 27 | "dependencies": { 28 | "apollo-server-lambda": "^2.14.3", 29 | "aws-sdk": "2.694.0", 30 | "dotenv": "^8.2.0", 31 | "graphql": "^14.3.0", 32 | "stripe": "^8.63.0", 33 | "uuid": "^8.1.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/booking-db.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | BookingsDB: 3 | Type: AWS::DynamoDB::Table 4 | Properties: 5 | TableName: ${self:custom.BookingsDB} 6 | AttributeDefinitions: 7 | - AttributeName: bookingId 8 | AttributeType: S 9 | - AttributeName: listingId 10 | AttributeType: S 11 | 12 | KeySchema: 13 | - AttributeName: bookingId 14 | KeyType: HASH 15 | - AttributeName: listingId 16 | KeyType: RANGE 17 | # Set the capacity based on the stage 18 | ProvisionedThroughput: 19 | ReadCapacityUnits: ${self:custom.tableThroughput} 20 | WriteCapacityUnits: ${self:custom.tableThroughput} 21 | -------------------------------------------------------------------------------- /resources/listing-db.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | ListingsDB: 3 | Type: AWS::DynamoDB::Table 4 | Properties: 5 | TableName: ${self:custom.ListingsDB} 6 | AttributeDefinitions: 7 | - AttributeName: listingId 8 | AttributeType: S 9 | - AttributeName: listingName 10 | AttributeType: S 11 | 12 | KeySchema: 13 | - AttributeName: listingId 14 | KeyType: HASH 15 | - AttributeName: listingName 16 | KeyType: RANGE 17 | # Set the capacity based on the stage 18 | ProvisionedThroughput: 19 | ReadCapacityUnits: ${self:custom.tableThroughput} 20 | WriteCapacityUnits: ${self:custom.tableThroughput} 21 | -------------------------------------------------------------------------------- /seed-data/insertData.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | this function will allow us to add all the listings into Dynamo dynamically 4 | */ 5 | 6 | //set up the aws-sdk 7 | const AWS = require("aws-sdk"); 8 | AWS.config.update({ region: "us-east-1" }); 9 | const docClient = new AWS.DynamoDB.DocumentClient(); 10 | 11 | //import the listings json 12 | const listings = require("./listings.json"); 13 | console.log("Listings.Init", listings); 14 | 15 | //lets inseert them into the table 16 | //loop over the listings 17 | listings.map((l) => { 18 | //create params object 19 | listingParams = { 20 | TableName: "dev-lunar-listings", 21 | Item: { 22 | coverPhoto: l.coverPhoto, 23 | guide: { 24 | avatar: l.guide.avatar, 25 | bio: l.guide.bio, 26 | name: l.guide.name, 27 | }, 28 | listingActivities: l.listingActivities, 29 | listingDescription: l.listingDescription, 30 | listingId: l.listingId, 31 | listingLocation: l.listingLocation, 32 | listingName: l.listingName, 33 | listingType: l.listingType, 34 | numberOfDays: l.numberOfDays, 35 | price: l.price, 36 | rating: l.rating, 37 | specialAmount: l.specialAmount, 38 | specialType: l.specialType, 39 | }, 40 | }; 41 | 42 | //put the data into the table 43 | 44 | docClient.put(listingParams, function (err, data) { 45 | if (err) { 46 | console.error( 47 | "Unable to add listing", 48 | user.name, 49 | ". Error JSON:", 50 | JSON.stringify(err, null, 2) 51 | ); 52 | } else { 53 | console.log("PutItem succeeded:"); 54 | } 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /seed-data/listings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "coverPhoto": "https://live.staticflickr.com/5687/21672342752_a0ca8434da_b.jpg", 4 | "guide": { 5 | "avatar": "https://randomuser.me/api/portraits/women/71.jpg", 6 | "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Temporibus unde veniam harum magnam molestias dignissimos omnis architecto, quod, obcaecati dolorum debitis dolore porro qui, iusto quo accusantium voluptates pariatur illo", 7 | "name": "Paige Van Der Roen" 8 | }, 9 | "listingActivities": [ 10 | "Deep space food production", 11 | "Soil studies", 12 | "Botany expirements" 13 | ], 14 | "listingDescription": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia corporis earum, rerum doloremque dolorum modi voluptates, eligendi? Minus cupiditate placeat, eveniet ad beatae animi illum nihil officia nulla, impedit sapiente.", 15 | "listingId": "14cd7707-e687-44e2-b982-4263d7599834", 16 | "listingLocation": "Stöfler", 17 | "listingName": "Lunar Food Farm", 18 | "listingType": ["Lab", "Hands on", "Come hungry"], 19 | "numberOfDays": 7, 20 | "price": "220.00", 21 | "rating": 2, 22 | "specialAmount": 0, 23 | "specialType": "none" 24 | }, 25 | { 26 | "coverPhoto": "https://www.nasa.gov/sites/default/files/thumbnails/image/as15-88-11901orig.jpg", 27 | "guide": { 28 | "avatar": "https://images-na.ssl-images-amazon.com/images/M/MV5BMTgxMTc1MTYzM15BMl5BanBnXkFtZTgwNzI5NjMwOTE@._V1_UY256_CR16,0,172,256_AL_.jpg", 29 | "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Temporibus unde veniam harum magnam molestias dignissimos omnis architecto, quod, obcaecati dolorum debitis dolore porro qui, iusto quo accusantium voluptates pariatur illo", 30 | "": "Arina Mulke" 31 | }, 32 | "listingActivities": ["2hr Lunar Rides"], 33 | "listingDescription": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia corporis earum, rerum doloremque dolorum modi voluptates, eligendi? Minus cupiditate placeat, eveniet ad beatae animi illum nihil officia nulla, impedit sapiente.", 34 | "listingId": "2dd77a8b-ff23-43f7-8694-e05807cf359f", 35 | "listingLocation": "Picard", 36 | "listingName": "Lunar Rovering", 37 | "listingType": ["Hotel Avaialble", "Oxygen Avaialble", "8G Internet"], 38 | "numberOfDays": 5, 39 | "price": "250.00", 40 | "rating": 4, 41 | "specialAmount": 50, 42 | "specialType": "Lighting Special" 43 | }, 44 | { 45 | "coverPhoto": "https://live.staticflickr.com/762/21472225870_b3d006b973_b.jpg", 46 | "guide": { 47 | "avatar": "https://pbs.twimg.com/profile_images/883458234685587456/KtCFjlD4.jpg", 48 | "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Temporibus unde veniam harum magnam molestias dignissimos omnis architecto, quod, obcaecati dolorum debitis dolore porro qui, iusto quo accusantium voluptates pariatur illo", 49 | "name": "Lovemore Buthi" 50 | }, 51 | "listingActivities": ["Telescope viewing", "Tech tour"], 52 | "listingDescription": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia corporis earum, rerum doloremque dolorum modi voluptates, eligendi? Minus cupiditate placeat, eveniet ad beatae animi illum nihil officia nulla, impedit sapiente.", 53 | "listingId": "2ee2a78e-c707-456d-9b9a-7c9614ce10a2", 54 | "listingLocation": "Pitatus", 55 | "listingName": "Lunar Observatory", 56 | "listingType": ["Moderately Activate", "Interactive sessions"], 57 | "numberOfDays": 1, 58 | "price": "355.00", 59 | "rating": 5, 60 | "specialAmount": 0, 61 | "specialType": "None" 62 | }, 63 | { 64 | "coverPhoto": "https://live.staticflickr.com/762/21472225870_b3d006b973_b.jpg", 65 | "guide": { 66 | "avatar": "https://pbs.twimg.com/profile_images/883458234685587456/KtCFjlD4.jpg", 67 | "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Temporibus unde veniam harum magnam molestias dignissimos omnis architecto, quod, obcaecati dolorum debitis dolore porro qui, iusto quo accusantium voluptates pariatur illo", 68 | "name": "Lovemore Buthi" 69 | }, 70 | "listingActivities": ["Telescope viewing", "Tech tour"], 71 | "listingDescription": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia corporis earum, rerum doloremque dolorum modi voluptates, eligendi? Minus cupiditate placeat, eveniet ad beatae animi illum nihil officia nulla, impedit sapiente.", 72 | "listingId": "2ee2a78e-c707-456d-9b9a-7c9614ce10a2", 73 | "listingLocation": "Pitatus", 74 | "listingName": "Lunar Observatory", 75 | "listingType": ["Moderately Activate", "Interactive sessions"], 76 | "numberOfDays": 1, 77 | "price": "355.00", 78 | "rating": 5, 79 | "specialAmount": 0, 80 | "specialType": "None" 81 | }, 82 | { 83 | "coverPhoto": "https://live.staticflickr.com/5650/21639917782_05f57bce5c_k.jpg", 84 | "guide": { 85 | "avatar": "https://randomuser.me/api/portraits/men/78.jpg", 86 | "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Temporibus unde veniam harum magnam molestias dignissimos omnis architecto, quod, obcaecati dolorum debitis dolore porro qui, iusto quo accusantium voluptates pariatur illo", 87 | "name": "Mando Tano" 88 | }, 89 | "listingActivities": [ 90 | "Propellent testing", 91 | "Engine manufacturing", 92 | "Engine CAD modeling", 93 | "Propellent manufacturing" 94 | ], 95 | "listingDescription": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia corporis earum, rerum doloremque dolorum modi voluptates, eligendi? Minus cupiditate placeat, eveniet ad beatae animi illum nihil officia nulla, impedit sapiente.", 96 | "listingId": "550e123d-710a-4d6d-8ee6-4ad3e4c7b087", 97 | "listingLocation": "Aristarchus", 98 | "listingName": "Propellent Plant", 99 | "listingType": [ 100 | "Hazaradous", 101 | "Instructor reliant", 102 | "Hands on", 103 | "Mind boggling" 104 | ], 105 | "numberOfDays": 3, 106 | "price": "230.00", 107 | "rating": 3, 108 | "specialAmount": 0, 109 | "specialType": "none" 110 | }, 111 | { 112 | "coverPhoto": "https://live.staticflickr.com/782/21060916274_a57e1bf901_b.jpg", 113 | "guide": { 114 | "avatar": "https://pbs.twimg.com/profile_images/642171524569427968/z2S0ttIf.jpg", 115 | "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Temporibus unde veniam harum magnam molestias dignissimos omnis architecto, quod, obcaecati dolorum debitis dolore porro qui, iusto quo accusantium voluptates pariatur illo", 116 | "name": "Martin Brundle" 117 | }, 118 | "listingActivities": [ 119 | "Hubble maintainace", 120 | "Earth satilite control", 121 | "Lightspeed research" 122 | ], 123 | "listingDescription": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia corporis earum, rerum doloremque dolorum modi voluptates, eligendi? Minus cupiditate placeat, eveniet ad beatae animi illum nihil officia nulla, impedit sapiente.", 124 | "listingId": "6a33bba5-d955-4e38-aa7e-21f1c0734d49", 125 | "listingLocation": "Copernicus", 126 | "listingName": "Satelite/Probe Facility", 127 | "listingType": ["Lab", "Hands on", "Tech heavy"], 128 | "numberOfDays": 7, 129 | "price": "100.00", 130 | "rating": 4, 131 | "specialAmount": 0, 132 | "specialType": "none" 133 | }, 134 | { 135 | "coverPhoto": "https://live.staticflickr.com/711/21654854445_9f9f3bd6e1_b.jpg", 136 | "guide": { 137 | "avatar": "https://pbs.twimg.com/profile_images/1051157346796400646/t33XAm04.jpg", 138 | "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Temporibus unde veniam harum magnam molestias dignissimos omnis architecto, quod, obcaecati dolorum debitis dolore porro qui, iusto quo accusantium voluptates pariatur illo", 139 | "name": "Kevin Parker" 140 | }, 141 | "listingActivities": ["2hr hike", "Crater walks", "Camping"], 142 | "listingDescription": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia corporis earum, rerum doloremque dolorum modi voluptates, eligendi? Minus cupiditate placeat, eveniet ad beatae animi illum nihil officia nulla, impedit sapiente.", 143 | "listingId": "6fb8fcbd-7ed3-45b6-b2ba-5bb99d7024e2", 144 | "listingLocation": "Plinius", 145 | "listingName": "Lunar Crater Hiking", 146 | "listingType": ["High risk", "Long exposure to Solar radiation"], 147 | "numberOfDays": 5, 148 | "price": "500.00", 149 | "rating": 5, 150 | "specialAmount": 0, 151 | "specialType": "High Demand" 152 | }, 153 | { 154 | "coverPhoto": "https://live.staticflickr.com/672/21281789484_0d7a257ec0_b.jpg", 155 | "guide": { 156 | "avatar": "https://randomuser.me/api/portraits/men/80.jpg", 157 | "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Temporibus unde veniam harum magnam molestias dignissimos omnis architecto, quod, obcaecati dolorum debitis dolore porro qui, iusto quo accusantium voluptates pariatur illo", 158 | "name": "Nikola Tesla" 159 | }, 160 | "listingActivities": ["Weapons demo", "Weapons materials creation"], 161 | "listingDescription": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia corporis earum, rerum doloremque dolorum modi voluptates, eligendi? Minus cupiditate placeat, eveniet ad beatae animi illum nihil officia nulla, impedit sapiente.", 162 | "listingId": "a114dded-ddef-4052-a106-bb18b94e6b51", 163 | "listingLocation": "Piccolomini", 164 | "listingName": "Advanced Weapons Testing", 165 | "listingType": [ 166 | "Hotel Avaialble", 167 | "Oxygen Avaialble", 168 | "Outside excursions" 169 | ], 170 | "numberOfDays": 2, 171 | "price": "195.00", 172 | "rating": 2, 173 | "specialAmount": 0, 174 | "specialType": "None" 175 | }, 176 | { 177 | "coverPhoto": "https://live.staticflickr.com/5664/21061385534_ab38aec9e0_b.jpg", 178 | "guide": { 179 | "avatar": "https://pbs.twimg.com/profile_images/834493671785525249/XdLjsJX_.jpg", 180 | "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Temporibus unde veniam harum magnam molestias dignissimos omnis architecto, quod, obcaecati dolorum debitis dolore porro qui, iusto quo accusantium voluptates pariatur illo", 181 | "name": "Antaya Valire" 182 | }, 183 | "listingActivities": [ 184 | "Rock collecting", 185 | "Crater absailing", 186 | "Geology decomposition" 187 | ], 188 | "listingDescription": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia corporis earum, rerum doloremque dolorum modi voluptates, eligendi? Minus cupiditate placeat, eveniet ad beatae animi illum nihil officia nulla, impedit sapiente.", 189 | "listingId": "c01682a0-aabf-40f2-b564-20364aa966ed", 190 | "listingLocation": "Metius", 191 | "listingName": "Geology Center", 192 | "listingType": ["Hands on", "Field work", "Dangerous"], 193 | "numberOfDays": 7, 194 | "price": "156.00", 195 | "rating": 3, 196 | "specialAmount": 0, 197 | "specialType": "none" 198 | }, 199 | { 200 | "coverPhoto": "https://live.staticflickr.com/749/21657696456_bbc6974617_b.jpg", 201 | "guide": { 202 | "avatar": "https://faces.design/faces/m/m10.png", 203 | "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Temporibus unde veniam harum magnam molestias dignissimos omnis architecto, quod, obcaecati dolorum debitis dolore porro qui, iusto quo accusantium voluptates pariatur illo", 204 | "name": "Dimpho Mgodpo" 205 | }, 206 | "listingActivities": [ 207 | "Solar system data viewing", 208 | "Parker probe access", 209 | "Deep space networking tuts" 210 | ], 211 | "listingDescription": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia corporis earum, rerum doloremque dolorum modi voluptates, eligendi? Minus cupiditate placeat, eveniet ad beatae animi illum nihil officia nulla, impedit sapiente.", 212 | "listingId": "c214b19f-720b-48c8-8855-2c13dd613ed5", 213 | "listingLocation": "Rheita", 214 | "listingName": "Deep Space Network Facility", 215 | "listingType": [ 216 | "Lab", 217 | "Oxygen Avaialble", 218 | "Highly interlectuall", 219 | "Drug free" 220 | ], 221 | "numberOfDays": 1, 222 | "price": "100.00", 223 | "rating": 2, 224 | "specialAmount": 0, 225 | "specialType": "none" 226 | } 227 | ] 228 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | # NOTE: update this with your service name 2 | service: lunar-tour-api 3 | 4 | # Use the serverless-webpack plugin to transpile ES6 5 | plugins: 6 | - serverless-bundle 7 | - serverless-offline 8 | - serverless-dotenv-plugin 9 | 10 | # serverless-webpack configuration 11 | # Enable auto-packing of external modules 12 | custom: 13 | stage: ${opt:stage, self:provider.stage} 14 | region: ${opt:region, self:provider.region} 15 | ListingsDB: ${self:custom.stage}-lunar-listings 16 | BookingsDB: ${self:custom.stage}-lunar-bookings 17 | tableThroughputs: 18 | prod: 1 19 | default: 1 20 | tableThroughput: ${self:custom.tableThroughputs.${self:custom.stage}, self:custom.tableThroughputs.default} 21 | serverless-offline: 22 | port: 4000 23 | bundle: 24 | linting: false 25 | 26 | provider: 27 | name: aws 28 | runtime: nodejs12.x 29 | stage: dev 30 | region: us-east-1 31 | environment: 32 | BookingsDB: ${self:custom.BookingsDB} 33 | ListingsDB: ${self:custom.ListingsDB} 34 | stripeSecretKey: ${env:STRIPE_SECRET_KEY} 35 | 36 | iamRoleStatements: 37 | - Effect: Allow 38 | Action: 39 | - dynamodb:DescribeTable 40 | - dynamodb:Query 41 | - dynamodb:Scan 42 | - dynamodb:GetItem 43 | - dynamodb:PutItem 44 | - dynamodb:UpdateItem 45 | - dynamodb:DeleteItem 46 | - dynamodb:GetRecords 47 | - dynamodb:GetShardIterator 48 | - dynamodb:DescribeStream 49 | - dynamodb:ListStream 50 | 51 | Resource: 52 | - "Fn::GetAtt": [ListingsDB, Arn] 53 | - "Fn::GetAtt": [BookingsDB, Arn] 54 | 55 | functions: 56 | graphql: 57 | handler: src/graphql.graphqlHandler 58 | environment: 59 | SLS_DEBUG: true 60 | events: 61 | - http: 62 | path: graphql 63 | method: post 64 | cors: true 65 | integration: lambda-proxy 66 | 67 | - http: 68 | path: graphql 69 | method: get 70 | cors: true 71 | integration: lambda-proxy 72 | 73 | resources: 74 | - ${file(resources/listing-db.yml)} 75 | - ${file(resources/booking-db.yml)} 76 | -------------------------------------------------------------------------------- /src/graphql.js: -------------------------------------------------------------------------------- 1 | import { ApolloServer, gql } from "apollo-server-lambda"; 2 | import { schema } from "./schema"; 3 | import { resolvers } from "./resolvers"; 4 | 5 | const server = new ApolloServer({ 6 | typeDefs: schema, 7 | resolvers: resolvers, 8 | formatError: (error) => { 9 | return error; 10 | }, 11 | formatResponse: (response) => { 12 | return response; 13 | }, 14 | context: ({ event, context }) => ({ 15 | headers: event.headers, 16 | functionName: context.functionName, 17 | event, 18 | context, 19 | }), 20 | tracing: true, 21 | playground: true, 22 | introspection: true, 23 | }); 24 | 25 | exports.graphqlHandler = (event, context, callback) => { 26 | const handler = server.createHandler({ 27 | cors: { 28 | origin: "*", 29 | credentials: true, 30 | methods: ["POST", "GET"], 31 | allowedHeaders: ["Content-Type", "Origin", "Accept"], 32 | }, 33 | }); 34 | return handler(event, context, callback); 35 | }; 36 | -------------------------------------------------------------------------------- /src/resolvers/index.js: -------------------------------------------------------------------------------- 1 | import { hello, getAllListings, getAListing } from "./query"; 2 | import { makeABooking } from "./mutation"; 3 | 4 | export const resolvers = { 5 | Query: { 6 | hello: (root, args, context) => hello(args, context), 7 | getAllListings: (root, args, context) => getAllListings(args, context), 8 | getAListing: (root, args, context) => getAListing(args, context), 9 | }, 10 | Mutation: { 11 | makeABooking: (root, args, context) => makeABooking(args, context), 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/resolvers/mutation.js: -------------------------------------------------------------------------------- 1 | import { v1 as uuidv1 } from "uuid"; 2 | import stripePackage from "stripe"; 3 | import dynamodb from "../../libs/dynamodb-lib"; 4 | 5 | export const makeABooking = async (args, context) => { 6 | //Get the listing that the user selected 7 | //from the client 8 | const getPrices = async () => { 9 | const params = { 10 | TableName: process.env.ListingsDB || "dev-lunar-listings", 11 | KeyConditionExpression: "listingId = :listingId", 12 | ExpressionAttributeValues: { 13 | ":listingId": args.listingId, 14 | }, 15 | }; 16 | try { 17 | const listings = await dynamodb.query(params); 18 | return listings; 19 | } catch (e) { 20 | return e; 21 | } 22 | }; 23 | 24 | //set the listing to a variables so we can resuse it 25 | const listingObject = await getPrices(); 26 | 27 | //caLCULATE THE amount to be charged to the 28 | //customers card 29 | 30 | const bookingCharge = 31 | parseInt(listingObject.Items[0].price) * args.customers.length; 32 | //get the name of the listing 33 | 34 | const listingName = listingObject.listingName; 35 | //create an instance of the stripe lib 36 | 37 | const stripe = stripePackage(process.env.stripeSecretKey); 38 | 39 | //charge the users card 40 | 41 | const stripeResult = await stripe.charges.create({ 42 | source: "tok_visa", 43 | amount: bookingCharge, 44 | description: `Charge for booking of listing ${args.listingId}`, 45 | currency: "usd", 46 | }); 47 | 48 | //create the booking in the table 49 | const params = { 50 | TableName: process.env.BookingsDB || "dev-lunar-bookings", 51 | Item: { 52 | bookingId: uuidv1(), 53 | listingId: args.listingId, 54 | bookingDate: args.bookingDate, 55 | size: args.customers.length > 0 ? args.customers.length : 0, 56 | bookingTotal: bookingCharge, 57 | customerEmail: args.customerEmail, 58 | customers: args.customers, 59 | createdTimestamp: Date.now(), 60 | chargeReciept: stripeResult.receipt_url, 61 | paymentDetails: stripeResult.payment_method_details, 62 | }, 63 | }; 64 | try { 65 | //insert the booking into the table 66 | await dynamodb.put(params); 67 | 68 | return { 69 | bookingId: params.Item.bookingId, 70 | listingId: params.Item.listingId, 71 | bookingDate: params.Item.bookingDate, 72 | size: params.Item.size, 73 | bookingTotal: params.Item.bookingTotal, 74 | customerEmail: params.Item.customerEmail, 75 | customers: params.Item.customers.map((c) => ({ 76 | name: c.name, 77 | surname: c.surname, 78 | country: c.country, 79 | passportNumber: c.passportNumber, 80 | physioScore: c.physioScore, 81 | })), 82 | chargeReciept: params.Item.chargeReciept, 83 | }; 84 | } catch (e) { 85 | return e; 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /src/resolvers/query.js: -------------------------------------------------------------------------------- 1 | import dynamodb from "../../libs/dynamodb-lib"; 2 | export const hello = (args, context) => { 3 | return "Your GraphQL API is now LIVE!🎈 "; 4 | }; 5 | 6 | export const getAllListings = async (args, context) => { 7 | const params = { 8 | TableName: process.env.ListingsDB || "dev-lunar-listings", 9 | }; 10 | 11 | try { 12 | const result = await dynamodb.scan(params); 13 | 14 | if (result.Items.length === 0) { 15 | return "You have no listings"; 16 | } else { 17 | return result.Items.map((i) => ({ 18 | listingId: i.listingId, 19 | coverPhoto: i.coverPhoto, 20 | listingName: i.listingName, 21 | listingDescription: i.listingDescription, 22 | listingType: i.listingType.map((m) => ({ 23 | name: m, 24 | })), 25 | listingLocation: i.listingLocation, 26 | listingActivities: i.listingActivities.map((k) => ({ 27 | name: k, 28 | })), 29 | specialType: i.specialType, 30 | specialAmount: i.specialAmount, 31 | rating: i.rating, 32 | guide: { 33 | Name: i.guide.name, 34 | Bio: i.guide.bio, 35 | Avatar: i.guide.avatar, 36 | }, 37 | price: i.price, 38 | numberOfDays: i.numberOfDays, 39 | })); 40 | } 41 | 42 | // return result; 43 | } catch (e) { 44 | return { 45 | message: e.message, 46 | code: "500x", 47 | }; 48 | } 49 | }; 50 | 51 | export const getAListing = async (args, context) => { 52 | const params = { 53 | TableName: process.env.ListingsDB || "dev-lunar-listings", 54 | FilterExpression: "listingId = :listingId", 55 | ExpressionAttributeValues: { 56 | ":listingId": args.listingId, 57 | }, 58 | }; 59 | console.log(params); 60 | 61 | try { 62 | const listing = await dynamodb.scan(params); 63 | 64 | console.log(listing); 65 | 66 | if (listing.Items.length === 0) { 67 | return "There is no listing"; 68 | } else { 69 | return { 70 | listingName: listing.Items[0].listingName, 71 | 72 | listingId: listing.Items[0].listingId, 73 | coverPhoto: listing.Items[0].coverPhoto, 74 | listingDescription: listing.Items[0].listingDescription, 75 | listingType: listing.Items[0].listingType.map((m) => ({ 76 | name: m, 77 | })), 78 | listingLocation: listing.Items[0].listingLocation, 79 | listingActivities: listing.Items[0].listingActivities.map((k) => ({ 80 | name: k, 81 | })), 82 | specialType: listing.Items[0].specialType, 83 | specialAmount: listing.Items[0].specialAmount, 84 | rating: listing.Items[0].rating, 85 | guide: { 86 | Name: listing.Items[0].guide.name, 87 | Bio: listing.Items[0].guide.bio, 88 | Avatar: listing.Items[0].guide.avatar, 89 | }, 90 | price: listing.Items[0].price, 91 | numberOfDays: listing.Items[0].numberOfDays, 92 | }; 93 | } 94 | } catch (e) { 95 | return { 96 | message: e.message, 97 | code: "500x", 98 | }; 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /src/schema.js: -------------------------------------------------------------------------------- 1 | const schema = ` 2 | 3 | type ListingType { 4 | name: String 5 | } 6 | 7 | type ListingActivities { 8 | name: String 9 | } 10 | 11 | type Guide { 12 | Name: String 13 | Bio: String 14 | Avatar: String 15 | } 16 | 17 | type Listing { 18 | listingId: String 19 | coverPhoto: String 20 | listingName: String 21 | listingDescription: String 22 | listingType: [ListingType] 23 | listingLocation: String 24 | listingActivities: [ListingActivities] 25 | specialType: String 26 | specialAmount: Int 27 | rating: Int 28 | guide: Guide 29 | price: String 30 | numberOfDays: Int 31 | 32 | } 33 | 34 | 35 | type Booking { 36 | bookingId: String 37 | listingId: String 38 | bookingDate: String 39 | size: Int 40 | bookingTotal: String 41 | customerEmail: String 42 | customers: [Customer] 43 | chargeReciept: String 44 | 45 | } 46 | type Customer { 47 | name: String 48 | surname: String 49 | country: String 50 | passportNumber: String 51 | physioScore: String 52 | } 53 | 54 | input CustomerInput { 55 | name: String 56 | surname: String 57 | country: String 58 | passportNumber: String 59 | physioScore: String 60 | } 61 | 62 | 63 | """ 64 | A hello world Query 65 | """ 66 | type Query { 67 | hello: String! 68 | getAllListings: [Listing] 69 | getAListing(listingId: String!): Listing! 70 | 71 | } 72 | 73 | 74 | type Mutation { 75 | makeABooking( 76 | listingId: String 77 | bookingDate: String, 78 | customerEmail: String, 79 | customers: [CustomerInput] 80 | ): Booking 81 | 82 | } 83 | 84 | `; 85 | 86 | export { schema }; 87 | -------------------------------------------------------------------------------- /tests/getAllListings.test.js: -------------------------------------------------------------------------------- 1 | import { getAllListings, getAListing } from "../src/resolvers/query"; 2 | describe("All Listings", () => { 3 | test("brings back all listings", async () => { 4 | const args = "args"; 5 | const context = "context"; 6 | 7 | const response = await getAllListings(args, context); 8 | expect(response[0]).toHaveProperty("listingId"); 9 | expect(response.length).toBeGreaterThan(1); 10 | }); 11 | 12 | test("brings a listing", async () => { 13 | const args = { listingId: "a114dded-ddef-4052-a106-bb18b94e6b51" }; 14 | const context = "context"; 15 | 16 | const response = await getAListing(args, context); 17 | expect(response.listingId).toEqual(args.listingId); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/handler.test.js: -------------------------------------------------------------------------------- 1 | import * as handler from '../handler'; 2 | 3 | test('hello', async () => { 4 | const event = 'event'; 5 | const context = 'context'; 6 | const callback = (error, response) => { 7 | expect(response.statusCode).toEqual(200); 8 | expect(typeof response.body).toBe("string"); 9 | }; 10 | 11 | await handler.hello(event, context, callback); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/makeABooking.test.js: -------------------------------------------------------------------------------- 1 | import { makeABooking } from "../src/resolvers/mutation"; 2 | 3 | describe("Make a booking", () => { 4 | test("Successfully able to make a booking", async () => { 5 | const args = { 6 | listingId: "a114dded-ddef-4052-a106-bb18b94e6b51", 7 | bookingDate: "24-Apr-20", 8 | size: 2, 9 | customerEmail: "angela@dundler.com", 10 | customers: [ 11 | { 12 | name: "Dwight", 13 | surname: "Shrut", 14 | passportNumber: "3333344", 15 | physioScore: "454", 16 | }, 17 | { 18 | name: "Pam", 19 | surname: "Papper", 20 | passportNumber: "34354", 21 | physioScore: "2945", 22 | }, 23 | ], 24 | }; 25 | 26 | const context = "context"; 27 | 28 | const response = await makeABooking(args, context); 29 | 30 | console.log(response); 31 | }); 32 | }); 33 | --------------------------------------------------------------------------------