query
takes care of the R in CRUD. For the rest, we need mutations.
For that, cashay will have 3 methods: add
, update
, delete
.
cashay.add(mutationString, options)
Given a standard graphQL request string, it's pretty easy to add to the client cache. Just look up the Type
and put it in that bin. The difficult part is how to update queries that reference the object.
For example, grabbing the 10 most recent Posts
. If I add a new post, I may want to prepend it to the array if I have infinity scrolling, or I may want to ignore it if I use pages with a fixed count. If I sort by something like reputation, I may even want to inject it in the middle (currently impossible with relay). Additionally, I may have something like a sidebar of "top 5 posts by rep" and a main area of "top 5 most recent posts". So, I'll need an option that can programmatically place an item based on the current array and the item.
options.affectArray = {
"": () => 0,
"{orderBy: 'reputation'}": (array, item) => {
const placeBefore = array.findIndex(obj => obj.reputation < item.reputation);
return array.slice().splice(placeBefore, 0, item);
}
}
options.affectObject = {
"": (currentObject, item) => return currentObject.rep > item.rep ? currentObject : item,
}
This solves the same problem as relay's getConfigs.rangeBehaviors
, except we derive the Type info from the clientSchema and a graph data structure isn't required. This makes the assumption that (Type, args) => <Array>
. If I had hardcoded queries like get5RecentPosts
and get10ReputablePosts
, which don't have args and both return a List of Posts, then I'd be forced to apply the same function to each. This is OK since hardcoding the limit and orderBy is not a best practice. As a workaround, such queries could add another argument to each. The arguments in affectArray
don't need to include all args, but it must be a subset to call the callback.
It'd be possible to handle objects, too, but the user-defined callback would have to return the single object instead of an array (eg getMostPopularPost
). Since it's possible an object and an array could share arguments, This change in the API warrants its own option to keep things very clean and unambiguous.
cashay.update(mutationsQuery, options)
Updates are easy. The mutation lookup in the clientSchema tells us the collection, and the id is provided by the user.
cashay.delete(mutationsQuery, options)
Deleting things is the hardest, because you can delete an item & all of it's dependents/associations (what relay calls NODE_DELETE) or a single item & leave the associations (RANGE_DELETE). Relay's example of RANGE_DELETE is to remove a tag from an item. Unless I'm missing something, this would be considered an update given my current normalization schema (#13). If I wanted to remove a tag from a todo, I'd just write a removeTagMutation
and call that, which would return the item without the tag (and I could minimize that query before sending it).
If I wanted to remove a tag, which would trigger a removal from multiple todos, then I would delete Tag:id
, and then traverse the tree removing any reference to Tag:id
. Optionally, if the clientSchema shows that Tag:id
is mandatory, then I could remove the parent, too, which would trigger a recursive check & delete.
Afterwards, probably within a setImmediate
, I'd need to garbage collect the orphaned leaf nodes.
Finally, removing an item from an array that allows length of 0 is fine. But removing an item from a query that returns a single object (eg getMostPopularPost
) would not be OK to leave blank. So, just like add
, I'll need something like:
options.affectObject = {
"": (objectStore) => Object.keys(objectStore).reduce((reduction, item) => item.rep > reduction.rep ? item : reduction)
}
OPTIMISTIC UI
Again, this will be broken out into 3 different actions.
To add something, all we need is item and either an array + index, or object.
Creating the item:
Ideally, the entire item is sent via a variable, but this isn't always the case, since creating the item might require data that is only on the server. Therefore, I think the easiest thing for add
is to have an options.optimisticItem
. This could take either the object itself, or a function to create the object. Since it'll only run once per batch of variables, it'll be simpler to accept an object (or an executed function that returns an object). For example:
options.optimisticItem = {
title: 'Hello',
id: '123',
};
instead of handling this in a global redux middleware, each CashayTransport
will have its own optimistic handler that assigns a transactionID
.
next(Object.assign({}, action, {meta: {optimistic: {type: BEGIN, id: transactionID}}})); //execute optimistic update
const socket = socketCluster.connect(socketOptions);
socket.emit('graphql', payload, error => {
next({
type: type + (error ? _ERROR : _SUCCESS),
error,
payload,
meta: {
synced: true,
optimistic: error ? {type: REVERT, id: transactionID} : {type: COMMIT, id: transactionID}
}
});
})