Subscription Triggers

In the previous section, we only used a single hard-coded topic value, but when we start thinking about tracking the life cycle of a particular entity, we need to pay a lot more attention to how we’re setting up our subscriptions and how we’re triggering them. The challenge isn’t just keeping track of how the topics are constructed; it can also be hard to make sense of where in your code base publish/3 calls may be happening. We’re going to explore an alternative approach to trigger mutations as we expand on the order-tracking capabilities of the PlateSlate system.

Everything that has a beginning has an end, and for the hungry customer, orders are fortunately no exception. We need to complete the life cycle of an order by providing two mutations: one to indicate that it’s ready, and one to indicate that it was picked up.

Fortunately, most of what we need to do this in our context and schema already exists, so we can just jump directly to building out the relevant mutation fields in the GraphQL schema and filling out each resolver.

 mutation ​do
 
  field ​:ready_order​, ​:order_result​ ​do
  arg ​:id​, non_null(​:id​)
  resolve &Resolvers.Ordering.ready_order/3
 end
  field ​:complete_order​, ​:order_result​ ​do
  arg ​:id​, non_null(​:id​)
  resolve &Resolvers.Ordering.complete_order/3
 end
 
 # Other fields
 end

Our :ready_order and :complete_order fields use new resolver functions from PlateSlateWeb.Resolvers.Ordering; let’s add those:

 def​ ready_order(_, %{​id:​ id}, _) ​do
  order = Ordering.get_order!(id)
 with​ {​:ok​, order} <- Ordering.update_order(order, %{​state:​ ​"​​ready"​}) ​do
  {​:ok​, %{​order:​ order}}
 else
  {​:error​, changeset} ->
  {​:ok​, %{​errors:​ transform_errors(changeset)}}
 end
 end
 
 def​ complete_order(_, %{​id:​ id}, _) ​do
  order = Ordering.get_order!(id)
 
 with​ {​:ok​, order} <- Ordering.update_order(order, %{​state:​ ​"​​complete"​}) ​do
  {​:ok​, %{​order:​ order}}
 else
  {​:error​, changeset} ->
  {​:ok​, %{​errors:​ transform_errors(changeset)}}
 end
 end

So far, so good. This may start to feel pretty second nature at this point. If you are concerned that the changeset error handling here is seeming kind of redundant, hold on tight—that is covered in the very next chapter on middleware.

Subscribing to these events is just a little bit different than before, because now we’re trying to handle events for specific orders based on ID. When the client is notified about new orders via a new_order subscription, we then want to give them the ability to subscribe to future updates for each of those subscriptions specifically.

We want to support a GraphQL document that looks like:

 subscription​ {
  updateOrder(id: ​"13"​) {
  customerNumber
  state
  }
 }

Notably, we want to use this one subscription field to get updates triggered by both the :ready_order and :complete_order mutation fields. While it’s important to represent the mutations as different fields, it’s often the case that you just need a single subscription that lets you get all the state changes for a particular entity that you want to watch.

 subscription ​do
  field ​:update_order​, ​:order​ ​do
  arg ​:id​, non_null(​:id​)
 
  config ​fn​ args, _info ->
  {​:ok​, ​topic:​ args.id}
 end
 end
 
 # Other fields
 end

The main difference is that we’re now doing something more dynamic in our config function. Here we’re using the arguments provided to the field to generate a topic that is specific to the ID of the order we care about.

Based on your previous experience with the Absinthe.Subscription.publish/3 function, you might be able to figure out the function call you could put in each mutation resolver to trigger this subscription field:

 Absinthe.Subscription.publish(
  PlateSlateWeb.Endpoint, order,
 update_order:​ order.id
 )

However, while we could use the publish/3 function here, we’re going to explore a slightly different option. The issue with our approach thus far is that although our schema contains the :place_order mutation and also the :new_order subscription fields, there isn’t any indicator in the schema that these two fields are connected in any way. Moreover, for subscription fields that are triggered by several different mutations, the topic logic is similarly distributed in a way that can make it difficult to keep track of.

This pattern of connecting mutation and subscription fields to one another is so common that Absinthe considers it a first-class concept and supports setting it as a trigger on subscription fields, avoiding the need to scatter publish/3 calls throughout your code base. Let’s look at how we can use the trigger macro to connect the new subscription field to each mutation without touching our resolvers:

 subscription ​do
  field ​:update_order​, ​:order​ ​do
  arg ​:id​, non_null(​:id​)
 
  config ​fn​ args, _info ->
  {​:ok​, ​topic:​ args.id}
 end
 
  trigger [​:ready_order​, ​:complete_order​], ​topic:​ ​fn
  %{​order:​ order} -> [order.id]
  _ -> []
 end
 
  resolve ​fn​ %{​order:​ order}, _ , _ ->
  {​:ok​, order}
 end
 end
 
 # Other fields
 end

The trigger macro takes two arguments: a mutation field name (or list of names) and a set of options that let you specify a topic function. This trigger topic function receives the output of the mutation as an argument, and should return a list of topics that are each used to find relevant subscriptions.

Let’s think through how this works.

Two new orders are created with IDs "1" and "2". The UI client will send in two subscriptions. The first looks like this:

 subscription​ {
  updateOrder(id: ​"1"​) {
  state
  }
 }

