Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Relay Mutations: Mutating Paginated Associations

In a lot of cases we have mutations where there is a one-to-many or many-to-many association we need to mutate, and where the association is exposed to queries as a paginated list.

There are a handful of critical requirements:

  • Clients must be able to delete, add, and update elements of the association
  • In some cases, the ordering of the association is important in which case clients must be able to reorder the elements as well

Less critical:

  • Clients should be able to specify the association at the creation of the parent (e.g. create an ordered set of variants at the same time as creating the product)
  • Clients should be able to delete, add, update, and reorder elements of the association at once with some form of transactional atomicity
  • Clients should not have to paginate through the entire current association in order to add or remove a single element

There's a number of possible solutions to this problem:

Option 1 - Single Input Field, No Additional Mutations

The input type has a single array input field which represents the total truth of the association (new elements are added, existing elements are updated, missing elements are deleted, and order is preserved when necessary).

Cons: Deletion is very implicit. Clients have to paginated through the entire current state of the association. Not granular.

Option 2 - Single Input Field with Positions, Delete Mutation

The input type has a single array input field which is used to update existing elements and add new ones (missing elements are ignored). A position or index value can be specified on elements to reorder them. A separate mutation is used to delete elements.

Cons: It is inconsistent for deletion to be off on its own in a mutation while all other operations are on the parent. Not very granular.

Option 3 - Single Input Field, Delete and Reorder Mutations

The input type has a single array input field which is used to update existing elements and add new ones (missing elements are ignored). Separate mutations are used to delete and re-order elements.

Cons: Clients cannot add new elements to specific locations in the association, they would have to be added and then reordered separately. Not very granular.

Option 4 - Single Input Field, Add/Delete/Reorder Mutations

Like option 3 except the input field is only used for updates; a separate mutation is used for adding new elements.

Cons: Clients have to make multiple mutations to perform complex updates. Clients cannot create the parent with initial associations.

Option 5 - Entirely Separate Mutations

The parent input type has no related fields, everything is done via four separate mutations for add/remove/update/reorder.

Pros: Very explicit and granular, keeps different data model objects separate. Cons: Clients have to make multiple mutations to perform complex updates. Clients cannot create the parent with initial associations.

Option 6 - Two Input Fields with Positions

The input type has two array fields: one used to update, add, and reorder elements (see option 2) and the other to delete.

Cons: Feels like we're polluting the parent mutation; not granular.

Option 7 - Two Input Fields, Reorder Mutations

Like option 6, except a separate re-order mutation is used instead of position arguments.

Cons: Inconsistent for reorder to be off on its own. Also see cons for option 6.

All these options seem to have drawbacks. Option 5 seems to be to most explicit, but requires the user to use multiple mutations at the same time, where the operation is not really atomic anymore.

What is Facebook's way of handling those types of mutations? What is your way ? Thanks!

like image 536
XuoriG Avatar asked Feb 22 '17 19:02

XuoriG


1 Answers

There are definitely tradeoffs between all of these approaches, so to choose one really would depend on what I was trying to build.

The most common case in Facebook's schema has two nice constraints:

  • no reordering
  • no bulk operations

Comments on Facebook are the canonical example here. For cases like that, we've used Option 5 with great success, and I can confidently recommend it. For comments, we'd just have three mutations; commentCreate, commentEdit, and commentDelete.

For cases like that, we've ended up with common patterns:

  • For create and edit (or update) mutations, the object that the mutation resolves to usually contains the edge that was modified (where I use edge in the same sense that we use edge in the Pagination Best Practice doc). From that edge, you can then get the modified object trivially... but you can also get any edge data that might be desired.
  • delete mutations usually just return the ID of the deleted object; that's a pure convenience for the client if the client provided that ID in the input, but can be really useful if the deletion input takes some other piece of information and the server converted that to the ID, in which case returning the deleted ID allows the client to know which object was deleted.

Option 5 [...] Clients cannot create the parent with initial associations.

I'm not entirely sure I follow this; there's not a great example here with my comment example (since it doesn't exist in the product), but hypothetically if I wanted to post a comment and replies to that comment at the same time, I could imagine doing something like:

commentCreate(input: {text:"Hello World", replies:[{text:"Reply 1"}, {text:"Reply 2"}]})

Where I'm reusing the input type that commentCreate used as the plural input type accepted by replies.


For cases where reordering or bulk operations are necessary, which it sounds like is the primary case you're looking at, I don't have as good of an answer, and it's definitely a trickier case as you noted. I don't think I've seen enough examples of it to confidently recommend one option over another, and I think which option is best may end up depending on the particular use case. One additional option to consider, though, would be to have a single mutation where the input is a list of operations. So if we had a list of "favorite photos" and we wanted to do a bulk update, we could do something like:

favoritePhotosUpdate({operations: {operation:ADD, addedId:1234}, {operation:REMOVE, addedId:5678}, {operation:UPDATE, oldId:4321, newId:8765}, {operation:SWAP, oldId:32, newId:76}

Not sure if that's actually any better than the options above, but it's another option that we've discussed in the past (though I'm not sure we've put it into practice), so it's worth at least adding to the list.

Hope this helps!

like image 115
Daniel Schafer Avatar answered Sep 20 '22 11:09

Daniel Schafer