├── .gitignore ├── .husky └── .gitignore ├── .vscode └── launch.json ├── README.md ├── api └── graphqlHandler.js ├── apollo.config.js ├── commitlint.config.js ├── database └── connection.js ├── graphql ├── apollo-server.js ├── resolvers.js └── schema.graphql ├── helpers └── firebase-cloud-messaging.js ├── package-lock.json ├── package.json └── serverless.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | 11 | # Env variables/secrets 12 | env.yml 13 | 14 | yarn.lock 15 | package-lock.json 16 | 17 | .env 18 | .DS_Store 19 | 20 | firebase-adminsdk.json 21 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Lambda", 5 | "type": "node", 6 | "request": "launch", 7 | "runtimeArgs": ["--inspect", "--debug-port=9229"], 8 | "program": "${workspaceFolder}/node_modules/serverless/bin/serverless", 9 | "args": ["offline"], 10 | "port": 9229, 11 | "console": "integratedTerminal" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-graphql-mongodb-lambda 2 | 3 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 4 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 5 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) 6 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 7 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/) 8 | 9 | ## Features / Tools 10 | 11 | 1. VSCode debugger setup at `.vscode/launch.json` with serverless-offline, just press f5 to start a local server and inspect break points 12 | 1. Apollo-Server v2 setup for an AWS Lambda 13 | 1. Caching GraphQL Responses in memory or redis 14 | 1. MongoDB/Mongoose integration & Caching connections outside of lambda handler 15 | 1. Serverless setup for different environments & deployment targets 16 | 1. serverless-warmup plugin integration to reduce cold starts possibility and keep the lambdas warm 17 | 1. Semantic-versioning, changelog generation and automated releases thanks to semantic-release 18 | 1. Linting with StandardJS 19 | 1. Gzip compression 20 | 1. Environment variables per stage can be defined in an `env.yml` file 21 | 1. AWS X-Ray integration for traces 22 | 1. GraphQL Playground integration w/ traces 23 | 1. Apollo Engine/Graph Manager integration for monitoring 24 | 1. Serverless Dashboard monitoring 25 | 26 | ## Setup 27 | 28 | - Setup your aws credentials and aws profile using `aws configure --profile ` 29 | - `npm install` 30 | - use `yarn start` to run in offline mode` 31 | - `yarn deploy:dev` to deploy dev environment 32 | - `yarn deploy:live` to deploy to live 33 | 34 | ## Environment variables 35 | 36 | - Environment variables can be setup in `env.yml` file 37 | - add your `ENGINE_API_KEY` to a .env file 38 | 39 | ## Deploying 40 | 41 | - Install serverless cli using `npm i -g serverless` 42 | - Install and setup your aws cli with an aws profile using `aws config --profile=` 43 | - use `yarn deploy:dev` or `yarn deploy:live` 44 | 45 | ## Releasing 46 | 47 | This repo uses [semantic-release](https://github.com/semantic-release/semantic-release), [commitizen](https://github.com/commitizen/cz-cli), [commitlint](http://commitlint.js.org), [husky](https://github.com/typicode/husky) and [conventional commits](https://conventionalcommits.org/en/v1.0.0-beta.4/) in order to automate the release proccess 48 | 49 | ### Setup/Prequisites 50 | 51 | 1. The user needs to have a `GH_TOKEN` environment variable set with a valid git token that has push access to the repository which can be generated following these [steps](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) 52 | 2. the token can be added into a `~/.bash_profile` to avoid manually setting it before running the command. 53 | 3. The script should be run from the release branch only (default: master) otherwise it will fail. To change the release branch you can edit the "release" section in the `package.json` file to temporarily set the release branch as follows 54 | 55 | ```js 56 | "release": { 57 | ... 58 | "branch": "feature/integrate-semantic-release" 59 | ... 60 | } 61 | ``` 62 | 63 | ### Usage 64 | 65 | - `yarn commit` will run commitizen cli to generate a conventional style commit message 66 | - `yarn release` will do a dry-run of the release without pushing or changing any files 67 | - `yarn release --no-ci` will do a real release. 68 | We could potentially use this to automate releases when new commits/PRs land in the master branch 69 | Details 70 | 71 | ### A release will 72 | 73 | - Bump the version in `package.json` according to semantic versioning (semver) based on the changes/commit messages (fix/perf = patch, feat = minor, breaking = major) 74 | - Create a git tag 75 | - Generate a CHANGELOG.md with the latest changes since the last release 76 | - Push the changes to the repository & create a git release 77 | -------------------------------------------------------------------------------- /api/graphqlHandler.js: -------------------------------------------------------------------------------- 1 | 2 | const { server } = require('../graphql/apollo-server') 3 | const { initConnection } = require('../database/connection') 4 | 5 | exports.graphqlHandler = (event, context, callback) => { 6 | context.callbackWaitsForEmptyEventLoop = false 7 | // warmup plugin early return 8 | if (event.source === 'serverless-plugin-warmup' || (context.custom && context.custom.source === 'serverless-plugin-warmup')) { 9 | console.log('WarmUp - Lambda is warm!') 10 | callback(null, { 11 | statusCode: 200, 12 | body: 'warmed' 13 | }) 14 | } else { 15 | initConnection() 16 | .then((connection) => { 17 | console.log('creating handler') 18 | server.createHandler({ 19 | cors: { 20 | origin: '*', 21 | credentials: true 22 | } 23 | })(event, context, callback) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | service: 'my-graph@dev', 4 | includes: ['./graphql/**/*.js'] 5 | }, 6 | service: { 7 | localSchemaFile: './graphql/schema.graphql' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /database/connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | mongoose.set('debug', true) 3 | let cachedConnection = null 4 | 5 | function initConnection () { 6 | if (cachedConnection === null) { 7 | return mongoose.createConnection(process.env.MONGO_URL, { 8 | bufferCommands: false, 9 | bufferMaxEntries: 0, 10 | useNewUrlParser: true, 11 | useUnifiedTopology: true, 12 | useCreateIndex: true, 13 | useFindAndModify: false 14 | }).then(async connection => { 15 | cachedConnection = connection 16 | console.log('connected to mongo') 17 | return cachedConnection 18 | }) 19 | } else { 20 | console.log('using cached connection') 21 | return Promise.resolve(cachedConnection) 22 | } 23 | } 24 | 25 | module.exports = { initConnection } 26 | -------------------------------------------------------------------------------- /graphql/apollo-server.js: -------------------------------------------------------------------------------- 1 | const { ApolloServer } = require('apollo-server-lambda') 2 | const { importSchema } = require('graphql-import') 3 | const { resolvers } = require('./resolvers') 4 | const typeDefs = importSchema('./graphql/schema.graphql') 5 | const responseCachePlugin = require('apollo-server-plugin-response-cache') 6 | const { RedisCache } = require('apollo-server-cache-redis') 7 | 8 | const redisOptions = { 9 | host: process.env.REDIS_HOST, 10 | password: process.env.REDIS_PASSWORD, 11 | port: process.env.REDIS_PORT 12 | } 13 | 14 | const server = new ApolloServer({ 15 | typeDefs, 16 | resolvers, 17 | mocks: true, 18 | playground: { endpoint: process.env.IS_OFFLINE ? `http://localhost:3000/${process.env.AWS_STAGE}/graphql` : `${process.env.BASE_URL}/graphql` }, 19 | introspection: true, 20 | tracing: false, 21 | sendReportsImmediately: true, 22 | cacheControl: { defaultMaxAge: 10 }, 23 | cache: new RedisCache(redisOptions), 24 | plugins: [responseCachePlugin()], 25 | context: async ({ event, context }) => { 26 | // get the user token from the headers 27 | // const token = req.headers.authorization || '' 28 | // try to retrieve a user with the token 29 | // const user = getUser(token) 30 | // add the user to the context 31 | // return { user } 32 | }, 33 | persistedQueries: { 34 | cache: new RedisCache(redisOptions) 35 | } 36 | }) 37 | module.exports = { server } 38 | -------------------------------------------------------------------------------- /graphql/resolvers.js: -------------------------------------------------------------------------------- 1 | const resolvers = { 2 | Query: { 3 | hello: () => { 4 | return new Promise((resolve, reject) => { 5 | setTimeout(() => { 6 | resolve('Hello after two seconds, cached for 10 seconds') 7 | }, 2000) 8 | }) 9 | }, 10 | user: () => { 11 | return { 12 | name: 'khaled', 13 | age: 12 14 | } 15 | } 16 | } 17 | } 18 | 19 | module.exports = { resolvers } 20 | -------------------------------------------------------------------------------- /graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | hello: String 3 | user: User 4 | } 5 | 6 | type User { 7 | name: String 8 | age: Int 9 | } -------------------------------------------------------------------------------- /helpers/firebase-cloud-messaging.js: -------------------------------------------------------------------------------- 1 | const { google } = require('googleapis') 2 | const axios = require('axios') 3 | 4 | const firebaseServiceAccount = { 5 | type: '', 6 | project_id: '', 7 | private_key_id: '', 8 | private_key: '', 9 | client_email: '', 10 | client_id: '', 11 | auth_uri: '', 12 | token_uri: '', 13 | auth_provider_x509_cert_url: '', 14 | client_x509_cert_url: '' 15 | } 16 | 17 | function sendFcmMessage (subscription, token, messageOpts) { 18 | const message = { 19 | notification: { 20 | body: 'body', 21 | title: 'title' 22 | }, 23 | data: { 24 | body: 'body', 25 | title: 'title' 26 | // ... anything else we need to send to the app 27 | }, 28 | token: subscription 29 | } 30 | 31 | return axios.post(`https://fcm.googleapis.com/v1/projects/${firebaseServiceAccount.project_id}/messages:send`, { 32 | message 33 | }, { 34 | headers: { 35 | Authorization: `Bearer ${token}` 36 | } 37 | }) 38 | } 39 | 40 | function getAccessToken () { 41 | return new Promise((resolve, reject) => { 42 | var jwtClient = new google.auth.JWT( 43 | firebaseServiceAccount.client_email, 44 | null, 45 | firebaseServiceAccount.private_key, 46 | ['https://www.googleapis.com/auth/firebase.messaging'], 47 | null 48 | ) 49 | jwtClient.authorize(function (err, tokens) { 50 | if (err) { 51 | reject(err) 52 | return 53 | } 54 | resolve(tokens.access_token) 55 | }) 56 | }) 57 | } 58 | 59 | module.exports = { sendFcmMessage, getAccessToken } 60 | // async function sendTestNotification () { 61 | // const token = await getAccessToken() 62 | // return sendFcmMessage(subscription, token, {}) 63 | // } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-graphql-mongodb-lambda", 3 | "version": "1.0.0", 4 | "description": "Serverless webpack example using Typescript", 5 | "main": "handler.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "" 9 | }, 10 | "config": { 11 | "commitizen": { 12 | "path": "./node_modules/cz-conventional-changelog" 13 | } 14 | }, 15 | "release": { 16 | "plugins": [ 17 | "@semantic-release/commit-analyzer", 18 | "@semantic-release/release-notes-generator", 19 | [ 20 | "@semantic-release/npm", 21 | { 22 | "npmPublish": false 23 | } 24 | ], 25 | "@semantic-release/changelog", 26 | "@semantic-release/git" 27 | ] 28 | }, 29 | "scripts": { 30 | "start": "sls offline start", 31 | "debug": "node --inspect --debug-port=9229 node_modules/serverless/bin/serverless offline start", 32 | "test": "echo \"Error: no test specified\" && exit 1", 33 | "commit": "npx git-cz", 34 | "release": "npx semantic-release", 35 | "commitizen": "exec < /dev/tty && git cz", 36 | "lint": "standard", 37 | "lint:fix": "standard --fix", 38 | "deploy": "yarn deploy:dev", 39 | "deploy:dev": "sls deploy --stage dev --region eu-central-1", 40 | "deploy:live": "sls deploy --stage live --region us-east-1", 41 | "remove:dev": "sls remove --stage dev --region eu-central-1", 42 | "remove:live": "sls remove --stage live --region us-east-1" 43 | }, 44 | "husky": { 45 | "hooks": { 46 | "pre-commit": "yarn lint:fix", 47 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 48 | } 49 | }, 50 | "dependencies": { 51 | "apollo-server-cache-redis": "^1.2.3", 52 | "apollo-server-lambda": "^2.21.0", 53 | "apollo-server-plugin-response-cache": "^0.6.0", 54 | "axios": "^0.21.2", 55 | "graphql": "^15.5.0", 56 | "graphql-import": "^0.7.1", 57 | "mongoose": "^5.13.15" 58 | }, 59 | "devDependencies": { 60 | "@commitlint/cli": "^11.0.0", 61 | "@commitlint/config-conventional": "^11.0.0", 62 | "@semantic-release/changelog": "^5.0.1", 63 | "@semantic-release/commit-analyzer": "^8.0.1", 64 | "@semantic-release/git": "^9.0.0", 65 | "@semantic-release/npm": "^7.0.10", 66 | "@semantic-release/release-notes-generator": "^9.0.1", 67 | "@types/aws-lambda": "^8.10.72", 68 | "@types/node": "^14.14.31", 69 | "aws-sdk": "^2.848.0", 70 | "cz-conventional-changelog": "^3.3.0", 71 | "husky": "^5.0.9", 72 | "semantic-release": "^19.0.3", 73 | "serverless": "^2.72.3", 74 | "serverless-offline": "^9.3.0", 75 | "serverless-plugin-warmup": "^5.2.0" 76 | }, 77 | "author": "Khaled Osman", 78 | "license": "MIT" 79 | } 80 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: serviceName 2 | # app: app 3 | # org: org 4 | 5 | custom: 6 | aws_profile: "awsprofile" 7 | environment: ${file(env.yml):${self:provider.stage}, file(env.yml):offline} 8 | warmup: 9 | enabled: true 10 | prewarm: true 11 | name: ${self:service}-${self:provider.stage}-lambda-warmer 12 | concurrency: 1 13 | 14 | # Add the serverless-webpack plugin 15 | plugins: 16 | - serverless-offline 17 | - serverless-plugin-warmup 18 | 19 | provider: 20 | name: aws 21 | runtime: nodejs10.x 22 | stage: ${opt:stage, 'offline'} 23 | profile: ${self:custom.aws_profile} # aws credentials profile to use 24 | region: ${opt:region, 'eu-central-1'} 25 | lambdaHashingVersion: 20201221 26 | apiGateway: 27 | shouldStartNameWithService: true 28 | minimumCompressionSize: 1024 29 | tracing: 30 | apiGateway: true 31 | lambda: true 32 | environment: 33 | REDIS_HOST: ${self:custom.environment.REDIS_HOST} 34 | REDIS_PASSWORD: ${self:custom.environment.REDIS_PASSWORD} 35 | REDIS_PORT: ${self:custom.environment.REDIS_PORT} 36 | MONGO_URL: ${self:custom.environment.MONGO_URL} 37 | ENGINE_API_KEY: ${self:custom.environment.ENGINE_API_KEY} 38 | BASE_URL: ${self:custom.environment.BASE_URL} 39 | NODE_ENV: PRODUCTION 40 | AWS_STAGE: ${self:provider.stage} 41 | GOOGLE_APPLICATIONS_CREDENTIALS: "./firebase-adminsdk.json" 42 | APOLLO_KEY: ${self:custom.environment.APOLLO_KEY} 43 | APOLLO_GRAPH_VARIANT: ${self:custom.environment.APOLLO_GRAPH_VARIANT} 44 | APOLLO_SCHEMA_REPORTING: ${self:custom.environment.APOLLO_SCHEMA_REPORTING} 45 | iam: 46 | role: 47 | statements: 48 | - Effect: Allow 49 | Action: 50 | - lambda:* 51 | Resource: "*" 52 | 53 | - Effect: "Allow" 54 | Action: 55 | - "ses:SendEmail" 56 | - "ses:SendRawEmail" 57 | Resource: 58 | - "*" 59 | 60 | - Effect: "Allow" 61 | Action: 62 | - s3:* 63 | Resource: "*" 64 | 65 | - Effect: "Allow" 66 | Action: 67 | - "sqs:*" 68 | Resource: "*" 69 | 70 | package: # Optional deployment packaging configuration 71 | # include: # Specify the directories and files which should be included in the deployment package 72 | # - src/** 73 | # - handler.js 74 | exclude: # Specify the directories and files which should be excluded in the deployment package 75 | - .git/** 76 | - apollo.config.js 77 | - commitlint.config.js 78 | - env.yml 79 | - .env 80 | - package-lock.json 81 | - package.json 82 | - yarn.lock 83 | - README.md 84 | - scripts/** 85 | - .vscode/** 86 | - .DS_Store 87 | excludeDevDependencies: true 88 | 89 | functions: 90 | graphqlHandler: 91 | handler: api/graphqlHandler.graphqlHandler 92 | events: 93 | - http: 94 | path: playground 95 | method: get 96 | cors: true 97 | - http: 98 | path: graphql 99 | method: get 100 | cors: true 101 | - http: 102 | path: graphql 103 | method: post 104 | cors: true 105 | # - cloudFront: 106 | # eventType: origin-request 107 | # origin: https://app.acme.com 108 | --------------------------------------------------------------------------------