Fun with GraphQL

GraphQL and Relay walk into a Bar…

Ok. The description of this post may have been a click bait.

I would still argue that we can do some interesting experiments now that we have the skeleton of a GraphQL server.

Those tests will help us better understand the underlying fundamentals of GraphQL.

We’ll be starting from the GraphQL schema we built in the previous post: this basic schema defined a mutation to create a Playlist for a user.

Let’s first define a basic impleemntation for the resolve method of the mutation:

let mutationType = new GraphQLObjectType({
  name: 'RootMutationType',
  fields: {
    AddPlayList: {
      description: 'Add a playlist for a given user',
      type: new GraphQLObjectType({
        name: 'AddPlayListInput',
        fields: () => ({
          playList : {
            type: PlayListType
          },
          user : { type: UserType }
        })
      }),
      args: {
        userId : { type: new GraphQLNonNull(GraphQLString) },
        title : { type: new GraphQLNonNull(GraphQLString) },
        description : { type: GraphQLString }
      },
      resolve: (parent, args, ast) => {
        return {
          playList: {
            id: "p1",
            title: args.title,
            description: args.description
          },
          user: {
            id: args.userId,
            name: 'Pierre'
          }
        };
      }
    },

As expected, this query :

mutation test {
  AddPlayList(userId: "Ax", title: "the title",
              description: "the desc") {
    playList  {
        id
      title
      description
    }
    user {
      id
      name
    }
  }
}

returns:

{
  "data": {
    "AddPlayList": {
      "playList": {
        "id": "p1",
        "title": "the title",
        "description": "the desc"
      },
      "user": {
        "id": "Ax",
        "name": "Pierre"
      }
    }
  }
}

The question we want to play with in this post is:

How does the GraphQL implementation checks the data we are sending back from the `resolve` method?

What if we add extra fields in the response?

A quick implementation may not want to parse the GraphQLResolveInfo argument to filter out the fields the query is requesting…

What if we return extra data?

resolve: (parent, args, ast) => {
  return {
    playList: {
      id: "p1",
      title: args.title,
      description: args.description,
      extra: 'timeo danaos'
    },
    user: {
      id: args.userId,
      name: 'Pierre'
    }
  };
}

Response:

GraphQL filters out any extra field returned in the response.

That means that the implementation may decide to not perform the selection of the fields in the response and let the GraphQL implementation do that.

Problem:

What if we omit some fields in the response?

For this test we also need the description of the PlayListType:

var PlayListType = new GraphQLObjectType({
  name: 'PlayList',
  fields: () => ({
    id: {
      description: 'ID of the play list',
      type: new GraphQLNonNull(GraphQLString)
    },
    title: {
      description: 'title of the play list',
      type: new GraphQLNonNull(GraphQLString)
    },
    description: {
      type: GraphQLString
    },

Let’s try to omit first the description field (which is a non required field):

resolve: (parent, args, ast) => {
  return {
    playList: {
      id: "p1",
      title: args.title
    },
    user: {
      id: args.userId,
      name: 'Pierre'
    }
  };
}

No surprises here :

{
  "data": {
    "AddPlayList": {
      "playList": {
        "id": "p1",
        "title": "the title",
        "description": null
      },
      "user": {
        "id": "Ax",
        "name": "Pierre"
      }
    }
  }
}

If a non required field is omitted in the response, GraphQL returns null for its value

If we omit title, which is a required field:

{
  "data": {
    "AddPlayList": {
      "playList": null,
      "user": {
        "id": "Ax",
        "name": "Pierre"
      }
    }
  },
  "errors": [
    {
      "message": "Cannot return null for non-nullable field PlayList.title.",
      "locations": [
        {
          "line": 6,
          "column": 7
        }
      ]
    }
  ]
}

If a required field is omitted in the response, GraphQL returns an error in the errors array of the response

It is nice to see that the error message is very informative. Even the location information is correct.

What is we add a resolve method to the returned type?

To specify our request we defined the AddPlayListInput type to describe the response (see above).

As any other type, it is possible to define a resolve method there. Shall we?

type: new GraphQLObjectType({
  name: 'AddPlayListInput',
  fields: () => ({
    playList : {
      type: PlayListType ,
      resolve: () => {}
    },
    user : { type: UserType }
  })
}),

The result of the query is then:

{
  "data": {
    "AddPlayList": {
      "playList": null,
      "user": {
        "id": "Ax",
        "name": "Pierre"
      }
    }
  }
}

If a resolve method is provided for a type, this method should return an instance of this type.

As we already did in the past, let’s explore the parameters of the resolve method:

type: new GraphQLObjectType({
  name: 'AddPlayListInput',
  fields: () => ({
    playList : {
      type: PlayListType ,
      resolve: (parent, args, ast) => {
        console.log('@@@ resolve from PlayListType in AddPlayListInput ');
        console.log('- parent:' + require('util').inspect(parent, { showHidden: false, depth: null }));
        console.log('-   args:' + require('util').inspect(args  , { showHidden: false, depth: null }));
      }
    },
    user : { type: UserType }
  })

The output on the console is interesting:

@@@ resolve from PlayListType in AddPlayListInput
- parent:{ playList: { id: 'p1', title: 'the title', description: 'the description' },
  user: { id: 'Ax', name: 'Pierre' } }
-   args:{}

We see here how the parent parameters trickles down during the processing of a GraphQL query:

In this same resolve method, if we call our listOfFieldsFromSelectionSet method, we directly have the list of file the query is asking for:

listOfFieldsFromSelectionSet: [ 'id', 'title', 'description' ]

Was fun after all no? :tada: