How to invoke a mutation with AppSync's HTTP data source
Call a mutation from anywhere in AppSync
Calling mutations from AppSync
If you use a notify mutation to trigger a subscription you often need to call a mutation from some part of the AppSync API. This is because a subscription is linked to mutations and a subscription event is generated only when that mutation is called.
For example, instead of linking the todo
subscription to addTodo
, link it to notifyTodo
. Then when there is a change to a Todo
item,
invoke the notifyTodo
mutation.
type Mutation {
addTodo(userId: ID!, name: String!): Todo!
notifyTodo(id: ID!): TodoEvent!
}
type Subscription {
todo(userId: ID, groupId: ID): TodoEvent
@aws_subscribe(mutations: ["notifyTodo"])
}
This is useful as the TodoEvent
can be different than a Todo
, allowing custom filtering on the subscription. This is because AppSync can only
filter on the top-level fields of a subscription event, so you need to move up all fields you want to enable filtering on.
In the above example, a Todo
has a User
which has a Group
field:
type Todo {
id: ID!
name: String!
checked: Boolean!
created: AWSDateTime!
user: User!
}
type User {
id: ID!
name: String!
todos: [Todo!]!
}
type Group {
name: String!
users: [User!]!
}
To allow filtering Todo
events for a user or a group, we need the event to have the userId
and the groupId
as a top-level field:
type TodoEvent {
userId: ID!
groupId: ID!
todo: Todo!
}
But then, whenever a Todo
item changes (for example, via the addTodo
mutation) we need to call the notifyTodo
.
In this article we'll look into how to implement that using the HTTP data source. An advantage to use that is that it does not rely on a Lambda function, everything is handled by AppSync.
Permissions
The HTTP data source can add an AWS signature to outgoing requests. This relies on an IAM Role and policies attached to that role. In essence, AppSync needs permission to call the target API, even if it's calling itself.
data "aws_iam_policy_document" "appsync" {
statement {
actions = [
"appsync:GraphQL",
]
resources = [
"${aws_appsync_graphql_api.appsync.arn}/types/Mutation/fields/notifyTodo"
]
}
}
This policy gives very limited permissions: it allows calling only a single mutation.
Data source
Next, configure the data source:
resource "aws_appsync_datasource" "notifyTodo" {
api_id = aws_appsync_graphql_api.appsync.id
name = "notifyTodo"
service_role_arn = aws_iam_role.appsync.arn
type = "HTTP"
http_config {
endpoint = regex("^[^/]+//[^/]+", aws_appsync_graphql_api.appsync.uris["GRAPHQL"])
authorization_config {
aws_iam_config {
signing_region = data.aws_region.current.name
signing_service_name = "appsync"
}
}
}
}
The endpoint is the AppSync endpoint host. Then the region is where the target API is, in this case it's the current region. Then the signing_service_name
is appsync
as the request is going to AppSync.
Resolver
Then the resolver defines the other aspects of the HTTP request. It's a bit tricky as it needs to combine GraphQL with HTTP:
{
"version": "2018-05-29",
"method": "POST",
"params": {
"query": {},
"headers": {
"Content-Type" : "application/json"
},
"body": $util.toJson({
"query": "mutation notifyTodo($id: ID!) {
notifyTodo(id: $id) {
userId
groupId
todo {
id
name
checked
created
}
}
}",
"operationName": 'notifyTodo',
"variables": {"id": $ctx.prev.result.id}
})
},
"resourcePath": "/graphql"
}
Let's start with the easy things!
The method is POST
as all GraphQL queries are HTTP POST requests. The Content-Type
is application/json
as we expect a JSON response. Then the
resourcePath
is /graphql
as that's where AppSync exposes its API.
Then the body is the GraphQL part. Here, the operationName
defines which part to call from the query. Then the variables
defines values for the
placeholders.
Finally, the query
defines the actual GraphQL operation. The top-level name must match the operationName
, then it needs to use the variables
defined. Here, it maps the id
to $id
.
Then the inside of the operation is which mutation to call (notifyTodo
) and what values to get for the result object. Since only the fields defined here
will be available in the subscription event, this is the place to define what clients can get.
Response mapping template
Then process the response:
#set($result = $util.parseJson($ctx.result.body))
#if ($result.errors)
$util.error($result.errors[0].message)
#end
$util.toJson($ctx.prev.result)
Note the special error handling here. GraphQL by design allows partial responses so you can't rely on the status code to see if anything went wrong. Instead,
there is an errors
field in the response that lists the problematic fields. The response mapping template checks this and throws an error if the response
contains any.
Conclusion
The HTTP data source allows sending GraphQL requsts to the same or different AppSync API. This provides an easy but efficient way to call a mutation which in turn can trigger a subscription event.