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:
Less critical:
There's a number of possible solutions to this problem:
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.
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.
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.
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.
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.
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.
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!
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:
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:
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With