├── .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 |
--------------------------------------------------------------------------------