├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYRIGHT ├── LICENSE ├── README.md ├── package.json ├── prettier.config.js ├── serverless.component.yml ├── src ├── _src │ ├── resolvers.js │ ├── schema.graphql │ └── serverless.yml ├── package.json ├── serverless.js └── userLambdaHandler.js └── templates └── graphql-starter ├── resolvers.js ├── schema.graphql ├── serverless.template.yml └── serverless.yml /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['prettier'], 4 | plugins: ['import', 'prettier'], 5 | env: { 6 | es6: true, 7 | jest: true, 8 | node: true 9 | }, 10 | parser: 'babel-eslint', 11 | parserOptions: { 12 | ecmaVersion: 2018, 13 | sourceType: 'module', 14 | ecmaFeatures: { 15 | jsx: true 16 | } 17 | }, 18 | globals: { 19 | on: true // for the Socket file 20 | }, 21 | rules: { 22 | 'array-bracket-spacing': [ 23 | 'error', 24 | 'never', 25 | { 26 | objectsInArrays: false, 27 | arraysInArrays: false 28 | } 29 | ], 30 | 'arrow-parens': ['error', 'always'], 31 | 'arrow-spacing': ['error', { before: true, after: true }], 32 | 'comma-dangle': ['error', 'never'], 33 | curly: 'error', 34 | 'eol-last': 'error', 35 | 'func-names': 'off', 36 | 'id-length': [ 37 | 'error', 38 | { 39 | min: 2, 40 | max: 50, 41 | properties: 'never', 42 | exceptions: ['e', 'i', 'n', 't', 'x', 'y', 'z', '_', '$'] 43 | } 44 | ], 45 | 'no-alert': 'error', 46 | 'no-console': 'error', 47 | 'no-const-assign': 'error', 48 | 'no-else-return': 'error', 49 | 'no-empty': 'off', 50 | 'no-shadow': 'error', 51 | 'no-undef': 'error', 52 | 'no-unused-vars': 'error', 53 | 'no-use-before-define': 'error', 54 | 'no-useless-constructor': 'error', 55 | 'object-curly-newline': 'off', 56 | 'object-shorthand': 'off', 57 | 'prefer-const': 'error', 58 | 'prefer-destructuring': ['error', { object: true, array: false }], 59 | quotes: [ 60 | 'error', 61 | 'single', 62 | { 63 | allowTemplateLiterals: true, 64 | avoidEscape: true 65 | } 66 | ], 67 | semi: ['error', 'never'], 68 | 'spaced-comment': 'error', 69 | strict: ['error', 'never'], 70 | 'prettier/prettier': 'error' 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | *.log 5 | .serverless 6 | v8-compile-cache-* 7 | jest/* 8 | coverage 9 | .serverless_plugins 10 | testProjects/*/package-lock.json 11 | testProjects/*/yarn.lock 12 | .serverlessUnzipped 13 | node_modules 14 | .vscode/ 15 | .eslintcache 16 | dist 17 | .idea 18 | build/ 19 | .env* 20 | env.js 21 | package-lock.json 22 | test 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | components 2 | examples 3 | .idea 4 | .serverless 5 | coverage 6 | .env* 7 | env.js 8 | tmp 9 | test 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting our team at **hello@serverless.com**. As an alternative 59 | feel free to reach out to any of us personally. All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | [version]: http://contributor-covenant.org/version/1/4/ 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a [code of conduct](./CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 7 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Serverless, Inc. https://serverless.com 2 | 3 | Serverless Components may be freely distributed under the Apache 2.0 license. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2018 Serverless, Inc. 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | https://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless GraphQL Component 2 | 3 | This Serverless Framework Component is a specialized developer experience focused on making it easy to deploy and manage GraphQL applications on serverless infrastructure (specifically AWS AppSync and AWS Lambda) on your own AWS account. It comes loaded with powerful development features and represents possibly the easiest, cheapest and most scalable way to host GraphQL apps. 4 | 5 |
6 | 7 | - [x] **Never Pay For Idle** - No requests, no cost. Averages $0.0000002-$0.0000009 per request. 8 | - [x] **Zero Configuration** - All we need is your code, then just deploy (advanced config options are available). 9 | - [x] **Fast Deployments** - Deploy to the cloud in seconds. 10 | - [x] **Realtime Logging** - Rapidly develop on the cloud w/ real-time logs and errors in the CLI. 11 | - [x] **Team Collaboration** - Collaborate with your teammates with shared state and outputs. 12 | - [x] **Custom Domain + SSL** - Auto-configure a custom domain w/ a free AWS ACM SSL certificate. 13 | - [x] **Lambda Default Resolver** - Automatically deploys your code to a lambda function for rapid query resolution. 14 | - [x] **Works with All Data Sources** - Can be configured to work with directly with DynamodDB, and other data sources. 15 | - [x] **Flexible Authorization Options** - Supports all AppSync authorization options, API Key, IAM, Cognito or OpenID auth. 16 | 17 |
18 | 19 | # Contents 20 | 21 | - [**Quick Start**](#quick-start) 22 | - [**Install**](#install) 23 | - [**Initialize**](#initialize) 24 | - [**Deploy**](#deploy) 25 | - [**Query**](#query) 26 | - [**Configuration Reference**](#configuration-reference) 27 | - [**Extend Existing API**](#extend-existing-api) 28 | - [**Lambda Configuration**](#lambda-configuration) 29 | - [**Custom Domain**](#custom-domain) 30 | - [**Custom IAM Policies**](#custom-iam-policies) 31 | - [**Authorization**](#authorization) 32 | - [**Data Sources Resolvers**](#data-sources-resolvers) 33 | - [**CLI Reference**](#cli-reference) 34 | - [**Outputs Reference**](#outputs-reference) 35 | - [**FAQs**](#faqs) 36 | 37 | # Quick Start 38 | 39 | ## Install 40 | 41 | To get started with this component, install the latest version of the Serverless Framework: 42 | 43 | ``` 44 | npm install -g serverless 45 | ``` 46 | 47 | After installation, make sure you connect your AWS account by setting a provider in the org setting page on the [Serverless Dashboard](https://app.serverless.com). 48 | 49 | ## Initialize 50 | 51 | The easiest way to start using the graphql component is by initializing the `graphql-starter` template. Just run this command: 52 | 53 | ``` 54 | serverless init graphql-starter 55 | cd graphql-starter 56 | ``` 57 | 58 | This will also run `npm install` for you. You should now have a directory that looks something like this: 59 | 60 | ``` 61 | |- serverless.yml 62 | |- schema.graphql 63 | |- resolvers.js 64 | ``` 65 | 66 | The `serverless.yml` file is where you define your component config. It looks something like this: 67 | 68 | ```yml 69 | component: graphql 70 | name: graphql-api 71 | 72 | inputs: 73 | src: ./ 74 | ``` 75 | 76 | For more configuration options for the `serverless.yml` file, [check out the Configuration section](#configuration-reference) below. 77 | 78 | The `schema.graphql` is where you define your GraphQL schema. It looks something like this: 79 | 80 | ```graphql 81 | type Post { 82 | id: ID! 83 | } 84 | 85 | type Query { 86 | getPost(id: ID!): Post 87 | } 88 | 89 | type Mutation { 90 | createPost(id: ID!): Post 91 | } 92 | 93 | schema { 94 | query: Query 95 | mutation: Mutation 96 | } 97 | ``` 98 | 99 | The `resolvers.js` file is where you define your schema resolvers. It looks something like this: 100 | 101 | 102 | ```js 103 | const Query = { 104 | // resolver for field getPost in type Query 105 | getPost: async ({ id }) => { 106 | return { id } 107 | } 108 | } 109 | 110 | const Mutation = { 111 | // resolver for field createPost in type Mutation 112 | createPost: async ({ id }) => { 113 | return { id } 114 | } 115 | } 116 | 117 | module.exports = { Query, Mutation } 118 | 119 | 120 | ``` 121 | In this file, you simply export each of your schema types (ie. `Query` & `Mutation`) as an object of functions. Each function is a field resolver for that type. 122 | 123 | **All these files are required**. Needless to say, any resolver you define in `resolvers.js`, must also be defined in your schema in the `schema.graphql` file, otherwise, you'll get an AppSync error. Same goes for the resolvers inputs & outputs. Remember, GraphQL is strongly typed by design. 124 | 125 | ## Deploy 126 | 127 | Once you have the directory set up, you're now ready to deploy. Just run the following command from within the directory containing the `serverless.yml` file: 128 | 129 | ``` 130 | serverless deploy 131 | ``` 132 | 133 | Your first deployment might take a little while, but subsequent deployment would just take few seconds. 134 | 135 | After deployment is done, you should see your the following outputs: 136 | 137 | ```yml 138 | name: graphql-api-pxzaf135 139 | apiKey: da2-yf444kxlhjerxl376jxyafb2rq 140 | apiId: survbmoad5ewtnm3e3cd7qys4q 141 | url: https://cnbfx5zutbe4fkrtsldsrunbuu.appsync-api.us-east-1.amazonaws.com/graphql 142 | ``` 143 | 144 | Your GraphQL API is now deployed! Next time you deploy, if you'd like to know what's happening under the hood and see realtime logs, you can pass the `--debug` flag: 145 | 146 | ``` 147 | serverless deploy --debug 148 | ``` 149 | 150 | ## Query 151 | 152 | You can query and test your newly created GraphQL API directly with the AWS AppSync console, or any HTTP client. 153 | 154 | Here's a snippet using `fetch` or `node-fetch` with the example above: 155 | 156 | ```js 157 | // you can get the url and apiKey values from the deployment outputs 158 | const url = 'https://cnbfx5zutbe4fkrtsldsrunbuu.appsync-api.us-east-1.amazonaws.com/graphql' 159 | const apiKey = 'da2-yf444kxlhjerxl376jxyafb2rq' 160 | 161 | fetch(url, { 162 | method: 'POST', 163 | headers: { 164 | 'Content-Type': 'application/json', 165 | 'x-api-key': apiKey // the "x-api-key" header is required by AppSync 166 | }, 167 | body: JSON.stringify({ 168 | query: `query getPost { getPost(id: "123") { id }}` 169 | }) 170 | }) 171 | .then((res) => res.json()) 172 | .then((post) => console.log(post)) 173 | ``` 174 | 175 | The response should be an echo of the post id, something like this: 176 | 177 | ```json 178 | { 179 | "data": { 180 | "getPost": { 181 | "id": "123" 182 | } 183 | } 184 | } 185 | ``` 186 | 187 | # Configuration Reference 188 | 189 | The GraphQL component is a zero configuration component, meaning that it'll work out of the box with no configuration and sane defaults. With that said, there are still a lot of optional configuration that you can specify. 190 | 191 | Here's a very minimal configuration to get you started. Most of these properties are optional, but if you use them, remember to substitute with your own value if required (ie. the `org` property) 192 | 193 | ```yml 194 | component: graphql # (required) name of the component. In that case, it's graphql. 195 | name: graphql-api # (required) name of your graphql component instance. 196 | org: serverlessinc # (optional) serverless dashboard org. default is the first org you created during signup. 197 | app: myApp # (optional) serverless dashboard app. default is the same as the name property. 198 | stage: dev # (optional) serverless dashboard stage. default is dev. 199 | 200 | inputs: 201 | src: ./ # (optional) path to the source folder. default is a simple blogging app. 202 | region: us-east-2 # (optional) aws region to deploy to. default is us-east-1. 203 | ``` 204 | 205 | Even the `src` input is optional. If you didn't specify any `src` directory containing your code, an example app will be deployed for you. 206 | 207 | Keep reading to learn more about all the configuration options available to you. 208 | 209 | ## Extend Existing API 210 | 211 | If the `appId` input variable is provided this component will extend an existing AppSync API: 212 | 213 | ```yml 214 | inputs: 215 | src: ./ 216 | apiId: xxx # (optional) if provided will extend an existing api. 217 | ``` 218 | 219 | The `apiId` can be reference from the source component using the `apiId` output variable from the component instance that created the graphql API: `${output:[STAGE]:[APP]:[NAME].apiId}` 220 | 221 | ## Lambda Configuration 222 | 223 | If you specify resolvers in a `resolvers.js` file as shown in the quick start above, the component will deploy a lambda function automatically for you to host your resolvers and connect everything together. You can configure this default lambda function with the following inputs: 224 | 225 | ```yml 226 | inputs: 227 | src: ./ 228 | description: My GraphQL App # (optional) lambda description. default is en empty string. 229 | memory: 512 # (optional) lambda memory size. default is 3008. 230 | timeout: 10 # (optional) lambda timeout. default is 300. 231 | env: # (optional) env vars. default is an empty object 232 | TABLE: 'my-table' 233 | layers: # (optional) list of lambda layer arns to attach to your lambda function. 234 | - arn:aws:first:layer 235 | - arn:aws:second:layer 236 | vpcConfig: # (optional) specify a vpc 237 | securityGroupIds: 238 | - sg-xxx 239 | subnetIds: 240 | - subnet-xxx 241 | - subnet-xxx 242 | ``` 243 | 244 | ## Custom Domain 245 | 246 | If you've purchased your domain from AWS Route53, you can configure the domain with a single input: 247 | 248 | ```yml 249 | inputs: 250 | src: ./ 251 | domain: example.com 252 | ``` 253 | 254 | Subdomains work too: 255 | 256 | ```yml 257 | inputs: 258 | src: ./ 259 | domain: api.example.com 260 | ``` 261 | 262 | This will create a a free SSL certificate for you with AWS ACM, deploy a CDN with AWS CloudFront, and setup all the DNS records required. 263 | 264 | If you've purchased your domain elsewhere, you'll have to manually create a Route53 hosted zone for your domain, and point to the AWS nameservers on your registrar before you add the `domain` input. 265 | 266 | ## Custom IAM Policies 267 | 268 | The component creates the minimum required IAM policy based on your configuration. But you could always add your own policy statements using the `policy` input: 269 | 270 | ```yml 271 | inputs: 272 | src: ./src 273 | policy: 274 | - Action: '*' 275 | Effect: Allow 276 | Resource: '*' 277 | ``` 278 | 279 | This policy applies to both the built-in Lambda function and the AppSync API. Keep in mind that this component automatically adds the required IAM policies to invoke your data source depending on your configuration. 280 | 281 | ## Authorization 282 | 283 | This component uses `apiKey` authorization by default. However all other AppSync authorization options are available via the `auth` input. 284 | 285 | `IAM` authorization: 286 | 287 | ```yml 288 | inputs: 289 | src: ./ 290 | auth: iam 291 | ``` 292 | 293 | `Cognito` authorization: 294 | 295 | ```yml 296 | inputs: 297 | src: ./ 298 | auth: 299 | userPoolId: qwertyuiop 300 | defaultAction: ALLOW 301 | region: us-east-1 302 | appIdClientRegex: qwertyuiop 303 | ``` 304 | 305 | `OpenID` authorization: 306 | 307 | ```yml 308 | inputs: 309 | src: ./ 310 | auth: 311 | issuer: qwertyuiop 312 | authTTL: 0 313 | clientId: wertyuiop 314 | iatTTL: 0 315 | ``` 316 | 317 | ## Data Sources Resolvers 318 | 319 | If you'd like to setup your resolvers to use your own existing data sources, you could specify your resolvers as a `serverless.yml` input instead of inside a `resolvers.js` file. 320 | 321 | In that case, you'll need to also specify your own `request` and `response` VTL templates. You could do that directly in `serverless.yml`, or by pointing to a `vtl` file inside of your `src` directory. 322 | 323 | Here's an example using an existing lambda as a data source: 324 | 325 | ```yml 326 | inputs: 327 | src: ./ 328 | resolvers: 329 | Query: # this must be a valid type in your schema 330 | getPost: # this must be a valid resolver in your schmea 331 | lambda: my-lambda # this will set up the my-lambda Lambda as a data source for this resolver 332 | request: > # the request VTL template for this resolver. 333 | { "version": "2017-02-28", "operation": "Invoke", "payload": $util.toJson($context) } 334 | response: response.vtl # you could also point to a VTL file relative to your src directory. 335 | ``` 336 | 337 | These `request` and `response` properties are required regardless of which data source you are working with, and they're different depending on your schema and your application requirements. Check out the [official AWS docs](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference.html) for more information on the required syntax for each data source. 338 | 339 | ## Lambda Resolvers 340 | 341 | ```yml 342 | inputs: 343 | src: ./ 344 | resolvers: 345 | Query: 346 | getPost: 347 | lambda: my-lambda 348 | request: '{ "version": "2017-02-28", "operation": "Invoke", "payload": $util.toJson($context) }' 349 | response: '$util.toJson($context.result)' 350 | ``` 351 | 352 | 353 | ## DynamoDB Resolvers 354 | 355 | ```yml 356 | inputs: 357 | src: ./ 358 | resolvers: 359 | Query: 360 | getPost: 361 | table: my-table 362 | request: > 363 | { 364 | "version" : "2017-02-28", 365 | "operation" : "PutItem", 366 | "key" : { 367 | "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id) 368 | } 369 | } 370 | response: '$util.toJson($context.result)' 371 | ``` 372 | 373 | ## ElasticSearch Resolvers 374 | 375 | ```yml 376 | inputs: 377 | src: ./ 378 | resolvers: 379 | Query: 380 | getPost: 381 | endpoint: https://search-my-sample-data-abbaabba.us-east-1.es.amazonaws.com 382 | request: > 383 | { 384 | "version":"2017-02-28", 385 | "operation":"GET", 386 | "path":"/id/post/_search", 387 | "params":{ 388 | "headers":{}, 389 | "queryString":{}, 390 | "body":{ 391 | "from":0, 392 | "size":50 393 | } 394 | } 395 | } 396 | response: > 397 | [ 398 | #foreach($entry in $context.result.hits.hits) 399 | #if( $velocityCount > 1 ) , #end 400 | $utils.toJson($entry.get("_source")) 401 | #end 402 | ] 403 | ``` 404 | 405 | ## Relational Database Resolvers 406 | 407 | ```yml 408 | inputs: 409 | src: ./ 410 | resolvers: 411 | Query: 412 | getPost: 413 | database: my-database 414 | dbClusterIdentifier: arn:aws:rds:us-east-1:123456789123:cluster:my-serverless-aurora-postgres-1 415 | awsSecretStoreArn: arn:aws:secretsmanager:us-east-1:123456789123:secret:rds-db-credentials/cluster-ABCDEFGHI/admin-aBc1e2 416 | relationalDatabaseSourceType: RDS_HTTP_ENDPOINT 417 | schema: public 418 | request: > 419 | { 420 | "version": "2018-05-29", 421 | "statements": [ 422 | $util.toJson("select * from Posts WHERE id='$ctx.args.id'") 423 | ] 424 | } 425 | response: '$utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])' 426 | ``` 427 | 428 | # CLI Reference 429 | 430 | ## deploy 431 | 432 | To deploy, simply run `deploy` from within the directory containing the `serverless.yml` file: 433 | 434 | ``` 435 | serverless deploy 436 | ``` 437 | 438 | If you'd like to know what's happening under the hood and see realtime logs, you can pass the `--debug` flag: 439 | 440 | ``` 441 | serverless deploy --debug 442 | ``` 443 | 444 | 445 | ## dev (dev mode) 446 | 447 | Instead of having to run `serverless deploy` everytime you make changes you wanna test, you can enable **dev mode**, which allows the CLI to watch for changes in your source directory as you develop, and deploy instantly on save. 448 | 449 | To enable dev mode, simply run the following command from within the directory containing the `serverless.yml` file: 450 | 451 | ``` 452 | serverless dev 453 | ``` 454 | 455 | Dev mode also enables live streaming logs from your GraphQL app so that you can see the results of your code changes right away on the CLI as they happen. 456 | 457 | ## info 458 | 459 | Anytime you need to know more about your running GraphQL instance, you can run the following command to view the most critical info: 460 | 461 | ``` 462 | serverless info 463 | ``` 464 | 465 | This is especially helpful when you want to know the outputs of your instances so that you can reference them in another instance. It also shows you the status of your instance, when it was last deployed, how many times it was deployed, and the error message & stack if the latest deployment failed. 466 | 467 | To dig even deeper, you can pass the `--debug` flag to view the state object of your component instance: 468 | 469 | ``` 470 | serverless info --debug 471 | ``` 472 | 473 | ## remove 474 | 475 | If you wanna tear down your entire GraphQL infrastructure that was created during deployment, just run the following command in the directory containing the `serverless.yml` file: 476 | 477 | ``` 478 | serverless remove 479 | ``` 480 | 481 | The GraphQL component will then use all the data it needs from the built-in state storage system to delete only the relavent cloud resources that it created. 482 | 483 | Just like deployment, you could also specify a `--debug` flag for realtime logs from the GraphQL component running in the cloud: 484 | 485 | ``` 486 | serverless remove --debug 487 | ``` 488 | 489 | # Outputs Reference 490 | 491 | ```yml 492 | name: graphql-api-pxzaf135 493 | apiKey: da2-yf444kxlhjerxl376jxyafb2rq 494 | apiId: survbmoad5ewtnm3e3cd7qys4q 495 | url: https://cnbfx5zutbe4fkrtsldsrunbuu.appsync-api.us-east-1.amazonaws.com/graphql 496 | ``` 497 | 498 | # FAQs 499 | 500 | ## How do I add NPM packages to the resolvers? 501 | 502 | You can run `npm init` & `npm install` as you normally would in the directory containing the `resolvers.js` file. This is the root of your app. This entire directory is uploaded to your Lambda function, and you can structure it however you want. Just make sure `resolvers.js` and `schema.graphql` are in the root of the directory. 503 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "jest": "^25.1.0", 4 | "babel-eslint": "9.0.0", 5 | "eslint": "5.6.0", 6 | "eslint-config-prettier": "^3.6.0", 7 | "eslint-plugin-import": "^2.20.0", 8 | "eslint-plugin-prettier": "^3.1.0", 9 | "prettier": "^1.18.2" 10 | }, 11 | "dependencies": { 12 | "@serverless/aws-sdk": "^2.0.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | printWidth: 100, 4 | semi: false, 5 | singleQuote: true, 6 | tabWidth: 2, 7 | trailingComma: 'none' 8 | } 9 | -------------------------------------------------------------------------------- /serverless.component.yml: -------------------------------------------------------------------------------- 1 | name: graphql 2 | version: 3.0.4 3 | author: eahefnawy 4 | org: serverlessinc 5 | description: Create a GraphQL API with AWS AppSync 6 | keywords: aws, serverless, appsync, graphql 7 | repo: https://github.com/owner/project 8 | readme: '' 9 | license: MIT 10 | main: ./src 11 | 12 | types: 13 | providers: 14 | - aws 15 | -------------------------------------------------------------------------------- /src/_src/resolvers.js: -------------------------------------------------------------------------------- 1 | const Query = { 2 | getPost: async ({ id }) => { 3 | return { id } 4 | } 5 | } 6 | 7 | const Mutation = { 8 | createPost: async ({ id }) => { 9 | return { id } 10 | } 11 | } 12 | 13 | module.exports = { Query, Mutation } 14 | -------------------------------------------------------------------------------- /src/_src/schema.graphql: -------------------------------------------------------------------------------- 1 | type Post { 2 | id: ID! 3 | } 4 | 5 | type Query { 6 | getPost(id: ID!): Post 7 | } 8 | 9 | type Mutation { 10 | createPost(id: ID!): Post 11 | } 12 | 13 | schema { 14 | query: Query 15 | mutation: Mutation 16 | } 17 | -------------------------------------------------------------------------------- /src/_src/serverless.yml: -------------------------------------------------------------------------------- 1 | component: graphql 2 | name: graphql-api 3 | 4 | inputs: 5 | src: ./ 6 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@serverless/aws-sdk-extra": "^1.2.5" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/serverless.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const crypto = require('crypto') 3 | const fs = require('fs') 4 | const { Component } = require('@serverless/core') 5 | const AWS = require('@serverless/aws-sdk-extra') 6 | 7 | const generateId = () => 8 | Math.random() 9 | .toString(36) 10 | .substring(6) 11 | 12 | const fileExists = async (filePath) => { 13 | try { 14 | await fs.promises.access(filePath) 15 | return true 16 | } catch (e) { 17 | return false 18 | } 19 | } 20 | 21 | const checksum = (data) => { 22 | return crypto 23 | .createHash('sha256') 24 | .update(data) 25 | .digest('hex') 26 | } 27 | 28 | const log = (msg) => console.log(msg) // eslint-disable-line 29 | 30 | const getSchema = async (sourceDirectory, schemaFilename) => { 31 | const schemaFilePath = path.join(sourceDirectory, schemaFilename) 32 | const schemaFileExists = await fileExists(schemaFilePath) 33 | 34 | // make sure schema file exists 35 | if (!schemaFileExists) { 36 | throw new Error(`The "${schemaFilename}" file was not found in your source directory`) 37 | } 38 | 39 | return fs.promises.readFile(path.join(sourceDirectory, schemaFilename), 'utf-8') 40 | } 41 | 42 | class GraphQL extends Component { 43 | async deploy(inputs = {}) { 44 | inputs.region = inputs.region || 'us-east-1' 45 | inputs.name = inputs.name || this.name 46 | this.state.region = inputs.region 47 | this.state.name = this.state.name || `${inputs.name}-${generateId()}` 48 | 49 | if (inputs.apiId) { 50 | this.state.shouldDeployAppSync = false 51 | this.state.apiId = inputs.apiId 52 | } else { 53 | this.state.shouldDeployAppSync = true 54 | } 55 | 56 | const extras = new AWS.Extras({ 57 | credentials: this.credentials.aws, 58 | region: inputs.region 59 | }) 60 | 61 | const sourceDirectory = await this.unzip(inputs.src) 62 | 63 | // add default app if src is not provided 64 | if (!inputs.src) { 65 | // make sure source directory exists 66 | if (!fs.existsSync(sourceDirectory)) { 67 | fs.mkdirSync(sourceDirectory) 68 | } 69 | fs.copyFileSync( 70 | path.join(__dirname, '_src', 'resolvers.js'), 71 | path.join(sourceDirectory, 'resolvers.js') 72 | ) 73 | 74 | fs.copyFileSync( 75 | path.join(__dirname, '_src', 'schema.graphql'), 76 | path.join(sourceDirectory, 'schema.graphql') 77 | ) 78 | 79 | fs.copyFileSync( 80 | path.join(__dirname, '_src', 'serverless.yml'), 81 | path.join(sourceDirectory, 'serverless.yml') 82 | ) 83 | } 84 | 85 | const schema = this.state.shouldDeployAppSync 86 | ? await getSchema(sourceDirectory, 'schema.graphql') 87 | : null 88 | 89 | log(`Deploying "${this.state.name}" to the "${this.state.region}" region.`) 90 | 91 | const resolversFilePath = path.join(sourceDirectory, 'resolvers.js') 92 | const resolversFileExists = await fileExists(resolversFilePath) 93 | 94 | inputs.resolvers = inputs.resolvers || {} 95 | let shouldDeployLambda = false 96 | if (resolversFileExists) { 97 | const lambdaResolvers = require(path.join(sourceDirectory, 'resolvers.js')) 98 | for (const type in lambdaResolvers) { 99 | if (typeof lambdaResolvers[type] !== 'object') { 100 | throw new Error(`type "${type}" in resolvers.js must be an object.`) 101 | } 102 | 103 | for (const field in lambdaResolvers[type]) { 104 | if (typeof lambdaResolvers[type][field] !== 'function') { 105 | throw new Error(`resolver "${type}.${field}" in resolvers.js must be a function.`) 106 | } 107 | 108 | inputs.resolvers[type] = inputs.resolvers[type] || {} 109 | // todo throw error if user defined the same resolver in js and yaml file? 110 | shouldDeployLambda = true 111 | inputs.resolvers[type][field] = { 112 | lambda: this.state.name 113 | } 114 | } 115 | } 116 | } 117 | 118 | for (const type in inputs.resolvers) { 119 | if (typeof inputs.resolvers[type] !== 'object') { 120 | throw new Error(`resolver type "${type}" in serverless.yml must be an object.`) 121 | } 122 | 123 | for (const field in inputs.resolvers[type]) { 124 | const resolver = inputs.resolvers[type][field] 125 | if (typeof resolver !== 'object') { 126 | throw new Error(`resolver "${type}.${field}" in serverless.yml must be an object.`) 127 | } 128 | 129 | // throw error if not a lambda resolver and user 130 | // did not define request template (there's a default response template) 131 | if (!resolver.lambda && !resolver.request) { 132 | throw new Error(`Missing request property for resolver "${type}.${field}".`) 133 | } 134 | 135 | if (resolver.request) { 136 | const requestTemplateAbsolutePath = path.resolve(sourceDirectory, resolver.request) 137 | 138 | if (await fileExists(requestTemplateAbsolutePath)) { 139 | inputs.resolvers[type][field].request = await fs.promises.readFile( 140 | requestTemplateAbsolutePath, 141 | 'utf-8' 142 | ) 143 | } 144 | } 145 | if (resolver.response) { 146 | const responseTemplateAbsolutePath = path.resolve(sourceDirectory, resolver.response) 147 | 148 | if (await fileExists(responseTemplateAbsolutePath)) { 149 | inputs.resolvers[type][field].response = await fs.promises.readFile( 150 | responseTemplateAbsolutePath, 151 | 'utf-8' 152 | ) 153 | } 154 | } 155 | } 156 | } 157 | 158 | log(`Deploying Role "${this.state.name}" to the "${this.state.region}" region.`) 159 | const deployRoleParams = { 160 | roleName: this.state.name, 161 | service: [`lambda.amazonaws.com`, `appsync.amazonaws.com`], 162 | policy: [ 163 | { 164 | Effect: 'Allow', 165 | Action: ['sts:AssumeRole'], 166 | Resource: '*' 167 | }, 168 | { 169 | Effect: 'Allow', 170 | Action: ['logs:CreateLogGroup', 'logs:CreateLogStream'], 171 | Resource: '*' 172 | }, 173 | { 174 | Effect: 'Allow', 175 | Action: [ 176 | 'logs:CreateLogGroup', 177 | 'logs:CreateLogStream', 178 | 'logs:PutLogEvents', 179 | 'ec2:CreateNetworkInterface', 180 | 'ec2:DescribeNetworkInterfaces', 181 | 'ec2:DeleteNetworkInterface' 182 | ], 183 | Resource: '*' 184 | } 185 | ] 186 | } 187 | 188 | // get the minimum policy needed for the defined resolvers 189 | const resolversPolicy = await extras.getAppSyncResolversPolicy(inputs.resolvers) 190 | deployRoleParams.policy = deployRoleParams.policy.concat(resolversPolicy) 191 | 192 | // add any other policy statements provided by the user 193 | if (inputs.policy instanceof Array) { 194 | deployRoleParams.policy = deployRoleParams.policy.concat(inputs.policy) 195 | } 196 | 197 | // deploy role 198 | const { roleArn } = await extras.deployRole(deployRoleParams) 199 | 200 | // if there's a resolvers.js, then we should deploy the built in lambda 201 | if (shouldDeployLambda) { 202 | log(`Deploying Lambda "${this.state.name}" to the "${this.state.region}" region.`) 203 | 204 | // inject handler 205 | fs.copyFileSync( 206 | path.join(__dirname, 'userLambdaHandler.js'), 207 | path.join(sourceDirectory, 'handler.js') 208 | ) 209 | 210 | const handler = await this.addSDK(sourceDirectory, 'handler.handler') 211 | const zipPath = await this.zip(sourceDirectory) 212 | 213 | const deployLambdaParams = { 214 | lambdaName: this.state.name, 215 | description: inputs.description, 216 | handler, 217 | memory: inputs.memory, 218 | timeout: inputs.timeout, 219 | env: inputs.env, 220 | layers: inputs.layers, 221 | vpcConfig: inputs.vpcConfig, 222 | roleArn, 223 | lambdaSrc: zipPath 224 | } 225 | await extras.deployLambda(deployLambdaParams) 226 | } 227 | 228 | const outputs = { 229 | name: this.state.name, 230 | apiId: this.state.apiId 231 | } 232 | 233 | if (this.state.shouldDeployAppSync) { 234 | log(`Deploying AppSync API "${this.state.name}" to the "${this.state.region}" region.`) 235 | const deployAppSyncApiParams = { 236 | apiName: this.state.name, 237 | auth: inputs.auth 238 | } 239 | 240 | if (this.state.apiId) { 241 | deployAppSyncApiParams.apiId = this.state.apiId 242 | } 243 | 244 | const { apiId, apiUrls } = await extras.deployAppSyncApi(deployAppSyncApiParams) 245 | this.state.apiId = apiId 246 | this.state.apiUrls = apiUrls 247 | outputs.apiId = apiId 248 | outputs.url = apiUrls.GRAPHQL // there's also REALTIME URL. dont know what that is 249 | 250 | const schemaChecksum = checksum(schema) 251 | if (schemaChecksum !== this.state.schemaChecksum) { 252 | log(`Deploying schema for AppSync API with ID "${apiId}".`) 253 | const deployAppSyncSchemaParams = { 254 | apiId, 255 | schema 256 | } 257 | await extras.deployAppSyncSchema(deployAppSyncSchemaParams) 258 | this.state.schemaChecksum = schemaChecksum 259 | } 260 | } 261 | 262 | const resolversChecksum = checksum(JSON.stringify(inputs.resolvers)) 263 | if (resolversChecksum !== this.state.resolversChecksum) { 264 | log(`Deploying resolvers for AppSync API with ID "${this.state.apiId}".`) 265 | const deployAppSyncResolversParams = { 266 | apiId: this.state.apiId, 267 | roleName: this.state.name, 268 | resolvers: inputs.resolvers 269 | } 270 | await extras.deployAppSyncResolvers(deployAppSyncResolversParams) 271 | this.state.resolversChecksum = resolversChecksum 272 | } 273 | 274 | // deploy api key if auth config is api key 275 | if ((!inputs.auth || inputs.auth === 'apiKey') && this.state.shouldDeployAppSync) { 276 | log(`Deploying api key for AppSync API with ID "${this.state.apiId}".`) 277 | // if api key not in state, a new one will be created 278 | // if it is in state, it will be verified on the provider 279 | // and a new one will be created if no longer exists 280 | const deployAppSyncApiKeyParams = { 281 | apiId: this.state.apiId, 282 | apiKey: this.state.apiKey, 283 | description: inputs.description 284 | } 285 | const { apiKey } = await extras.deployAppSyncApiKey(deployAppSyncApiKeyParams) 286 | this.state.apiKey = apiKey 287 | 288 | outputs.apiKey = apiKey 289 | } 290 | 291 | // deploy distribution and domain if configured 292 | if (inputs.domain && this.state.shouldDeployAppSync) { 293 | log( 294 | `Deploying CloudFront Distribution for AppSync API with URL "${this.state.apiUrls.GRAPHQL}".` 295 | ) 296 | const deployAppSyncDistributionParams = { 297 | apiId: this.state.apiId, 298 | apiUrl: this.state.apiUrls.GRAPHQL, 299 | domain: inputs.domain 300 | } 301 | 302 | if (this.state.distributionId) { 303 | deployAppSyncDistributionParams.distributionId = this.state.distributionId 304 | } 305 | const { distributionId, distributionUrl } = await extras.deployAppSyncDistribution( 306 | deployAppSyncDistributionParams 307 | ) 308 | 309 | this.state.domain = inputs.domain 310 | this.state.distributionId = distributionId 311 | this.state.distributionUrl = distributionUrl 312 | outputs.domain = `https://${this.state.domain}/graphql` 313 | } 314 | 315 | // remove default lambda if no longer configured 316 | if (!shouldDeployLambda && this.state.shouldDeployLambda) { 317 | log(`Removing Lambda "${this.state.name}" from the "${this.state.region}" region.`) 318 | await extras.removeLambda({ lambdaName: this.state.name }) 319 | } 320 | 321 | // keep in state the fact that we deployed a lambda 322 | // so that we could remove it if we have to later on 323 | this.state.shouldDeployLambda = shouldDeployLambda 324 | 325 | log(`Successfully deployed "${this.state.name}" to the "${this.state.region}" region.`) 326 | 327 | return outputs 328 | } 329 | 330 | async remove() { 331 | if (!this.state.name) { 332 | log(`State is empty. Aborting removal.`) 333 | return 334 | } 335 | 336 | const extras = new AWS.Extras({ 337 | credentials: this.credentials.aws, 338 | region: this.state.region || 'us-east-1' 339 | }) 340 | 341 | const removeRoleParams = { 342 | roleName: this.state.name 343 | } 344 | 345 | const removeLambdaParams = { 346 | lambdaName: this.state.name 347 | } 348 | 349 | const removeAppSyncApiParams = { 350 | apiId: this.state.apiId 351 | } 352 | 353 | log(`Removing Role "${this.state.name}" from the "${this.state.region}" region.`) 354 | log(`Removing Lambda "${this.state.name}" from the "${this.state.region}" region.`) 355 | 356 | const promises = [extras.removeRole(removeRoleParams), extras.removeLambda(removeLambdaParams)] 357 | 358 | if (this.state.shouldDeployAppSync) { 359 | log(`Removing AppSync API "${this.state.apiId}" from the "${this.state.region}" region.`) 360 | promises.push(extras.removeAppSyncApi(removeAppSyncApiParams)) 361 | } 362 | 363 | if (this.state.domain) { 364 | log( 365 | `Removing AppSync Distribution "${this.state.distributionId}" from the "${this.state.region}" region.` 366 | ) 367 | 368 | const removeDistribution = { 369 | distributionId: this.state.distributionId, 370 | domain: this.state.domain 371 | } 372 | 373 | promises.push(extras.removeDistribution(removeDistribution)) 374 | } 375 | 376 | await Promise.all(promises) 377 | 378 | log(`Successfully removed "${this.state.name}" from the "${this.state.region}" region.`) 379 | 380 | this.state = {} 381 | } 382 | } 383 | 384 | module.exports = GraphQL 385 | -------------------------------------------------------------------------------- /src/userLambdaHandler.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | 4 | const fileExists = async (filePath) => { 5 | try { 6 | await fs.promises.access(filePath) 7 | return true 8 | } catch (e) { 9 | return false 10 | } 11 | } 12 | 13 | module.exports.handler = async (event) => { 14 | const resolversFilePath = path.join(__dirname, 'resolvers.js') 15 | 16 | if (!(await fileExists(resolversFilePath))) { 17 | throw new Error(`The "resolvers.js" file was not found in your source directory`) 18 | } 19 | 20 | const resolvers = require('./resolvers') 21 | const { parentTypeName, fieldName } = event.info 22 | 23 | // these validation errors will likely never run because they're covered 24 | // by component validation. But I'm leaving them just in case 25 | if (!resolvers[parentTypeName]) { 26 | throw new Error(`The "${parentTypeName}" type is not exported in resolvers.js`) 27 | } 28 | 29 | if (!resolvers[parentTypeName][fieldName]) { 30 | throw new Error( 31 | `Resolver "${fieldName}" for type "${parentTypeName}" is not exported in resolvers.js` 32 | ) 33 | } 34 | 35 | if (typeof resolvers[parentTypeName][fieldName] !== 'function') { 36 | throw new Error(`Resolver "${fieldName}" for type "${parentTypeName}" must be a function`) 37 | } 38 | 39 | // todo how do variables work in graphql? 40 | return resolvers[parentTypeName][fieldName](event.arguments, event) 41 | } 42 | -------------------------------------------------------------------------------- /templates/graphql-starter/resolvers.js: -------------------------------------------------------------------------------- 1 | const Query = { 2 | // resolver for field getPost in type Query 3 | getPost: async ({ id }) => { 4 | return { id }; 5 | }, 6 | }; 7 | 8 | const Mutation = { 9 | // resolver for field createPost in type Mutation 10 | createPost: async ({ id }) => { 11 | return { id }; 12 | }, 13 | }; 14 | 15 | module.exports = { Query, Mutation }; 16 | -------------------------------------------------------------------------------- /templates/graphql-starter/schema.graphql: -------------------------------------------------------------------------------- 1 | type Post { 2 | id: ID! 3 | } 4 | 5 | type Query { 6 | getPost(id: ID!): Post 7 | } 8 | 9 | type Mutation { 10 | createPost(id: ID!): Post 11 | } 12 | 13 | schema { 14 | query: Query 15 | mutation: Mutation 16 | } 17 | -------------------------------------------------------------------------------- /templates/graphql-starter/serverless.template.yml: -------------------------------------------------------------------------------- 1 | name: graphql-starter 2 | org: serverlessinc 3 | description: Deploys an serverless GraphQL API with AWS AppSync 4 | keywords: aws, serverless, graphql, appsync 5 | repo: https://github.com/serverless-components/graphql 6 | license: MIT -------------------------------------------------------------------------------- /templates/graphql-starter/serverless.yml: -------------------------------------------------------------------------------- 1 | component: graphql 2 | name: graphql-starter 3 | org: serverlessinc 4 | 5 | inputs: 6 | src: ./ 7 | --------------------------------------------------------------------------------