├── .gitignore ├── README.md ├── index.js ├── lib ├── schema │ ├── index.js │ ├── mutation │ │ └── addTrackMutation.js │ └── query │ │ ├── playlistQuery.js │ │ └── suggestionsQuery.js ├── services │ ├── playlistService.js │ └── suggestionsService.js └── utils │ └── lambda-invoke.js ├── package.json ├── scripts ├── create-api.sh ├── create-iam.sh └── create-s3-bucket.sh └── test ├── lambda └── index.test.js ├── schema ├── addTrackMutation.test.js ├── fixtures │ └── index.js ├── playlistQuery.test.js └── suggestionsQuery.test.js ├── services └── playlistService.test.js ├── test-helpers.js ├── introspectGraphQL.js └── schemaHelper.js ├── test-helpers ├── introspectGraphQL.js └── schemaHelper.js └── utils └── lambda-invoke.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | node_modules/ 15 | .DS_Store 16 | npm-debug.log 17 | .env 18 | coverage/ 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless-GraphQL 2 | 3 | A Serverless Jukebox app built using AWS Lambda, React, GraphQL, API Gateway and Node.js. You can search for songs using the [Last.fm api](http://www.last.fm/api) and save the names of songs you like to a playlist. 4 | 5 | ## Technologies 6 | - GraphQL 7 | - AWS Lambda - hosts the GraphQL server 8 | - AWS API Gateway - expose a public HTTP endpoint for the GraphQL lambda 9 | - Node.js 10 | 11 | ## Architecture 12 | 13 | ![architecture](https://cloud.githubusercontent.com/assets/5912647/15094803/deb865aa-14a7-11e6-870f-1fe552ead186.png) 14 | 15 | This repo has all the code for the GraphQL Lambda function and creation of an s3 bucket and API Gateway endpoint connected to the lambda function. 16 | 17 | In addition to the GraphQL Lambda there are three other lambda functions 18 | 19 | * [**song-suggester**](https://github.com/nikhilaravi/song-suggester) - Queries the Last.fm api to retrieve song suggestions 20 | * [**s3-save**](https://github.com/nikhilaravi/s3-save) - Saves a selected song to an s3 bucket 21 | * [**s3-get**](https://github.com/nikhilaravi/s3-get) - Retrieves all songs from an s3 bucket 22 | 23 | The GraphQL IDE and UI live here: https://github.com/nikhilaravi/serverless-graphql-app 24 | 25 | ## Before you start 26 | - Create an account on [Amazon](https://aws.amazon.com/console/) 27 | - Get your access key id and secret access key. Give your user super access - add an administrator access policy so you can create lambdas, api endpoints etc using the aws-cli. 28 | - Install the [aws-cli](http://docs.aws.amazon.com/cli/latest/userguide/installing.html#install-bundle-other-os) and configure it with your credentials by typing `aws configure` and pressing Enter. This interactive command will prompt you for your access keys and region. 29 | - Get a last.fm api key by creating an account [https://secure.last.fm/login?next=/api/account/create](https://secure.last.fm/login?next=/api/account/create) 30 | 31 | Add the following environment variables to a .env file 32 | ```sh 33 | AWS_ACCOUNT_ID=[your_account_id_here] 34 | AWS_ACCESS_KEY_ID=[your_key_here] 35 | AWS_SECRET_ACCESS_KEY=[your_secret_here] 36 | AWS_REGION=[aws_region] (e.g. 'eu-west-1') 37 | API_KEY=[lastfm_api_key] 38 | ``` 39 | 40 | ## Start building! 41 | 42 | ### 1. Create IAM Roles 43 | 44 | AWS requires you to specify the permissions which are given to different resources e.g. who can view the objects inside an s3 bucket, who can invoke a lambda function etc. 45 | 46 | These permissions are defined as policies and for different IAM roles which can then be assigned to different services or external services like CodeShip. 47 | 48 | ```sh 49 | npm run create-iam 50 | ``` 51 | 52 | This will create an iam role for the lambda function and print it in the terminal. 53 | 54 | ***Export this as an environment variable***. This role will be used by the deploy script. 55 | 56 | ```sh 57 | export AWS_IAM_ROLE="arn:account_id:role_name" 58 | ``` 59 | 60 | ### 2. Create an s3 bucket to use as the data store 61 | 62 | When a track is added to the playlist, it will be saved as a json file in s3. An s3 bucket needs to be created and given permission to be modified by other AWS services (i.e. permission to allow our Lambda to write and read from it). 63 | 64 | Modify the name of the bucket at the top of `./scripts/create-s3-bucket.sh` and also update the bucket name in the `.env` file (so the lambda knows which bucket to read and write to). 65 | 66 | Create the bucket using the following command: 67 | 68 | ```sh 69 | npm run create-s3 70 | ``` 71 | 72 | and to the `.env` file add: 73 | 74 | `` 75 | S3_BUCKET='name of bucket' 76 | `` 77 | 78 | ### 3. Create the microservices 79 | 80 | There are three microservices apart from the GraphQL microservice. 81 | 82 | Clone the following repos and run `npm run deploy` for each one. 83 | 84 | * [**song-suggester**](https://github.com/nikhilaravi/song-suggester) - Queries the Last.fm api to retrieve song suggestions (requires an `API_KEY` as an environment variable) 85 | * [**s3-save**](https://github.com/nikhilaravi/s3-save) - Saves a selected song to an s3 bucket 86 | * [**s3-get**](https://github.com/nikhilaravi/s3-get) - Retrieves all songs from an s3 bucket 87 | 88 | The lambda functions are zipped and uploaded to AWS using the `dpl` node module. The components of the function are specified in the `files_to_deploy` key in the `package.json`. The important file is `index.js` which must contain an `exports.handler` function which accepts `event` and `context` parameters. The node modules in the `dependencies` in the `package.json` are also zipped along with any other files that are specified before being uploaded to AWS Lambda (either updating a function or creating a new function if it doesn't exist). 89 | 90 | `dpl` names the lambda using the name in the `package.json` with the major version number suffixed e.g. _'serverless-graphql-v1'_. 91 | 92 | Have a look at the notes in the [dpl npm module docs](https://github.com/numo-labs/aws-lambda-deploy) or ask @nelsonic for more info! 93 | 94 | Then save the following three environment variables to the `.env` file in this repo - this will be used by the graphql lambda to call the correct microservice. 95 | 96 | ```sh 97 | LAMBDA_SONG_SUGGESTER=song-suggester-v1 98 | LAMBDA_S3_SAVE=s3-save-v1 99 | LAMBDA_S3_GET=s3-get-v1 100 | ``` 101 | 102 | ### 4. Create a GraphQL lambda function and connect it to an API GATEWAY endpoint. 103 | 104 | Now time to deploy the GraphQL lambda function and connect it to the API endpoint! 105 | 106 | To modify the name of the API you can edit the `./scripts/create-api.sh` file. You need to set the name of the lambda function at the top of this file. 107 | Run the following command in your terminal window (which has all the environment variables set) 108 | 109 | ```sh 110 | npm run deploy-app 111 | ``` 112 | 113 | The `aws-cli` is used to create the API Gateway endpoint and link it to the lambda function. 114 | 115 | Have a look at the `./scripts/create-api.sh` file for the step by step process. 116 | 117 | The last command is a test invocation of the api gateway endpoint to check it has been connected correctly with the lambda. It should print a list of songs which have the name 'Stronger'! 118 | 119 | ### 4. Enable CORS in the AWS Console and get the invoke URL 120 | 121 | > When your API's resources receive requests from a domain other than the API's own domain, you must enable cross-origin resource > sharing (CORS) for selected methods on the resource. 122 | 123 | This can be done in the AWS Console. 124 | 125 | ![enablecors](https://cloud.githubusercontent.com/assets/5912647/14939120/89607546-0f31-11e6-8b3f-37bf4b0c0a4d.png) 126 | 127 | Navigate to the stages section and select 'prod'. You can then find the api invoke url. This will be needed to set up the GraphQL IDE (GraphiQL) and for the UI to invoke the lambda function 128 | 129 | ![invokeurl](https://cloud.githubusercontent.com/assets/5912647/14939122/8e752b30-0f31-11e6-83ee-81665d2f2856.png) 130 | 131 | 132 | ## What's next? 133 | 134 | GREAT! Your lambda and api are now ready for the front end! 135 | 136 | Head over to https://github.com/nikhilaravi/serverless-graphql-app to learn how to deploy the GraphiQL IDE to query your schema and the UI for the Jukebox app! 137 | 138 | 139 | ## What about tests? 140 | 141 | There are some example tests in the test folder for the GraphQL schema and services. You can run the tests with 142 | 143 | ```sh 144 | npm test 145 | ``` 146 | 147 | The coverage isn't 100% yet but i'll be adding more tests soon!! 148 | 149 | ## TODO 150 | 151 | * [ ] Add more notes on the AWS configuration and setting up of credentials and the cli 152 | * [ ] Update the 'create-api' script to update the gateway endpoint if it has already been created 153 | * [ ] Add a script to Enable CORS from the command line 154 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('env2')('.env'); 3 | var graphql = require('graphql'); 4 | var isEmpty = require('lodash.isempty'); 5 | var schema = require('./lib/schema'); 6 | 7 | exports.handler = function (event, context, callback) { 8 | console.log('Incoming Event', event); 9 | // In the introspection query from GraphiQL the variables key is not present in the event body 10 | var variables = event.variables && !isEmpty(event.variables) ? JSON.parse(event.variables) : {}; 11 | graphql.graphql(schema.root, event.query, null, variables) 12 | .then(data => callback(null, data)) 13 | .catch(err => callback(err)); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/schema/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const graphql = require('graphql'); 3 | const suggestionsQuery = require('./query/suggestionsQuery.js').suggestionsQuery; 4 | const playlistQuery = require('./query/playlistQuery.js').playlistQuery; 5 | const addTrackMutation = require('./mutation/addTrackMutation.js').addTrackMutation; 6 | const schema = exports; 7 | 8 | // The main schema 9 | schema.root = new graphql.GraphQLSchema({ 10 | query: new graphql.GraphQLObjectType({ 11 | name: 'Query', 12 | fields: { 13 | suggestions: suggestionsQuery, 14 | playlist: playlistQuery 15 | } 16 | }), 17 | mutation: new graphql.GraphQLObjectType({ 18 | name: 'Mutation', 19 | fields: { 20 | addTrack: addTrackMutation 21 | } 22 | }) 23 | }); 24 | -------------------------------------------------------------------------------- /lib/schema/mutation/addTrackMutation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const graphql = require('graphql'); 4 | const playlistService = require('../../services/playlistService.js'); 5 | 6 | const newTrackId = new graphql.GraphQLObjectType({ 7 | name: 'SearchResultId', 8 | description: 'Search result id schema', 9 | fields: function () { 10 | return ({ 11 | id: { 12 | type: graphql.GraphQLString, 13 | description: 'Id of the track in the db' 14 | } 15 | }); 16 | } 17 | }); 18 | 19 | const addTrackMutation = { 20 | name: 'AddTrackMutation', 21 | description: 'Add a track to the playlist and get an id back', 22 | type: newTrackId, 23 | args: { 24 | name: { 25 | type: graphql.GraphQLString 26 | }, 27 | artist: { 28 | type: graphql.GraphQLString 29 | }, 30 | url: { 31 | type: graphql.GraphQLString 32 | }, 33 | imageUrl: { 34 | type: graphql.GraphQLString 35 | } 36 | }, 37 | resolve: function (_, parentArgs, args) { 38 | return playlistService.addTrack(args); 39 | } 40 | }; 41 | 42 | exports.addTrackMutation = addTrackMutation; 43 | -------------------------------------------------------------------------------- /lib/schema/query/playlistQuery.js: -------------------------------------------------------------------------------- 1 | const playlistService = require('../../services/playlistService.js'); 2 | const graphql = require('graphql'); 3 | 4 | const songType = new graphql.GraphQLObjectType({ 5 | name: 'SongType', 6 | description: 'The format of a song in the playlist', 7 | fields: function () { 8 | return { 9 | name: { 10 | type: graphql.GraphQLString 11 | }, 12 | artist: { 13 | type: graphql.GraphQLString 14 | }, 15 | url: { 16 | type: graphql.GraphQLString 17 | }, 18 | imageUrl: { 19 | type: graphql.GraphQLString 20 | }, 21 | id: { 22 | type: graphql.GraphQLString 23 | } 24 | }; 25 | } 26 | }); 27 | 28 | const playlistType = new graphql.GraphQLList(songType); 29 | 30 | const playlistQuery = { 31 | name: 'PlaylistQuery', 32 | description: 'Retrieve songs in the playlist', 33 | type: playlistType, 34 | resolve: function (_, parentArgs, args) { 35 | console.log('args', _, parentArgs, args); 36 | return playlistService.retrievePlaylist(); 37 | } 38 | }; 39 | 40 | exports.playlistQuery = playlistQuery; 41 | -------------------------------------------------------------------------------- /lib/schema/query/suggestionsQuery.js: -------------------------------------------------------------------------------- 1 | const suggestionsService = require('../../services/suggestionsService.js'); 2 | const graphql = require('graphql'); 3 | 4 | const suggestionsType = new graphql.GraphQLObjectType({ 5 | name: 'SongSuggestionsType', 6 | description: 'The format of a song suggestion', 7 | fields: function () { 8 | return { 9 | name: { 10 | type: graphql.GraphQLString 11 | }, 12 | artist: { 13 | type: graphql.GraphQLString 14 | }, 15 | url: { 16 | type: graphql.GraphQLString 17 | }, 18 | imageUrl: { 19 | type: graphql.GraphQLString 20 | } 21 | }; 22 | } 23 | }); 24 | 25 | const suggestionsListType = new graphql.GraphQLList(suggestionsType); 26 | 27 | const suggestionsQuery = { 28 | name: 'SuggestionQuery', 29 | description: 'Retrieve song search results by song name', 30 | type: suggestionsListType, 31 | args: { 32 | query: { 33 | type: graphql.GraphQLString, 34 | description: 'Name of a song' 35 | }, 36 | limit: { 37 | type: graphql.GraphQLInt, 38 | description: 'Number of song suggestions to retrieve' 39 | } 40 | }, 41 | resolve: function (_, parentArgs, args) { 42 | console.log('args', _, parentArgs, args); 43 | return suggestionsService.retrieveSongSuggestions(args.query, args.limit); 44 | } 45 | }; 46 | 47 | exports.suggestionsQuery = suggestionsQuery; 48 | -------------------------------------------------------------------------------- /lib/services/playlistService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var lambdaInvoke = require('../utils/lambda-invoke'); 3 | 4 | exports.addTrack = function (track) { 5 | var params = { 6 | FunctionName: process.env.LAMBDA_S3_SAVE, 7 | Payload: { 8 | data: track, 9 | bucket: process.env.S3_BUCKET 10 | } 11 | }; 12 | return lambdaInvoke.invoke(params) 13 | .then(function (data) { 14 | return data; 15 | }) 16 | .catch(function (err) { 17 | console.error('ERROR:', err); 18 | return err; 19 | }); 20 | }; 21 | 22 | exports.retrievePlaylist = function (param) { 23 | var params = { 24 | FunctionName: process.env.LAMBDA_S3_GET, 25 | Payload: { 26 | bucket: process.env.S3_BUCKET 27 | } 28 | }; 29 | return lambdaInvoke.invoke(params) 30 | .then(function (data) { 31 | return data; 32 | }) 33 | .catch(function (err) { 34 | console.error('ERROR:', err); 35 | return err; 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/services/suggestionsService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var lambdaInvoke = require('../utils/lambda-invoke'); 3 | 4 | exports.retrieveSongSuggestions = function (query, limit) { 5 | var params = { 6 | FunctionName: process.env.LAMBDA_SONG_SUGGESTER, 7 | Payload: { 8 | query, 9 | limit 10 | } 11 | }; 12 | return lambdaInvoke.invoke(params) 13 | .then(function (data) { 14 | return data; 15 | }) 16 | .catch(function (err) { 17 | console.error('ERROR:', err); 18 | return err; 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/utils/lambda-invoke.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | AWS.config.region = 'eu-west-1'; 3 | 4 | exports.invoke = function (params) { 5 | var Lambda = new AWS.Lambda(); 6 | var p = { 7 | FunctionName: params.FunctionName, 8 | InvocationType: 'RequestResponse', 9 | Payload: JSON.stringify(params.Payload), 10 | LogType: 'None' 11 | }; 12 | return new Promise(function (resolve, reject) { 13 | Lambda.invoke(p, function (err, data) { 14 | if (err) return reject(err); 15 | var payload = JSON.parse(data.Payload); 16 | if (payload.errorMessage) return reject(payload); 17 | console.info('incoming data:', payload); 18 | return resolve(payload); 19 | }); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-graphql", 3 | "version": "1.0.0", 4 | "description": "A serverless app built using AWS lambda, API gateway, React and graphql", 5 | "main": "index.js", 6 | "scripts": { 7 | "create-iam": "source ./scripts/create-iam.sh", 8 | "create-s3": ". ./scripts/create-s3-bucket.sh", 9 | "deploy": "node ./node_modules/dpl/dpl.js", 10 | "create-api": ". ./scripts/create-api.sh", 11 | "deploy-app": "npm run deploy && npm run create-api", 12 | "test": "node ./node_modules/.bin/mocha ./test/**/*.js", 13 | "coverage": "node_modules/.bin/istanbul cover node_modules/.bin/_mocha ./test/**/*.js --report lcov -- -R spec" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/nikhilaravi/serverless-graphql.git" 18 | }, 19 | "keywords": [ 20 | "AWS", 21 | "Lambda", 22 | "React", 23 | "GraphQL", 24 | "API", 25 | "Gateway", 26 | "Serverless" 27 | ], 28 | "author": "Nikhila Ravi", 29 | "license": "ISC", 30 | "bugs": { 31 | "url": "https://github.com/nikhilaravi/serverless-graphql/issues" 32 | }, 33 | "homepage": "https://github.com/nikhilaravi/serverless-graphql#readme", 34 | "devDependencies": { 35 | "aws-sdk": "^2.3.7", 36 | "aws-sdk-mock": "^1.0.10", 37 | "dpl": "^3.0.1", 38 | "istanbul": "^0.4.2", 39 | "mocha": "^2.4.5", 40 | "simple-mock": "^0.6.0", 41 | "sinon": "^1.17.4" 42 | }, 43 | "files_to_deploy": [ 44 | "package.json", 45 | "index.js", 46 | "lib/", 47 | ".env" 48 | ], 49 | "dependencies": { 50 | "env2": "^2.0.7", 51 | "graphql": "^0.5.0", 52 | "lodash.isempty": "^4.2.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/create-api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## TO BE SET 4 | api_name="serverless-graphql-v1" 5 | lambda_function="serverless-graphql-v1" 6 | ### 7 | 8 | api_description="Graphql endpoint" 9 | root_path=/ 10 | resource_path=graphql 11 | stage_name=prod 12 | region=$AWS_REGION 13 | account_id=$AWS_ACCOUNT_ID 14 | random_id1=$[(RANDOM+RANDOM)*100000] 15 | random_id2=$[(RANDOM+RANDOM)*100000] 16 | 17 | # create API 18 | 19 | api_id=$(aws apigateway create-rest-api \ 20 | --region "$region" \ 21 | --name "$api_name" \ 22 | --description "$api_description" \ 23 | --output text \ 24 | --query 'id') 25 | 26 | echo api_id=$api_id 27 | 28 | # Get resource id of root path 29 | 30 | parent_id=$(aws apigateway get-resources \ 31 | --region "$region" \ 32 | --rest-api-id "$api_id" \ 33 | --output text \ 34 | --query 'items[?path==`'$root_path'`].[id]') 35 | echo parent_id=$parent_id 36 | 37 | # Get resource id of resource path 38 | 39 | resource_id=$(aws apigateway create-resource \ 40 | --rest-api-id "$api_id" \ 41 | --parent-id "$parent_id" \ 42 | --path-part "$resource_path" \ 43 | --output text \ 44 | --query 'id') 45 | echo resource_id=${resource_id} 46 | 47 | # create a post method on the resource path 48 | 49 | aws apigateway put-method \ 50 | --region "$region" \ 51 | --rest-api-id "$api_id" \ 52 | --resource-id "$resource_id" \ 53 | --http-method POST \ 54 | --authorization-type NONE 55 | 56 | # create integration with lambda function 57 | 58 | aws apigateway put-integration \ 59 | --rest-api-id "$api_id" \ 60 | --resource-id "$resource_id" \ 61 | --http-method POST \ 62 | --type AWS \ 63 | --integration-http-method POST \ 64 | --uri arn:aws:apigateway:$region:lambda:path/2015-03-31/functions/arn:aws:lambda:$region:$account_id:function:$lambda_function/invocations 65 | 66 | # set the POST method response to JSON 67 | 68 | aws apigateway put-method-response \ 69 | --rest-api-id "$api_id" \ 70 | --resource-id "$resource_id" \ 71 | --http-method POST \ 72 | --status-code 200 \ 73 | --response-models "{\"application/json\": \"Empty\"}" \ 74 | --response-parameters '{"method.response.header.Access-Control-Allow-Origin":true}' 75 | 76 | # set the POST method integration response to JSON. This is the response type that Lambda function returns. 77 | 78 | aws apigateway put-integration-response \ 79 | --rest-api-id "$api_id" \ 80 | --resource-id "$resource_id" \ 81 | --http-method POST \ 82 | --status-code 200 \ 83 | --response-templates "{\"application/json\": \"\"}" \ 84 | --response-parameters '{"method.response.header.Access-Control-Allow-Origin":"'"'*'"'"}' 85 | 86 | # add permissions to lambda to call api gateway 87 | 88 | aws lambda add-permission \ 89 | --function-name "$lambda_function" \ 90 | --statement-id $random_id1 \ 91 | --action lambda:InvokeFunction \ 92 | --principal apigateway.amazonaws.com \ 93 | --source-arn "arn:aws:execute-api:$region:$account_id:$api_id/prod/POST/$resource_path" 94 | 95 | # # grant the Amazon API Gateway service principal (apigateway.amazonaws.com) permissions to invoke your Lambda function 96 | 97 | aws lambda add-permission \ 98 | --function-name "$lambda_function" \ 99 | --statement-id $random_id2 \ 100 | --action lambda:InvokeFunction \ 101 | --principal apigateway.amazonaws.com \ 102 | --source-arn "arn:aws:execute-api:$region:$account_id:$api_id/*/POST/$resource_path" 103 | 104 | # deploy the API 105 | 106 | deployment_id=$(aws apigateway create-deployment \ 107 | --region "$region" \ 108 | --rest-api-id "$api_id" \ 109 | --description "$api_name deployment" \ 110 | --stage-name "$stage_name" \ 111 | --stage-description "$api_name $stage_name" \ 112 | --output text \ 113 | --query 'id') 114 | 115 | echo deployment_id=$deployment_id 116 | 117 | # test invoke 118 | 119 | aws apigateway test-invoke-method \ 120 | --rest-api-id "$api_id" \ 121 | --resource-id "$resource_id" \ 122 | --http-method POST \ 123 | --path-with-query-string "" \ 124 | --body '{"query":"query($query: String){\n suggestions(query:$query) {\n name\n }\n}","variables":"{\n \"query\": \"stronger\"\n}"}' 125 | -------------------------------------------------------------------------------- /scripts/create-iam.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | role_name='lambda_execution_test' 3 | 4 | # IAM trust policy 5 | 6 | aws iam create-role \ 7 | --role-name "$role_name" \ 8 | --assume-role-policy-document '{ 9 | "Version": "2012-10-17", 10 | "Statement": [ 11 | { 12 | "Sid": "", 13 | "Effect": "Allow", 14 | "Principal": { 15 | "Service": "lambda.amazonaws.com" 16 | }, 17 | "Action": "sts:AssumeRole" 18 | } 19 | ] 20 | }' 21 | 22 | # IAM access policy 23 | 24 | aws iam put-role-policy \ 25 | --role-name "$role_name" \ 26 | --policy-name "lambda_execution_access_policy" \ 27 | --policy-document '{ 28 | "Version": "2012-10-17", 29 | "Statement": [ 30 | { 31 | "Effect": "Allow", 32 | "Action": [ 33 | "logs:CreateLogGroup", 34 | "logs:CreateLogStream", 35 | "logs:PutLogEvents" 36 | ], 37 | "Resource": "arn:aws:logs:*:*:*" 38 | }, 39 | { 40 | "Effect": "Allow", 41 | "Action": [ 42 | "s3:*" 43 | ], 44 | "Resource": [ 45 | "arn:aws:s3:::*" 46 | ] 47 | }, 48 | { 49 | "Effect": "Allow", 50 | "Action": [ 51 | "lambda:*" 52 | ], 53 | "Resource": [ 54 | "arn:aws:lambda:*:*:*" 55 | ] 56 | } 57 | ] 58 | }' 59 | 60 | 61 | AWS_IAM_ROLE=$(aws iam get-role \ 62 | --role-name "$role_name" \ 63 | --output text \ 64 | --query 'Role.Arn') 65 | 66 | echo $AWS_IAM_ROLE 67 | 68 | export AWS_IAM_ROLE=$AWS_IAM_ROLE 69 | -------------------------------------------------------------------------------- /scripts/create-s3-bucket.sh: -------------------------------------------------------------------------------- 1 | bucket="serverless-database" 2 | region="eu-west-1" 3 | 4 | aws s3api create-bucket \ 5 | --bucket "$bucket" \ 6 | --region "$region" \ 7 | --create-bucket-configuration LocationConstraint="$region" \ 8 | --acl "public-read" \ 9 | 10 | aws s3api put-bucket-policy \ 11 | --bucket "$bucket" \ 12 | --policy '{ 13 | "Version": "2012-10-17", 14 | "Id": "*******", 15 | "Statement": [ 16 | { 17 | "Sid": "*******", 18 | "Effect": "Allow", 19 | "Principal": "*", 20 | "Action": "s3:*", 21 | "Resource": "arn:aws:s3:::'"$bucket"'/*" 22 | } 23 | ] 24 | }' 25 | -------------------------------------------------------------------------------- /test/lambda/index.test.js: -------------------------------------------------------------------------------- 1 | const invokeQuery = require('../schema/fixtures').suggestionsQuery; 2 | 3 | var assert = require('assert'); 4 | var index = require('../../index.js'); 5 | describe('Invoke Test', () => { 6 | it('invokes the lambda with a suggestions query', (done) => { 7 | const event = { 8 | query: invokeQuery, 9 | variables: JSON.stringify({ 10 | query: 'Hello' 11 | }) 12 | }; 13 | index.handler(event, {}, (err, res) => { 14 | assert.equal(err, null); 15 | assert.equal(res.data.suggestions.length > 0, true); 16 | done(); 17 | }); 18 | }).timeout(5000); 19 | }); 20 | -------------------------------------------------------------------------------- /test/schema/addTrackMutation.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graphql = require('graphql').graphql; 4 | var assert = require('assert'); 5 | const simple = require('simple-mock'); 6 | 7 | var addTrack = require('../../lib/schema/mutation/addTrackMutation.js'); 8 | var root = require('../../lib/schema').root; 9 | var playlistService = require('../../lib/services/playlistService.js'); 10 | 11 | var introspect = require('../test-helpers/introspectGraphQL'); 12 | var schemaHelper = require('../test-helpers/schemaHelper'); 13 | 14 | var addTrackMutation = require('./fixtures').addTrackMutation; 15 | 16 | describe('addTrack schema', function () { 17 | it('should be possible to introspect the playlistQuery schema', function (done) { 18 | var schema = schemaHelper.createQuerySchema(addTrack.addTrackMutation); 19 | introspect.introspectGraphQL(schema, done); 20 | }); 21 | 22 | it('should be able to execute the addTrack mutation', function (done) { 23 | var id = { 24 | id: '123456' 25 | }; 26 | simple.mock(playlistService, 'addTrack').resolveWith(id); 27 | var expectedResult = { 28 | 'data': { 29 | 'addTrack': id 30 | } 31 | }; 32 | graphql(root, addTrackMutation, null, {}).then(function (result) { 33 | assert.deepEqual(result, expectedResult); 34 | simple.restore(); 35 | done(); 36 | }).catch(done); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/schema/fixtures/index.js: -------------------------------------------------------------------------------- 1 | exports.playlistQuery = ` 2 | query { 3 | playlist { 4 | name, 5 | artist, 6 | url, 7 | imageUrl 8 | } 9 | } 10 | `; 11 | 12 | exports.suggestionsQuery = ` 13 | query($query: String) { 14 | suggestions(query: $query) { 15 | name, 16 | artist, 17 | url, 18 | imageUrl 19 | } 20 | } 21 | `; 22 | 23 | exports.addTrackMutation = ` 24 | mutation addTrackMutation($name: String, $artist: String, $url: String, $imageUrl: String) { 25 | addTrack(name: $name, artist: $artist, url: $url, imageUrl: $imageUrl) { 26 | id 27 | } 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /test/schema/playlistQuery.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graphql = require('graphql').graphql; 4 | var assert = require('assert'); 5 | const simple = require('simple-mock'); 6 | 7 | var playlist = require('../../lib/schema/query/playlistQuery.js'); 8 | var root = require('../../lib/schema').root; 9 | var playlistService = require('../../lib/services/playlistService.js'); 10 | 11 | var introspect = require('../test-helpers/introspectGraphQL'); 12 | var schemaHelper = require('../test-helpers/schemaHelper'); 13 | 14 | var playlistQuery = require('./fixtures').playlistQuery; 15 | 16 | describe('Playlist schema', function () { 17 | it('should be possible to introspect the playlistQuery schema', function (done) { 18 | var schema = schemaHelper.createQuerySchema(playlist.playlistQuery); 19 | introspect.introspectGraphQL(schema, done); 20 | }); 21 | 22 | it('should be able to execute the playlistQuery', function (done) { 23 | var playlist = [{ 24 | name: 'Hello', 25 | artist: 'Adele', 26 | url: 'url', 27 | imageUrl: 'imageUrl' 28 | }]; 29 | simple.mock(playlistService, 'retrievePlaylist').resolveWith(playlist); 30 | var expectedResult = { 31 | 'data': { 32 | 'playlist': playlist 33 | } 34 | }; 35 | graphql(root, playlistQuery, null, {}).then(function (result) { 36 | assert.deepEqual(result, expectedResult); 37 | simple.restore(); 38 | done(); 39 | }).catch(done); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/schema/suggestionsQuery.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graphql = require('graphql').graphql; 4 | var assert = require('assert'); 5 | const simple = require('simple-mock'); 6 | 7 | var suggestions = require('../../lib/schema/query/suggestionsQuery.js'); 8 | var root = require('../../lib/schema').root; 9 | var suggestionsService = require('../../lib/services/suggestionsService.js'); 10 | 11 | var introspect = require('../test-helpers/introspectGraphQL'); 12 | var schemaHelper = require('../test-helpers/schemaHelper'); 13 | 14 | var suggestionsQuery = require('./fixtures').suggestionsQuery; 15 | 16 | describe('Suggestions schema', function () { 17 | it('should be possible to introspect the playlistQuery schema', function (done) { 18 | var schema = schemaHelper.createQuerySchema(suggestions.suggestionsQuery); 19 | introspect.introspectGraphQL(schema, done); 20 | }); 21 | 22 | it('should be able to execute the suggestionsQuery', function (done) { 23 | var songSuggestions = [{ 24 | name: 'Hello', 25 | artist: 'Adele', 26 | url: 'url', 27 | imageUrl: 'imageUrl' 28 | }]; 29 | simple.mock(suggestionsService, 'retrieveSongSuggestions').resolveWith(songSuggestions); 30 | var expectedResult = { 31 | 'data': { 32 | 'suggestions': songSuggestions 33 | } 34 | }; 35 | var variables = { query: 'str' }; 36 | graphql(root, suggestionsQuery, null, variables).then(function (result) { 37 | assert.deepEqual(result, expectedResult); 38 | simple.restore(); 39 | done(); 40 | }).catch(done); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/services/playlistService.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const simple = require('simple-mock'); 4 | const assert = require('assert'); 5 | const lambdaInvoke = require('../../lib/utils/lambda-invoke.js'); 6 | const playlistService = require('../../lib/services/playlistService'); 7 | 8 | const track = { 9 | name: 'Hello', 10 | artist: 'Adele', 11 | url: 'url', 12 | imageUrl: 'url' 13 | }; 14 | 15 | const suggestions = [{ 16 | name: 'Hello', 17 | artist: 'Adele', 18 | url: 'url', 19 | imageUrl: 'url' 20 | }]; 21 | 22 | describe('Playlist service', () => { 23 | afterEach(function (done) { 24 | simple.restore(); 25 | done(); 26 | }); 27 | 28 | it('retrievePlaylist: returns an array of songs', done => { 29 | simple.mock(lambdaInvoke, 'invoke').resolveWith(suggestions); 30 | 31 | playlistService.retrievePlaylist('Fire', 10).then(data => { 32 | assert.deepEqual(data, suggestions); 33 | done(); 34 | }).catch(done); 35 | }); 36 | 37 | it('retrievePlaylist: will return an error due to request error', done => { 38 | const error = 'Big bad error'; 39 | simple.mock(lambdaInvoke, 'invoke').rejectWith(error); 40 | 41 | playlistService.retrievePlaylist('some song', 10).then(data => { 42 | assert.deepEqual(data, error); 43 | done(); 44 | }).catch(done); 45 | }); 46 | 47 | it('addTrack: calls the s3 save micro service and returns a track id', done => { 48 | simple.mock(lambdaInvoke, 'invoke').resolveWith({id: '1234456'}); 49 | 50 | playlistService.addTrack(track).then(data => { 51 | assert.deepEqual(data, {id: '1234456'}); 52 | done(); 53 | }).catch(done); 54 | }); 55 | it('addTrack: will return an error due to request error', done => { 56 | const error = 'Big bad error'; 57 | simple.mock(lambdaInvoke, 'invoke').rejectWith(error); 58 | 59 | playlistService.addTrack(track).then(data => { 60 | assert.deepEqual(data, error); 61 | done(); 62 | }).catch(done); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/test-helpers.js/introspectGraphQL.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graphql = require('graphql'); 4 | var introspectionQuery = require('graphql/utilities').introspectionQuery; 5 | 6 | var q = exports; 7 | 8 | q.introspectGraphQL = function (schema, done) { 9 | graphql.graphql(schema, introspectionQuery).then(function (result) { 10 | if (result.errors && result.errors.length) { 11 | return done(new Error(result.errors[0].message)); 12 | } 13 | done(); 14 | }).catch(function (err) { 15 | console.log('ERR', err); 16 | done(err); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /test/test-helpers.js/schemaHelper.js: -------------------------------------------------------------------------------- 1 | var graphql = require('graphql'); 2 | 3 | var createQuerySchema = function (query, field) { 4 | return new graphql.GraphQLSchema({ 5 | query: new graphql.GraphQLObjectType({ 6 | name: 'RootQuery', 7 | fields: function () { 8 | var x = {}; 9 | x[field || 'type'] = query; 10 | return x; 11 | } 12 | }) 13 | }); 14 | }; 15 | 16 | var createMutationSchema = function (mutation, field) { 17 | return new graphql.GraphQLSchema({ 18 | query: new graphql.GraphQLObjectType({ 19 | name: 'RootQuery', 20 | fields: { 21 | helperField: { type: graphql.GraphQLString } 22 | } 23 | }), 24 | mutation: new graphql.GraphQLObjectType({ 25 | name: 'RootMutation', 26 | fields: function () { 27 | var x = {}; 28 | x[field || 'type'] = mutation; 29 | return x; 30 | } 31 | }) 32 | }); 33 | }; 34 | 35 | exports.createQuerySchema = createQuerySchema; 36 | exports.createMutationSchema = createMutationSchema; 37 | -------------------------------------------------------------------------------- /test/test-helpers/introspectGraphQL.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graphql = require('graphql'); 4 | var introspectionQuery = require('graphql/utilities').introspectionQuery; 5 | 6 | var q = exports; 7 | 8 | q.introspectGraphQL = function (schema, done) { 9 | graphql.graphql(schema, introspectionQuery).then(function (result) { 10 | if (result.errors && result.errors.length) { 11 | return done(new Error(result.errors[0].message)); 12 | } 13 | done(); 14 | }).catch(function (err) { 15 | console.log('ERR', err); 16 | done(err); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /test/test-helpers/schemaHelper.js: -------------------------------------------------------------------------------- 1 | var graphql = require('graphql'); 2 | 3 | var createQuerySchema = function (query, field) { 4 | return new graphql.GraphQLSchema({ 5 | query: new graphql.GraphQLObjectType({ 6 | name: 'RootQuery', 7 | fields: function () { 8 | var x = {}; 9 | x[field || 'type'] = query; 10 | return x; 11 | } 12 | }) 13 | }); 14 | }; 15 | 16 | var createMutationSchema = function (mutation, field) { 17 | return new graphql.GraphQLSchema({ 18 | query: new graphql.GraphQLObjectType({ 19 | name: 'RootQuery', 20 | fields: { 21 | helperField: { type: graphql.GraphQLString } 22 | } 23 | }), 24 | mutation: new graphql.GraphQLObjectType({ 25 | name: 'RootMutation', 26 | fields: function () { 27 | var x = {}; 28 | x[field || 'type'] = mutation; 29 | return x; 30 | } 31 | }) 32 | }); 33 | }; 34 | 35 | exports.createQuerySchema = createQuerySchema; 36 | exports.createMutationSchema = createMutationSchema; 37 | -------------------------------------------------------------------------------- /test/utils/lambda-invoke.test.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk-mock'); 2 | var assert = require('assert'); 3 | var lambdaInvoke = require('../../lib/utils/lambda-invoke'); 4 | 5 | describe('lambda invoke promise util', function () { 6 | afterEach(function (done) { 7 | AWS.restore(); 8 | done(); 9 | }); 10 | 11 | it('will invoke a lambda and return a promise', function (done) { 12 | AWS.mock('Lambda', 'invoke', {Payload: '{}'}); 13 | 14 | lambdaInvoke.invoke({some: 'params'}).then(function (data) { 15 | assert.deepEqual(data, {}); 16 | done(); 17 | }).catch(done); 18 | }); 19 | 20 | it('will throw an error when the lambda.invoke failed', function (done) { 21 | AWS.mock('Lambda', 'invoke', function (params, cb) { 22 | return cb('Big Error'); 23 | }); 24 | 25 | lambdaInvoke.invoke({some: 'params'}).catch(function (error) { 26 | assert.equal(error, 'Big Error'); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | --------------------------------------------------------------------------------