3
votes

I am trying to return a Query type from a mutation, which I could make work in some cases but not the way I want it. The problem is not particularly related to the Query type being used, as I found the same behaviour using other types than Query.

You can run and modify this code on https://codesandbox.io/s/1z8kjy8m93

Server

const { ApolloServer, gql } = require("apollo-server");

const typeDefs = gql`
  type Query {
    hello(msg: String): String
  }

  type Mutation {
    someMutation(someArg: String): MutationResponse
  }

  type MutationResponse {
    query: Query
    status: String
  }
`;

const resolvers = {
  Query: {
    hello: (root, args, context) => {
      console.log("hello: args = ", args);
      return `${args.msg}, world !`;
    }
  },
  Mutation: {
    someMutation: (root, args, context) => {
      console.log("someMutation: args = ", args);
      return { status: `Mute Mute: ${args.someArg}` };
    }
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers
});

server.listen().then(({ url }) => {
  console.log(`???? Server ready at ${url}`);
});

Mutation

mutation mutateMe($mutationArg: String = "YoloMute !", $helloMsg: String = "Yolhello") {
  someMutation(someArg: $mutationArg) {
    status
    query {
      hello(msg: $helloMsg)
    }
  }
}

Response

{
  "data": {
    "someMutation": {
      "status": "Mute Mute: YoloMute !",
      "query": null
    }
  }
}

I don't understand why the hello resolver is not called and the query field is null.

The status field is duly filled by the someMutation resolver, but as the query field is not resolved there I would expect GraphQL to call an existing resolver for this field, which exists and should be called for the Query type.

I found other ways that technically work but are not satisfying:

1

1 Answers

5
votes

This issue isn't really specific to the Query type, but rather deals with how you've set up your resolvers.

The status field is duly filled by the someMutation resolver, but as the query field is not resolved there I would expect GraphQL to call an existing resolver for this field, which exists and should be called for the Query type.

There is no resolver for the entire Query type, or any other type. Resolvers exist only for individual fields of a particular type. When a resolver isn't defined for a field, GraphQL will default to looking for a property on the parent object with the same name as the field, and will return the value of that property.

Let's walk through your document. The root-level field is:

someMutation(someArg: $mutationArg)

The parent value is the root value for all root-level mutations. Unless you're using a custom root value, this will typically be an empty object. If you didn't define a resolver for the someMutation field of the Mutation type, GraphQL would look for a property called someMutation in your root value and return that (i.e. undefined, which would be coerced to null in the response). We do have a resolver, though, and it returns:

{
  status: `Mute Mute: ${args.someArg}`,
}

Now, let's resolve the status field. Our parent object is the result returned by the parent field's resolver. In this case, the object above. We have no resolver for status on MutationResponse, so GraphQL looks for a status property on the parent -- it finds one and uses that. status has a Scalar type, so whatever value is returned by the resolver will be coerced into the appropriate scalar value.

What about the query field? Again, we have no resolver for a query field on the MutationResponse. However, we also don't have a property called query on the parent object. So, all GraphQL can do is return null for that field.

Even though the return type for query is an ObjectType, because it resolves to null, any resolvers for fields on that ObjectType will not be fired. Returning null means the object doesn't exist, so we don't need to bother resolving any fields on it. Imagine if a field returned a User object. If it returned null, there would be no need to resolve the user's name, for example.

So... how do we get around this? There's two ways:

Either add a property for query to the object returned by someMutation's resolver, like this:

{
  status: `Mute Mute: ${args.someArg}`,
  query: {},
}

Or, add a resolver for the field:

MutationResponse: {
  query: () => {},
},

Either way will ensure that the query field will resolve to a non-null value (in this case, just an empty object). Because the value resolved is not null and the return type is an ObjectType (in this case Query), now the resolvers for that type's fields will be triggered and hello will resolve as expected.