A GraphQL Schema describes the data model, and provides a GraphQL server with an associated set of resolve methods that know how to fetch data.

Open relay-todolist/data/schema.js and replace it with the following code:


import {
  GraphQLObjectType,
  GraphQLNonNull,
  GraphQLBoolean,
  GraphQLSchema,
  GraphQLString,
  GraphQLList,
  GraphQLInt,
  GraphQLID
} from 'graphql';

import {
  toGlobalId,
  fromGlobalId,
  globalIdField,
  offsetToCursor,
  connectionArgs,
  nodeDefinitions,
  connectionFromArray,
  connectionDefinitions,
  connectionFromPromisedArray,
  mutationWithClientMutationId
} from 'graphql-relay';

import {
  addTodo,
  getUser,
  getTodo,
  getTodos,
  removeTodo,
  updateTodo
} from './database';

import { getWithType, isType } from './utils';

/* Interface */
const { nodeInterface, nodeField } = nodeDefinitions(
  (globalId) => {
    let { type, id } = fromGlobalId(globalId);

    if (type === 'User') {
      return getWithType(getUser(id), 'User');
    } else if (type === 'Todo') {
      return getWithType(getTodo(id), 'Todo');
    } else {
      return null;
    }
  },
  (obj) => {
    if ( isType(obj, 'User') ) {
      return userType;
    } else if ( isType(obj, 'Todo') ) {
      return todoType;
    } else {
      return null;
    }
  }
);
          
  • Lines 1 - 34 deal with various imports. We first import basic GraphQL types that we will be using frequently in constructing our data types. We then import functions that deal with graphql-relay connections, creating node interfaces and assigning global Ids (don't panic: we will cover all these in future sections). We then import all the database functions that we created earlier in our relay-todolist/data/database.js file. We have also provided some utility functions called getWithType and isType which makes it easy to resolve the relevant GraphQL type when Relay does a re-fetch.
  • Lines 31-58 we create a nodeInterface and a nodeField. Relay uses globally unique identifiers to re-fetch data. The globally unique identifiers are created using globalIdField helper which produces a Base64 encoded string. Whenever Relay needs to re-fetch data, it needs to be able to resolve the final type by checking the globalId. This is what is happening in the nodeDefinitions function. The first callback in the nodeDefinitions function provides a globalId as an argument using which the type and actual id of the Node is inferred. Then depending on the type the relevant data type is returned. The data type is then made available to the second callback which then in turn returns the relevant GraphQL type.

Now that we have got the basics out of the way, let us start creating our custom query types. Since we have only 2 collections ("user" and "todos") we will also create only 2 types. Add the following code to the same relay-todolist/data/schema.js file:


/* Basic Types */
const todoType = new GraphQLObjectType({
  name: 'Todo',
  description: 'A todo item',
  fields: () => ({
    id: globalIdField('Todo', ({ _id }) => _id),
    _id: {
      type: GraphQLString,
      description: 'Todo id',
      resolve: ({ _id }) => _id,
    },
    todo: {
      type: GraphQLString,
      description: 'The todo text',
      resolve: ({ todo }) => todo,
    },
    completed: {
      type: GraphQLBoolean,
      description: 'Status of Todo item',
      resolve: ({ completed }) => completed,
    },
  }),
  interfaces: [nodeInterface],
});

const {
  connectionType: todoConnection,
        edgeType: todoEdge
} = connectionDefinitions({
      name: 'Todo',
  nodeType: todoType
});

const userType = new GraphQLObjectType({
  name: 'User',
  description: 'Main User',
  fields: () => ({
    id: globalIdField('User', ({ _id }) => _id),
    todos: {
      type: todoConnection,
      description: 'A list of Todos',
      args: connectionArgs,
      resolve: (user, args) => {
        return connectionFromPromisedArray(getTodos(), args);
      }
    },
    getTodo: {
      type: todoType,
      description: 'A single Todo Item',
      args: {
        id: {
          type: GraphQLString,
        },
      },
      resolve: (user, {id}) => {
        if (!id) return;
        return getTodo(fromGlobalId(id).id);
      }
    }
  }),
  interfaces: [nodeInterface],
});
          
  • The todoType we defined in Lines 2-24 implements the nodeInterface we defined earlier. It also has 4 return types: the global unique identifier "id", the autogenerated database identifier "_id", the todo text "todo" and the completed status "completed". Each of these return types know how to resolve from data it receives.

    This is what our Todo type looks like:

    
    type Todo implements Node {
      id: ID!
      _id: String
      todo: String
      completed: Boolean
    }
                  
  • We also define a todoConnection and todoEdge in Lines 26-32. The todoConnection is a connection type for the todoType which is used by Relay to provide bidirectional pagination support. The todoEdge on the other hand provides a place to locate per-edge, edge-specific data. Refer to the official Relay documentation and GraphQL-Relay-JS Connections documentation for more details.

  • We then define a userType in Lines 34-62 which also implements the nodeInterface we defined earlier. It has 3 return types: the global unique identifier "id", a todos field that returns a promised array of all the user's Todo items, a getTodo field that returns only the particular todo requested.

  • If you observe keenly, you'll notice that both todoType and userType use the autogenerated database unique identifier _id to create it's respective global unique identifiers. You might wonder as to why this is useful? In the getTodo field we resolve the actual database identifier from the global unique identifier by calling the fromGlobalId(id) helper. Relay provides fromGlobalId(id) and toGlobalId(type, id) and we'll be using both extensively while defining our schema.

With queries out of the way, let's start defining our mutations. Add the following code to the same relay-todolist/data/schema.js file:



/* Mutations */
const AddTodoMutation = mutationWithClientMutationId({
  name: 'AddTodo',
  inputFields: {
    todo: { type: new GraphQLNonNull(GraphQLString) },
  },
  outputFields: {
    todoEdge: {
      type: todoEdge,
      resolve: ({ _id, todo, completed }) => {
        return getTodos().then((todos) => {
          /**
            We make use of offsetToCursor to figure out the todo item's offset
            instead of cursorForObjectInConnection. This is because
            cursorForObjectInConnection uses indexOf to do a shallow scan (
            instead of a deep scan) of items. We manually scan the todo list
            to find the location todo item and then use offsetToCursor to get
            the Relay equivalent base64 encoded representation of the offset.
          */
          var offset = 0;

          // figure out where the todo item is located in the list of todos
          for (var i = 0; i < todos.length; i++) {
            if (todos[i]._id.equals(_id)) {
              // found the offset
              offset = i;
              break;
            }
          }

          return {
            cursor: offsetToCursor(offset),
            node: { _id, todo, completed },
          }
        });
      }
    },
    user: {
      type: userType,
      resolve: () => getUser(),
    },
    error: {
      type: GraphQLString
    }
  },
  mutateAndGetPayload: ({todo}) => addTodo(todo)
});

const UpdateTodoMutation = mutationWithClientMutationId({
  name: 'UpdateTodo',
  inputFields: {
    id: { type: new GraphQLNonNull(GraphQLID) },
    todo: { type: new GraphQLNonNull(GraphQLString) },
    completed: { type: new GraphQLNonNull(GraphQLBoolean) },
  },
  outputFields: {
    todo: {
      type: todoType,
      resolve: ({ todo }) => getTodo(todo)
    },
    user: {
      type: userType,
      resolve: () => getUser(),
    },
  },
  mutateAndGetPayload: ({ id, todo, completed }) =>
                          updateTodo(fromGlobalId(id).id, todo, completed)
});

const RemoveTodoMutation = mutationWithClientMutationId({
  name: 'RemoveTodo',
  inputFields: {
    id: { type: new GraphQLNonNull(GraphQLID) },
  },
  outputFields: {
    todoIdToBeDeleted: {
      type: GraphQLID,
      resolve: ({ todo }) => toGlobalId('Todo', todo),
    },
    user: {
      type: userType,
      resolve: () => getUser(),
    },
  },
  mutateAndGetPayload: ({ id }) => removeTodo(fromGlobalId(id).id)
});
          
  • As you can see, we have defined 3 mutations: one for adding a todo, one for updating a todo and another for removing a todo item.
  • The AddTodoMutation defined in Lines 2-47 accepts 1 input field todo and outputs a todoEdge containing the newly added node. It also returns a user field along with errors if any. The mutation happens when mutateAndGetPayload is called with the input field todo. We then call the addTodo(todo) database function to create the todo. The result is resolved in todoEdge's resolve function. As you can see in the commented section, we iterate over all todos in our database to find the offset of the newly created node. We then pass this offset to offsetToCursor which returns a Relay specific cursor.
  • Similarly, the UpdateTodoMutation defined in Lines 49-68 creates a mutation type for updating an existing todo item. This is much simpler than the AddTodoMutation in that it accepts 3 inputs: id, todo and completed. The output includes the updated todo and the user tied to the todo. Again, mutateAndGetPayload is called which in turn calls updateTodo(id, todo, completed) database function to update the todo item.
  • RemoveTodoMutation (defined in Lines 70-86), on the other hand, has a single input field id which in turn returns the global unique identifier todoIdToBeDeleted which instructs Relay to remove the item from it's store. mutateAndGetPayload calls the removeTodo(id) database function to remove the todo item.

  /* Query Type */
  const queryType = new GraphQLObjectType({
    name: 'Query',
    fields: () => ({
      node: nodeField,
      user: {
        type: userType,
        resolve: () => getUser(),
      }
    })
  });

  /* Mutation Type */
  const mutationType = new GraphQLObjectType({
    name: 'Mutation',
    fields: () => ({
      addTodo: AddTodoMutation,
      updateTodo: UpdateTodoMutation,
      removeTodo: RemoveTodoMutation,
    }),
  });

  export default new GraphQLSchema({
    query: queryType,
    mutation: mutationType
  });

We then define a queryType which accepts a node global unique identifier field, as well as a user field with type of userType. Also defined is a mutationType with fields addTodo, updateTodo and removeTodo. Finally, export a new GraphQLSchema with the queryType and `mutationType`.

You can test the GraphQL schema we defined by launching the GraphiQL interface that ships with the integration. Just navigate to http://localhost:8080/graphiql and play with the newly defined schema.