The schema is the vocabulary used by GraphQL backend server, and the Relay components in the frontend. The GraphQL type system enables the schema to describe the data that's available, and how to put it all together when a query request comes in. This is what makes the whole approach so scalable, the fact that the GraphQL runtime figures out how to put data together. All you need to supply are functions that tell GraphQL where the data is; for example, in a database or in some remote service endpoint.
Let's take a look at the types used in the GraphQL schema for the TodoMVC app, as follows:
import {
GraphQLBoolean,
GraphQLID,
GraphQLInt,
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
GraphQLString
} from 'graphql';
import {
connectionArgs,
connectionDefinitions,
connectionFromArray,
cursorForObjectInConnection,
fromGlobalId,
globalIdField,
mutationWithClientMutationId,
nodeDefinitions,
toGlobalId
} from 'graphql-relay';
import {
Todo,
User,
addTodo,
changeTodoStatus,
getTodo,
getTodos,
getUser,
getViewer,
markAllTodos,
removeCompletedTodos,
removeTodo,
renameTodo
} from './database';
const { nodeInterface, nodeField } = nodeDefinitions(
globalId => {
const { type, id } = fromGlobalId(globalId);
if (type === 'Todo') {
return getTodo(id);
}
if (type === 'User') {
return getUser(id);
}
return null;
},
obj => {
if (obj instanceof Todo) {
return GraphQLTodo;
}
if (obj instanceof User) {
return GraphQLUser;
}
return null;
}
);
const GraphQLTodo = new GraphQLObjectType({
name: 'Todo',
fields: {
id: globalIdField(),
complete: { type: GraphQLBoolean },
text: { type: GraphQLString }
},
interfaces: [nodeInterface]
});
const {
connectionType: TodosConnection,
edgeType: GraphQLTodoEdge
} = connectionDefinitions({ nodeType: GraphQLTodo });
const GraphQLUser = new GraphQLObjectType({
name: 'User',
fields: {
id: globalIdField(),
todos: {
type: TodosConnection,
args: {
status: {
type: GraphQLString,
defaultValue: 'any'
},
...connectionArgs
},
resolve: (obj, { status, ...args }) =>
connectionFromArray(getTodos(status), args)
},
numTodos: {
type: GraphQLInt,
resolve: () => getTodos().length
},
numCompletedTodos: {
type: GraphQLInt,
resolve: () => getTodos('completed').length
}
},
interfaces: [nodeInterface]
});
const GraphQLRoot = new GraphQLObjectType({
name: 'Root',
fields: {
viewer: {
type: GraphQLUser,
resolve: getViewer
},
node: nodeField
}
});
const GraphQLAddTodoMutation = mutationWithClientMutationId({
name: 'AddTodo',
inputFields: {
text: { type: new GraphQLNonNull(GraphQLString) }
},
outputFields: {
viewer: {
type: GraphQLUser,
resolve: getViewer
},
todoEdge: {
type: GraphQLTodoEdge,
resolve: ({ todoId }) => {
const todo = getTodo(todoId);
return {
cursor: cursorForObjectInConnection(getTodos(), todo),
node: todo
};
}
}
},
mutateAndGetPayload: ({ text }) => {
const todoId = addTodo(text);
return { todoId };
}
});
const GraphQLChangeTodoStatusMutation = mutationWithClientMutationId({
name: 'ChangeTodoStatus',
inputFields: {
id: { type: new GraphQLNonNull(GraphQLID) },
complete: { type: new GraphQLNonNull(GraphQLBoolean) }
},
outputFields: {
viewer: {
type: GraphQLUser,
resolve: getViewer
},
todo: {
type: GraphQLTodo,
resolve: ({ todoId }) => getTodo(todoId)
}
},
mutateAndGetPayload: ({ id, complete }) => {
const { id: todoId } = fromGlobalId(id);
changeTodoStatus(todoId, complete);
return { todoId };
}
});
const GraphQLMarkAllTodosMutation = mutationWithClientMutationId({
name: 'MarkAllTodos',
inputFields: {
complete: { type: new GraphQLNonNull(GraphQLBoolean) }
},
outputFields: {
viewer: {
type: GraphQLUser,
resolve: getViewer
},
changedTodos: {
type: new GraphQLList(GraphQLTodo),
resolve: ({ changedTodoIds }) => changedTodoIds.map(getTodo)
}
},
mutateAndGetPayload: ({ complete }) => {
const changedTodoIds = markAllTodos(complete);
return { changedTodoIds };
}
});
const GraphQLRemoveCompletedTodosMutation = mutationWithClientMutationId(
{
name: 'RemoveCompletedTodos',
outputFields: {
viewer: {
type: GraphQLUser,
resolve: getViewer
},
deletedIds: {
type: new GraphQLList(GraphQLString),
resolve: ({ deletedIds }) => deletedIds
}
},
mutateAndGetPayload: () => {
const deletedTodoIds = removeCompletedTodos();
const deletedIds = deletedTodoIds.map(
toGlobalId.bind(null, 'Todo')
);
return { deletedIds };
}
}
);
const GraphQLRemoveTodoMutation = mutationWithClientMutationId({
name: 'RemoveTodo',
inputFields: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
outputFields: {
viewer: {
type: GraphQLUser,
resolve: getViewer
},
deletedId: {
type: GraphQLID,
resolve: ({ id }) => id
}
},
mutateAndGetPayload: ({ id }) => {
const { id: todoId } = fromGlobalId(id);
removeTodo(todoId);
return { id };
}
});
const GraphQLRenameTodoMutation = mutationWithClientMutationId({
name: 'RenameTodo',
inputFields: {
id: { type: new GraphQLNonNull(GraphQLID) },
text: { type: new GraphQLNonNull(GraphQLString) }
},
outputFields: {
todo: {
type: GraphQLTodo,
resolve: ({ todoId }) => getTodo(todoId)
}
},
mutateAndGetPayload: ({ id, text }) => {
const { id: todoId } = fromGlobalId(id);
renameTodo(todoId, text);
return { todoId };
}
});
const GraphQLMutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
addTodo: GraphQLAddTodoMutation,
changeTodoStatus: GraphQLChangeTodoStatusMutation,
markAllTodos: GraphQLMarkAllTodosMutation,
removeCompletedTodos: GraphQLRemoveCompletedTodosMutation,
removeTodo: GraphQLRemoveTodoMutation,
renameTodo: GraphQLRenameTodoMutation
}
});
export default new GraphQLSchema({
query: GraphQLRoot,
mutation: GraphQLMutation
});
There are a lot of things being imported here, so I'll start with the imports. I wanted to include all of these imports because I think they're contextually relevant for this discussion. First, there's the primitive GraphQL types from the the graphql library. Next, you have helpers from the graphql-relay library that simplify defining a GraphQL schema. Lastly, there's imports from your own database module. This isn't necessarily a database, in fact, in this case, it's just mock data. You could replace database with api for instance, if you needed to talk to remote API endpoints, or we could combine the two; it's all GraphQL as far as your React components are concerned.
Then, you define some of your own GraphQL types. For example, the GraphQLTodo type has two fields—text and complete. One is a Boolean and one is a string. The important thing to note about GraphQL fields is the resolve() function. This is how you tell the GraphQL runtime how to populate these fields when they're required. These two fields simply return property values.
Then, there's the GraphQLUser type. This field represents the user's entire universe within the UI, hence the name. The todos field, for example, is how you query for todo items from Relay components. It's resolved using the connectionFromArray() function, which is a shortcut that removes the need for more verbose field definitions. Then, there's the GraphQLRoot type. This has a single viewer field that's used as the root of all queries.
Now let's take a closer look at the add todo mutation, as follows. I'm not going to go over every mutation that's used by the web version of this app, in the interests of space:
const GraphQLAddTodoMutation = mutationWithClientMutationId({
name: 'AddTodo',
inputFields: {
text: { type: new GraphQLNonNull(GraphQLString) }
},
outputFields: {
viewer: {
type: GraphQLUser,
resolve: getViewer
},
todoEdge: {
type: GraphQLTodoEdge,
resolve: ({ todoId }) => {
const todo = getTodo(todoId);
return {
cursor: cursorForObjectInConnection(getTodos(), todo),
node: todo
};
}
}
},
mutateAndGetPayload: ({ text }) => {
const todoId = addTodo(text);
return { todoId };
}
});
All mutations have a mutateAndGetPayload() method, which is how the mutation actually makes a call to some external service to change the data. The returned payload can be the changed entity, but it can also include data that's changed as a side-effect. This is where the outputFields come into play. This is the information that's handed back to Relay in the browser so that it has enough information to properly update components based on the side effects of the mutation. Don't worry, you'll see what this looks like from Relay's perspective shortly.
The mutation type that you've created here is used to hold all application mutations. Lastly, here's how the entire schema is put together and exported from the module:
export default new GraphQLSchema({
query: GraphQLRoot,
mutation: GraphQLMutation
});
Don't worry about how this schema is fed into the GraphQL server for now.