The second looks like this:

 subscription​ {
  updateOrder(id: ​"2"​) {
  state
  }
 }

Even though they use the same field, each of these documents should only get events that are for the particular order ID specified in the arguments. The first document produces a topic of "1", and the second document produces a topic of "2".

Now order "2" is marked by the kitchen as ready to be picked up. The ready_order resolver returns {:ok, %{order: %Order{id: 2, ...}}}, and it’s that value in that tuple that gets passed to the trigger function you’ve defined (where it’s matched by the first pattern). This clause returns order.id, which, in the case of this specific order, produces a result of 2. As we noted earlier, topics are always strings, so Absinthe calls to_string/1 on whatever return, in this case producing "2".

We now have a value ("2") with which to look up subscriptions; when we do so, we get just the second document.

Triggering Many Topics

images/aside-icons/info.png

Trigger topic functions can specify multiple topics by returning a list: ["topic1", "topic2"].

What about the other case that shows up in the trigger topic function: _, _ -> []? Remember that the mutation resolver can return error information from changesets. When this happens, we don’t want to push out anything because the order wasn’t actually updated. Returning [] prevents any publication from happening for this particular mutation, because we aren’t returning any topics that we want to publish to.

What if the :ready_order and :complete_order mutation fields returned different values? A given subscription field can have many different triggers defined on it each with a different topic function. For example, suppose :ready_order returned %{ready: order}, and :completed_order returned %{completed: order}. We could handle this by doing:

 trigger ​:ready_order​, ​topic:​ ​fn
  %{​ready:​ order}, _ -> [order.id]
  _, _ -> []
 end
 trigger ​:completed_order​, ​topic:​ ​fn
  %{​completed:​ order}, _ -> [order.id]
  _, _ -> []
 end

Finally, you’ll see that we’re using a resolver here, whereas we didn’t need to do so on the other subscription field. When we were calling publish/3 explicitly, we were passing the bare order record directly to publish. Now, however, we’re getting the full result of the mutation, which in the success case is %{order: order}. The resolver can just pattern match on this to unwrap it.

Whether to use an explicit Absinthe.Subscription.publish/3 call or the trigger macro will depend on the scenario. In general, however, it’s best to use triggers when there’s a clear and sensible mapping of mutations to subscriptions because it helps place this information in a clear and central location. Placing the trigger topic functions next to the subscription topic function goes a long way toward keeping track of how each operation connects to the other.

No feature would be complete without a test, so let’s encode this little narrative in a test case:

 defmodule​ PlateSlateWeb.Schema.Subscription.UpdateOrderTest ​do
 use​ PlateSlateWeb.SubscriptionCase
 
  @subscription ​"""
  subscription ($id: ID! ){
  updateOrder(id: $id) { state }
  }
  """
  @mutation ​"""
  mutation ($id: ID!) {
  readyOrder(id: $id) { errors { message } }
  }
  """
  test ​"​​subscribe to order updates"​, %{​socket:​ socket} ​do
  reuben = menu_item(​"​​Reuben"​)
 
  {​:ok​, order1} = PlateSlate.Ordering.create_order(%{
 customer_number:​ 123, ​items:​ [%{​menu_item_id:​ reuben.id, ​quantity:​ 2}]
  })
  {​:ok​, order2} = PlateSlate.Ordering.create_order(%{
 customer_number:​ 124, ​items:​ [%{​menu_item_id:​ reuben.id, ​quantity:​ 1}]
  })
 
  ref = push_doc(socket, @subscription, ​variables:​ %{​"​​id"​ => order1.id})
  assert_reply ref, ​:ok​, %{​subscriptionId:​ _subscription_ref1}
 
  ref = push_doc(socket, @subscription, ​variables:​ %{​"​​id"​ => order2.id})
  assert_reply ref, ​:ok​, %{​subscriptionId:​ subscription_ref2}
 
  ref = push_doc(socket, @mutation, ​variables:​ %{​"​​id"​ => order2.id})
  assert_reply ref, ​:ok​, reply
 
  refute reply[​:errors​]
  refute reply[​:data​][​"​​readyOrder"​][​"​​errors"​]
 
  assert_push ​"​​subscription:data"​, push
  expected = %{
 result:​ %{​data:​ %{​"​​updateOrder"​ => %{​"​​state"​ => ​"​​ready"​}}},
 subscriptionId:​ subscription_ref2
  }
  assert expected == push
 end
 end

This test case ultimately captures the story we’re going for. You’ve got two distinct subscriptions, and then an update happens to just one of them; that’s the one you get the update for, not any other. Let’s run the test:

 $ ​​mix​​ ​​test​​ ​​test/plate_slate_web/schema/subscription/update_order_test.exs
 .
 
 Finished in 0.4 seconds
 1 test, 0 failures

When you look back over the code in this chapter, you can see that nearly all of it just has to do with building out orders. At the end of the day, GraphQL subscriptions end up being rather simple, because with GraphQL, they’re just another part of your normal API. The GraphQL type system binds each of these parts of the API together, so that all the work you do to support ordinary queries and mutations can simply get referenced from subscriptions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.39.60