How to invoke a mutation with AppSync's HTTP data source

Call a mutation from anywhere in AppSync

Author's image
Tamás Sallai
4 mins

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.

October 11, 2022
In this article