Cannot set currently-valued fields to null in update mutation
Overview of the Issue
I cannot easily update a field on an object that currently has a value to be null.
It seems this is because the graphql-js library is stripping null variables that get passed to it, since the GraphQL spec currently does not have a concept of a null field value. Please see https://github.com/graphql/graphql-js/issues/133 and https://github.com/facebook/graphql/pull/83 for further information and hand-wringing.
Reproduce the Error
For example, if I have a model named User, and User has a field firstName, if I run an addUser mutation with firstName set to "Alex", but then want to set firstName on that same object to be null, I cannot.
I can try to send the following update mutation:
mutation updateMyUser($input: updateUserInput!) {
updateUser(input: $input) {
changedUser {
firstName
}
clientMutationId
}
}
with variables:
{
"input": {
"clientMutationId": "justTesting",
"firstName": null,
"id": "VXNlcjo1NzMzNDA4MDMwM2EzMTk4M2ExZjNmNGI="
}
}
... but when I do, I will still receive:
{
"data": {
"updateUser": {
"changedUser": {
"firstName": "Alex"
},
"clientMutationId": "justTesting"
}
}
}
Suggest a Fix
As suggested in https://github.com/graphql/graphql-js/issues/133#issuecomment-132751201, it seems like adding a deletions field is a viable possibility to work around the fact that the GraphQL folks seem very hesitant to implement a native null value. I'm working on that myself right now in the https://github.com/wellth-app/graffiti-mongoose fork (which has by now diverged quite a bit).
I'm also tossing around the idea of creating my own pre-graffiti middleware that notes all null values in the variables tree and adds their path to a top-level deletions array on the variables tree, so that our existing clients don't have to implement the new deletions field.
Here's my hack to make deletions possible: https://github.com/wellth-app/graffiti-mongoose/commit/886e8f3bbae16bf7d08db568203f74ea49a3433c
... and then I made this middleware that I can drop into my Koa stack to grab null variables from things that look like mutations and add their paths to the deletions argument:
function getPathsToNull(tree, parentPath) {
const nullPaths = [];
for (const key in tree) {
if (tree[key] === null) {
// add to null paths
nullPaths.push(
parentPath ?
[parentPath, key].join('.') :
key
);
} else if (tree[key] instanceof Object) {
// descend
nullPaths.push.apply(nullPaths, getPathsToNull(
tree[key],
parentPath ?
[parentPath, key].join('.') :
key
));
}
}
return nullPaths;
}
function * handleNullVariables(next) {
const body = this.request.body;
const { query, variables } = Object.assign({}, body, this.query);
// TODO: properly parse the full query AST to determine if actually an add/update mutation
if (query && /mutation[\s\S]*{\s*(update|add)[a-zA-Z_]+\s*\(/i.test(query)) {
const parsedVariables = (typeof variables === 'string' && variables.length > 0) ?
JSON.parse(variables) : variables;
for (const key in parsedVariables) {
if ({}.hasOwnProperty.call(parsedVariables, key)) {
const deletions = getPathsToNull(parsedVariables[key]);
if (deletions && parsedVariables[key] !== null) {
parsedVariables[key].deletions = deletions;
}
}
}
const correctedVariables = JSON.stringify(parsedVariables);
// set both GET and POST vars
this.request.body.variables = correctedVariables;
this.query.variables = correctedVariables;
}
yield next;
}
export default handleNullVariables;