You are viewing the preview version of this book
Click here for the full version.

Notify subscription pattern

Try it yourself

You can find code example for this chapter here.

In the previous chapters we've seen how subscriptions work, where the fields available for them are defined, and how to implement event filtering. Now you probably have the impression that it's relatively easy to implement real-time notifications that push data to clients whenever they need and keep them up-to-date with all changes. This is also the point where the AWS documentation and most tutorials stop.

Unfortunately, subscriptions implemented like this are hardly usable for any realistic scenario as they miss a lot of practical requirements.

First, filtering can be done only on top-level fields of the return type. In the previous examples, we used filters on the Todo items, but that's usually not what you want. When a user logs in to the app, it is interested in the new Todo items that is assigned to them. For this, we would need to move from the Todo to the User and filter by the User.id field. And that is not possible with AppSync.

Second, subscriptions don't handle the scenario when a change needs to send multiple notifications. In a ticketing system, moving a ticket between projects needs to send 2 events: one for the deletion from the original project and a second one for a creation in the new project, otherwise there will be some information disclosure. Or when a user is deleted, its tickets should become unassigned, and that can generate several events.

And third, the subscription event contains only the fields defined by the mutation, which is usually client-controlled. This can easily break real-time functionality for a seemingly unrelated change.

To solve all these problems, we need to think about subscriptions a bit differently and break the mutation -> subscription tie. In this chapter, we'll discuss a pattern that makes subscriptions practically useful.

Schema

First, add a dedicated mutation and event for the subscription:

type TodoEvent {
  userId: ID!
  groupId: ID!
  todoId: ID!
  severity: Severity!
  todo: Todo!
}

type Mutation {
  notifyTodo(userId: ID!, groupId: ID!, severity: Severity!, id: ID!): TodoEvent!
}

type Subscription {
  todo(userId: ID, groupId: ID, severity: Severity): TodoEvent
  @aws_subscribe(mutations: ["notifyTodo"])
}

The TodoEvent type has all the fields on the top level that are needed for filtering. In our case, that will be the todoId, severity, the userId, and a second level of indirection, groupId. This means the clients can subscribe to updates to individual Todo items, items belonging to a user, and items under a group.

The TodoEvent type allows filtering for users and groups too

Then the notifyTodo mutation has all the same arguments as the fields of the TodoEvent. This is to make sure all these filters can be used even when a Todo item is removed. For example, without getting the userId as an argument, the resolver for the notifyTodo wouldn't know which user the deleted Todo item belonged to.

Then the actual mutations are free to return any type they prefer, as the subscription's result type is no longer tied to them. In this example, we'll implement an addTodo and a removeTodo mutation:

type Mutation {
  addTodo(userId: ID!, name: String!, severity: Severity!): Todo!
  removeTodo(id: ID!): ID!
}
The subscription event contains all fields for filtering
Summary of the pattern
  • Separate the event type with all filterable fields on the top level
  • Add a mutation with all filterable fields as arguments
  • When a notification is sent, call the mutation

Let's implement the various parts of the pattern!

notifyTodo implementation

This is the simplest one, as it just needs to fetch the Todo item by id and return the object defined by the TodoEvent:

{
  "version" : "2018-05-29",
  "operation" : "GetItem",
  "key" : {
    "id": {"S": $util.toJson($ctx.args.id)}
  }
}

Then the response mapping template:

{
  "userId": $util.toJson($ctx.args.userId),
  "groupId": $util.toJson($ctx.args.groupId),
  "todoId": $util.toJson($ctx.args.id),
  "severity": $util.toJson($ctx.args.severity),
  "todo": $util.toJson($ctx.result)
}

Notice that the userId, groupId, todoId, and the severity comes from the arguments and not from the item fetched from the database. This is important as it supports deleted items, where the $ctx.result is null.

addTodo implementation

The addTodo mutation requires more steps as it needs to do quite a few things. For this, we'll use a pipeline resolver.

addTodo mutation steps

The first step is to add the new Todo item:

{
  "version" : "2018-05-29",
  "operation" : "PutItem",
  "key" : {
    "id": {"S": $util.toJson($util.autoId())}
  },
  "attributeValues": {
    "userid": {"S": $util.toJson($ctx.args.userId)},
    "created": {"S": $util.toJson($util.time.nowISO8601())},
    "name": {"S": $util.toJson($ctx.args.name)},
    "severity": {"S": $util.toJson($ctx.args.severity)}
  }
}

This is what we had before, so no changes here.

Second, it needs to fetch the User object for the Todo item:

{
  "version" : "2018-05-29",
  "operation" : "GetItem",
  "key" : {
    "id": {"S": $util.toJson($ctx.prev.result.userid)}
  }
}

Then store it in the stash:

#if ($ctx.error)
  $util.error($ctx.error.message, $ctx.error.type)
#end
$util.qr($ctx.stash.put("user", $ctx.result))
$util.toJson($ctx.prev.result)

Third, it needs to read the new Todo object as the mutation needs to return a Todo type:

{
  "version" : "2018-05-29",
  "operation" : "GetItem",
  "key" : {
    "id": {"S": $util.toJson($ctx.prev.result.id)}
  }
}

Finally, it needs to trigger the notifyTodo mutation. For this, we'll use the HTTP data source to call AppSync. To implement this, we need 3 things: a permission for the resolver to send GraphQL queries, a data source, then the resolver implementation.

The permission is the appsync:GraphQL action with the mutation field as the resource:

There is more, but you've reached the end of this preview
Read this and all other chapters in full and get lifetime access to:
  • all future updates
  • full web-based access
  • PDF and Epub versions