RisingStack / graffiti-mongoose

⚠️ DEVELOPMENT DISCONTINUED - Mongoose (MongoDB) adapter for graffiti (Node.js GraphQL ORM)
https://risingstack-graffiti.signup.team/
MIT License
382 stars 52 forks source link

Cannot set currently-valued fields to null in update mutation #129

Open zopf opened 8 years ago

zopf commented 8 years ago

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.

zopf commented 8 years ago

Here's my hack to make deletions possible: https://github.com/wellth-app/graffiti-mongoose/commit/886e8f3bbae16bf7d08db568203f74ea49a3433c

zopf commented 8 years ago

... 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;