|
13 | 13 | --- |
14 | 14 | <!--END STABILITY BANNER--> |
15 | 15 |
|
16 | | -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. |
| 16 | +The `@aws-cdk/aws-appsync` package contains constructs for building flexible |
| 17 | +APIs that use GraphQL. |
17 | 18 |
|
18 | | -## Usage Example |
| 19 | +### Example |
19 | 20 |
|
20 | | -Given the following GraphQL schema file `schema.graphql`: |
| 21 | +Example of a GraphQL API with `AWS_IAM` authorization resolving into a DynamoDb |
| 22 | +backend data source. |
21 | 23 |
|
22 | | -```graphql |
23 | | -type ServiceVersion { |
24 | | - version: String! |
25 | | -} |
| 24 | +GraphQL schema file `schema.graphql`: |
26 | 25 |
|
27 | | -type Customer { |
28 | | - id: String! |
29 | | - name: String! |
| 26 | +```gql |
| 27 | +type demo { |
| 28 | + id: String! |
| 29 | + version: String! |
30 | 30 | } |
31 | | - |
32 | | -input SaveCustomerInput { |
33 | | - name: String! |
| 31 | +type Query { |
| 32 | + getDemos: [ test! ] |
34 | 33 | } |
35 | | - |
36 | | -type Order { |
37 | | - customer: String! |
38 | | - order: String! |
| 34 | +input DemoInput { |
| 35 | + version: String! |
39 | 36 | } |
40 | | - |
41 | | -type Query { |
42 | | - getServiceVersion: ServiceVersion |
43 | | - getCustomers: [Customer] |
44 | | - getCustomer(id: String): Customer |
| 37 | +type Mutation { |
| 38 | + addDemo(input: DemoInput!): demo |
45 | 39 | } |
| 40 | +``` |
46 | 41 |
|
47 | | -input FirstOrderInput { |
48 | | - product: String! |
49 | | - quantity: Int! |
50 | | -} |
| 42 | +CDK stack file `app-stack.ts`: |
51 | 43 |
|
| 44 | +```ts |
| 45 | +import * as appsync from '@aws-cdk/aws-appsync'; |
| 46 | +import * as db from '@aws-cdk/aws-dynamodb'; |
| 47 | + |
| 48 | +const api = new appsync.GraphQLApi(stack, 'Api', { |
| 49 | + name: 'demo', |
| 50 | + schemaDefinitionFile: join(__dirname, 'schema.graphql'), |
| 51 | + authorizationConfig: { |
| 52 | + defaultAuthorization: { |
| 53 | + authorizationType: appsync.AuthorizationType.IAM |
| 54 | + }, |
| 55 | + }, |
| 56 | +}); |
| 57 | + |
| 58 | +const demoTable = new db.Table(stack, 'DemoTable', { |
| 59 | + partitionKey: { |
| 60 | + name: 'id', |
| 61 | + type: AttributeType.STRING, |
| 62 | + }, |
| 63 | +}); |
| 64 | + |
| 65 | +const demoDS = api.addDynamoDbDataSource('demoDataSource', 'Table for Demos"', demoTable); |
| 66 | +
|
| 67 | +// Resolver for the Query "getDemos" that scans the DyanmoDb table and returns the entire list. |
| 68 | +demoDS.createResolver({ |
| 69 | + typeName: 'Query', |
| 70 | + fieldName: 'getDemos', |
| 71 | + requestMappingTemplate: MappingTemplate.dynamoDbScanTable(), |
| 72 | + responseMappingTemplate: MappingTemplate.dynamoDbResultList(), |
| 73 | +}); |
| 74 | +
|
| 75 | +// Resolver for the Mutation "addDemo" that puts the item into the DynamoDb table. |
| 76 | +demoDS.createResolver({ |
| 77 | + typeName: 'Mutation', |
| 78 | + fieldName: 'addDemo', |
| 79 | + requestMappingTemplate: MappingTemplate.dynamoDbPutItem(PrimaryKey.partition('id').auto(), Values.projecting('demo')), |
| 80 | + responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), |
| 81 | +}); |
| 82 | +``` |
| 83 | +
|
| 84 | +## Permissions |
| 85 | +
|
| 86 | +When using `AWS_IAM` as the authorization type for GraphQL API, an IAM Role |
| 87 | +with correct permissions must be used for access to API. |
| 88 | +
|
| 89 | +When configuring permissions, you can specify specific resources to only be |
| 90 | +accessible by `IAM` authorization. For example, if you want to only allow mutability |
| 91 | +for `IAM` authorized access you would configure the following. |
| 92 | +
|
| 93 | +In `schema.graphql`: |
| 94 | +```ts |
52 | 95 | type Mutation { |
53 | | - addCustomer(customer: SaveCustomerInput!): Customer |
54 | | - saveCustomer(id: String!, customer: SaveCustomerInput!): Customer |
55 | | - removeCustomer(id: String!): Customer |
56 | | - saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order |
57 | | - doPostOnAws: String! |
| 96 | + updateExample(...): ... |
| 97 | + @aws_iam |
58 | 98 | } |
59 | 99 | ``` |
60 | 100 |
|
61 | | -the following CDK app snippet will create a complete CRUD AppSync API: |
| 101 | +In `IAM`: |
| 102 | +```json |
| 103 | +{ |
| 104 | + "Version": "2012-10-17", |
| 105 | + "Statement": [ |
| 106 | + { |
| 107 | + "Effect": "Allow", |
| 108 | + "Action": [ |
| 109 | + "appsync:GraphQL" |
| 110 | + ], |
| 111 | + "Resource": [ |
| 112 | + "arn:aws:appsync:REGION:ACCOUNT_ID:apis/GRAPHQL_ID/types/Mutation/fields/updateExample" |
| 113 | + ] |
| 114 | + } |
| 115 | + ] |
| 116 | +} |
| 117 | +``` |
| 118 | +
|
| 119 | +See [documentation](https://docs.aws.amazon.com/appsync/latest/devguide/security.html#aws-iam-authorization) for more details. |
| 120 | +
|
| 121 | +To make this easier, CDK provides `grant` API. |
| 122 | +
|
| 123 | +Use the `grant` function for more granular authorization. |
62 | 124 |
|
63 | 125 | ```ts |
64 | | -export class ApiStack extends Stack { |
65 | | - constructor(scope: Construct, id: string) { |
66 | | - super(scope, id); |
67 | | - |
68 | | - const userPool = new UserPool(this, 'UserPool', { |
69 | | - userPoolName: 'myPool', |
70 | | - }); |
71 | | - |
72 | | - const api = new GraphQLApi(this, 'Api', { |
73 | | - name: `demoapi`, |
74 | | - logConfig: { |
75 | | - fieldLogLevel: FieldLogLevel.ALL, |
76 | | - }, |
77 | | - authorizationConfig: { |
78 | | - defaultAuthorization: { |
79 | | - authorizationType: AuthorizationType.USER_POOL, |
80 | | - userPoolConfig: { |
81 | | - userPool, |
82 | | - defaultAction: UserPoolDefaultAction.ALLOW |
83 | | - }, |
84 | | - }, |
85 | | - additionalAuthorizationModes: [ |
86 | | - { |
87 | | - authorizationType: AuthorizationType.API_KEY, |
88 | | - } |
89 | | - ], |
90 | | - }, |
91 | | - schemaDefinitionFile: './schema.graphql', |
92 | | - }); |
93 | | - |
94 | | - const noneDS = api.addNoneDataSource('None', 'Dummy data source'); |
95 | | - |
96 | | - noneDS.createResolver({ |
97 | | - typeName: 'Query', |
98 | | - fieldName: 'getServiceVersion', |
99 | | - requestMappingTemplate: MappingTemplate.fromString(JSON.stringify({ |
100 | | - version: '2017-02-28', |
101 | | - })), |
102 | | - responseMappingTemplate: MappingTemplate.fromString(JSON.stringify({ |
103 | | - version: 'v1', |
104 | | - })), |
105 | | - }); |
106 | | - |
107 | | - const customerTable = new Table(this, 'CustomerTable', { |
108 | | - billingMode: BillingMode.PAY_PER_REQUEST, |
109 | | - partitionKey: { |
110 | | - name: 'id', |
111 | | - type: AttributeType.STRING, |
112 | | - }, |
113 | | - }); |
114 | | - // If your table is already created you can also use use import table and use it as data source. |
115 | | - const customerDS = api.addDynamoDbDataSource('Customer', 'The customer data source', customerTable); |
116 | | - customerDS.createResolver({ |
117 | | - typeName: 'Query', |
118 | | - fieldName: 'getCustomers', |
119 | | - requestMappingTemplate: MappingTemplate.dynamoDbScanTable(), |
120 | | - responseMappingTemplate: MappingTemplate.dynamoDbResultList(), |
121 | | - }); |
122 | | - customerDS.createResolver({ |
123 | | - typeName: 'Query', |
124 | | - fieldName: 'getCustomer', |
125 | | - requestMappingTemplate: MappingTemplate.dynamoDbGetItem('id', 'id'), |
126 | | - responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), |
127 | | - }); |
128 | | - customerDS.createResolver({ |
129 | | - typeName: 'Mutation', |
130 | | - fieldName: 'addCustomer', |
131 | | - requestMappingTemplate: MappingTemplate.dynamoDbPutItem( |
132 | | - PrimaryKey.partition('id').auto(), |
133 | | - Values.projecting('customer')), |
134 | | - responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), |
135 | | - }); |
136 | | - customerDS.createResolver({ |
137 | | - typeName: 'Mutation', |
138 | | - fieldName: 'saveCustomer', |
139 | | - requestMappingTemplate: MappingTemplate.dynamoDbPutItem( |
140 | | - PrimaryKey.partition('id').is('id'), |
141 | | - Values.projecting('customer')), |
142 | | - responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), |
143 | | - }); |
144 | | - customerDS.createResolver({ |
145 | | - typeName: 'Mutation', |
146 | | - fieldName: 'saveCustomerWithFirstOrder', |
147 | | - requestMappingTemplate: MappingTemplate.dynamoDbPutItem( |
148 | | - PrimaryKey |
149 | | - .partition('order').auto() |
150 | | - .sort('customer').is('customer.id'), |
151 | | - Values |
152 | | - .projecting('order') |
153 | | - .attribute('referral').is('referral')), |
154 | | - responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), |
155 | | - }); |
156 | | - customerDS.createResolver({ |
157 | | - typeName: 'Mutation', |
158 | | - fieldName: 'removeCustomer', |
159 | | - requestMappingTemplate: MappingTemplate.dynamoDbDeleteItem('id', 'id'), |
160 | | - responseMappingTemplate: MappingTemplate.dynamoDbResultItem(), |
161 | | - }); |
162 | | - |
163 | | - const httpDS = api.addHttpDataSource('http', 'The http data source', 'https://aws.amazon.com/'); |
164 | | - |
165 | | - httpDS.createResolver({ |
166 | | - typeName: 'Mutation', |
167 | | - fieldName: 'doPostOnAws', |
168 | | - requestMappingTemplate: MappingTemplate.fromString(`{ |
169 | | - "version": "2018-05-29", |
170 | | - "method": "POST", |
171 | | - # if full path is https://api.xxxxxxxxx.com/posts then resourcePath would be /posts |
172 | | - "resourcePath": "/path/123", |
173 | | - "params":{ |
174 | | - "body": $util.toJson($ctx.args), |
175 | | - "headers":{ |
176 | | - "Content-Type": "application/json", |
177 | | - "Authorization": "$ctx.request.headers.Authorization" |
178 | | - } |
179 | | - } |
180 | | - }`), |
181 | | - responseMappingTemplate: MappingTemplate.fromString(` |
182 | | - ## Raise a GraphQL field error in case of a datasource invocation error |
183 | | - #if($ctx.error) |
184 | | - $util.error($ctx.error.message, $ctx.error.type) |
185 | | - #end |
186 | | - ## if the response status code is not 200, then return an error. Else return the body ** |
187 | | - #if($ctx.result.statusCode == 200) |
188 | | - ## If response is 200, return the body. |
189 | | - $ctx.result.body |
190 | | - #else |
191 | | - ## If response is not 200, append the response to error block. |
192 | | - $utils.appendError($ctx.result.body, "$ctx.result.statusCode") |
193 | | - #end |
194 | | - `), |
195 | | - }); |
196 | | - } |
197 | | -} |
| 126 | +const role = new iam.Role(stack, 'Role', { |
| 127 | + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), |
| 128 | +}); |
| 129 | +const api = new appsync.GraphQLApi(stack, 'API', { |
| 130 | + definition |
| 131 | +}); |
| 132 | +
|
| 133 | +api.grant(role, appsync.IamResource.custom('types/Mutation/fields/updateExample'), 'appsync:GraphQL') |
198 | 134 | ``` |
| 135 | +
|
| 136 | +### IamResource |
| 137 | +
|
| 138 | +In order to use the `grant` functions, you need to use the class `IamResource`. |
| 139 | +
|
| 140 | +- `IamResource.custom(...arns)` permits custom ARNs and requires an argument. |
| 141 | +
|
| 142 | +- `IamResouce.ofType(type, ...fields)` permits ARNs for types and their fields. |
| 143 | +
|
| 144 | +- `IamResource.all()` permits ALL resources. |
| 145 | +
|
| 146 | +### Generic Permissions |
| 147 | +
|
| 148 | +Alternatively, you can use more generic `grant` functions to accomplish the same usage. |
| 149 | +
|
| 150 | +These include: |
| 151 | +- grantMutation (use to grant access to Mutation fields) |
| 152 | +- grantQuery (use to grant access to Query fields) |
| 153 | +- grantSubscription (use to grant access to Subscription fields) |
| 154 | +
|
| 155 | +```ts |
| 156 | +// For generic types |
| 157 | +api.grantMutation(role, 'updateExample'); |
| 158 | +
|
| 159 | +// For custom types and granular design |
| 160 | +api.grant(role, appsync.IamResource.ofType('Mutation', 'updateExample'), 'appsync:GraphQL'); |
| 161 | +``` |
0 commit comments