DEV Community

David Mraz for Atheros

Posted on • Updated on • Originally published at atheros.ai

GraphQL List - How to use arrays in GraphQL schema (GraphQL Modifiers)

Introduction

It is often common practice in REST APIs to return a JSON response with an array of objects. In GraphQL we would like to follow this pattern as well. In this article we will go through modifiers, a special group of types which allows us to modify the default behaviour of other types. In GraphQL we deal with various groups of types. These groups are as follows:

It may be helpful first to go through the articles above. After gaining a fundamental understanding of other types such as scalars and object types you can then move on to modifiers. Next, we can start working on the project set-up so that we can test our queries. We assume that npm, git and Node.js versions higher than 8 are already installed on your computer. Now you can execute this command in your shell

git clone git@github.com:atherosai/graphql-gateway-apollo-express.git
Enter fullscreen mode Exit fullscreen mode

install dependencies with

npm i
Enter fullscreen mode Exit fullscreen mode

and start the server in development with

npm run dev
Enter fullscreen mode Exit fullscreen mode

Then you can move to GraphQL Playground to execute the queries available in this article. In the model project, we use the in-memory database with fake data for executing our queries.

Model schema

Let’s first consider this model schema, which was printed with the printSchema function from graphql-js utilities. The model schema in the repository is built with a class-based approach using the graphql-js library. It is often much clearer to view the whole schema written in Schema definition language (SDL). For some time now, SDL has been a part of the specification and it is often used to build the schema itself using the build schema utility or the library called graphql-tools

"""Input payload for creating user"""
input CreateUserInput {
  username: String!
  email: String
  phone: String
  firstName: String
  lastName: String
  role: UserRoleEnum = ACCOUNTANT
}

"""User type definition"""
type CreateUserPayload {
  user: User!
}

"""User type definition"""
type CreateUsersPayload {
  users: [User]
}

"""An ISO-8601 encoded UTC date string."""
scalar DateTime

type Mutation {
  createUser(input: CreateUserInput!): CreateUserPayload
  createUsers(input: [CreateUserInput!]!): CreateUsersPayload
}

type Query {
  users(role: UserRoleEnum): [User!]!
}

"""User type definition"""
type User {
  id: ID!
  username: String!
  email: String
  phone: String
  firstName: String
  lastName: String
  role: UserRoleEnum!
  createdAt: DateTime!
  updatedAt: DateTime
}

enum UserRoleEnum {
  ADMIN
  ACCOUNTANT
}
Enter fullscreen mode Exit fullscreen mode

We can see that we have defined one output object type called User with the following fields: id, username, email, phone, firstName, lastName, createdAt, updatedAt. The id field is typed as an ID scalar and other fields are typed as Strings. We’ve also defined the queries user and users. The user query returns the User object based on the passed id. The users query then returns a list of users. We have also defined the non-required enum type role, which is used in the users query as an argument for filtering the result. In this simple schema we used modifiers quite a lot. In the rest of the article, we will go through these use cases.

Modifiers

First, let’s formally define modifier. As we have already mentioned, modifier is a special group of types in GraphQL. These types can be defined as follows:

A Modifier modifies the type to which it refers.

From this definition, it is clear that we always need to define the type to which we are applying the modifier. In current GraphQL specification, we have these two types of modifiers. Each of the modifiers is classified as a separate type:

  • List
  • Non-Null

The List modifier will be our main focus in this article. It will allow us to define if we would like to return a sequence of types. A Non-Null modifier allows us to define if the type/field is required. This can be null (default behavior in GraphQL) or is required and the GraphQL server raises an error. In this article, we will focus mainly on List modifiers and leave a more in-depth discussion of Non-Null modifiers for another article.

List

In general, a GraphQL list represents a sequence of values. It is possible to view these values as arrays (e.g. in Javascript), although the analogy is not completely precise. As we mentioned a list keeps items in an order. In SDL the list modifier is written as square brackets with the wrapped instance of the type in the bracket. In our schema, we used the list modifier to define that if we call the query users, it returns a sequence of types of User from the database. This is achieved by defining the schema as follows:

type Query {
  user(id: ID!): User
  users(role: RoleEnum): [User!]!
}
Enter fullscreen mode Exit fullscreen mode

By calling query users we expect to return a list of users. Let’s see how this looks when we use the graphql-js library. The queries in our repository are defined as follows:

import {
  GraphQLList,
  GraphQLNonNull,
} from 'graphql';
import { getUsers } from '../../operations/users-operations';
import User from './UserType';
import UserRoleEnum from './UserRoleEnumType';

const UserQueries = {
  users: {
    type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User))),
    args: {
      role: {
        type: UserRoleEnum,
      },
    },
    resolve: (_source, { role }) => {
      const result = getUsers();
      if (role != null) {
        return result.filter((user) => user.role === role);
      }
      return result;
    },
  },
};

export default UserQueries;
Enter fullscreen mode Exit fullscreen mode

We can see that we achieve the same functionality as with SDL. The GraphQLList class represents the List. We have applied the instance of this class to the instance of User. Now we are able to fetch the data by executing the users query in GraphQL Playground with the Play button.

Alt Text

We should retrieve this data and obtain users as a list.

{
  "data": {
    "users": [
      {
        "id": "7b838108-3720-4c50-9de3-a7cc04af24f5",
        "firstName": "Berniece",
        "lastName": "Kris",
        "username": "Ana_Quigley"
      },
      {
        "id": "66c9b0fd-7df6-4e2a-80c2-0e4f8cdd89b1",
        "firstName": "Bradly",
        "lastName": "Lind",
        "username": "Winona_Kulas12"
      },
      {
        "id": "718590a1-33ac-4e61-9fef-b06916acd76b",
        "firstName": "Leila",
        "lastName": "Schowalter",
        "username": "Isabell.Kautzer"
      },
      {
        "id": "411df0f3-bb2c-4f5f-870f-3db9c30d754f",
        "firstName": "Laila",
        "lastName": "Breitenberg",
        "username": "Christophe.Oberbrunner"
      },
      {
        "id": "e1254480-d205-4be8-abfa-67ad7dcd03fb",
        "firstName": "Joe",
        "lastName": "Crist",
        "username": "Dahlia.Gerhold56"
      },
      {
        "id": "d0087200-9621-4787-a3db-cebbede163e6",
        "firstName": "Bettye",
        "lastName": "Bartoletti",
        "username": "Thad_Mayert"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The other use case for List modifiers is for designing the createUsers mutation, where we can add users in batch. There are multiple reasons to design the mutations in this way. We may need to add users in a transaction, therefore we cannot have a different resolver context or we just want to simplify the API or improve the performance and execute the mutation for multiple users more quickly. This is a great use case for applying the List modifier to our input payload. We can define the input object type just once like this:

import {
  GraphQLString,
  GraphQLInputObjectType,
  GraphQLNonNull,
} from 'graphql';
import UserRole from './UserRoleEnumType';

const CreateUserInputType = new GraphQLInputObjectType({
  name: 'CreateUserInput',
  description: 'Input payload for creating user',
  fields: () => ({
    username: {
      type: new GraphQLNonNull(GraphQLString),
    },
    email: {
      type: GraphQLString,
    },
    phone: {
      type: GraphQLString,
    },
    firstName: {
      type: GraphQLString,
    },
    lastName: {
      type: GraphQLString,
    },
    role: {
      type: UserRole,
      defaultValue: UserRole.getValue('ACCOUNTANT').value,
    },
  }),
});

export default CreateUserInputType;
Enter fullscreen mode Exit fullscreen mode

or in SDL language

input CreateUserInput {
  username: String!
  email: String
  phone: String
  firstName: String
  lastName: String
}
Enter fullscreen mode Exit fullscreen mode

and then apply List modifier to achieve the ability to pass multiple payloads in one input variable.


import {
  GraphQLList,
  GraphQLNonNull,
} from 'graphql';
import { isEmail } from 'validator';
import { createUser, createUsers } from '../../operations/users-operations';
import CreateUserInput from './CreateUserInputType';
import CreateUserPayload from './CreateUserPayload';
import CreateUsersPayload from './CreateUsersPayload';

const UserMutations = {
  createUser: {
    type: CreateUserPayload,
    args: {
      input: {
        type: new GraphQLNonNull(CreateUserInput),
      },
    },
    resolve: (_source, args) => {
      const { input } = args;

      if (input.email && !isEmail(input.email)) {
        throw new Error('Email is not in valid format');
      }
      return {
        user: createUser(input),
      };
    },
  },
  createUsers: {
    type: CreateUsersPayload,
    args: {
      input: {
        type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(CreateUserInput))),
      },
    },
    resolve: (_source, { input }) => {
      const createdUsers = createUsers(input);
      return {
        users: createdUsers,
      };
    },
  },
};

export default UserMutations;
Enter fullscreen mode Exit fullscreen mode

We can execute the mutation with using inline arguments or if you prefer with using variables

mutation {
  createUsers(input: [{lastName: "Test", firstName: "test", username: "t1est"}, {lastName: "Test", firstName: "test", username: "te2st"}]) {
    users {
        id
        firstName
        lastName
        phone
        email
        username
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Now let’s go through the rules for result and input coercion. If you are not familiar with these terms, you can take a look at the article on scalars, where we describe input and result in coercion.

Result coercion

For the query users, result coercion is relevant for us as we would like to obtain an array of users from the executed query. When we coerce lists, the GraphQL server needs to ensure that the returned data from the resolver function will remain in the same order. The coercion of each item in the list is then delegated to the result coercion of the referenced type; each item of the array needs to comply to User type or null value. If it returns an object instead of array-like in this resolver function:

import {
  GraphQLList,
  GraphQLNonNull,
} from 'graphql';
import { getUsers } from '../../operations/users-operations';
import User from './UserType';
import UserRoleEnum from './UserRoleEnumType';

const UserQueries = {
  users: {
    type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User))),
    args: {
      role: {
        type: UserRoleEnum,
      },
    },
    resolve: (_source, { role }) => {
      const result = getUsers();
      if (role != null) {
        return result.filter((user) => user.role === role);
      }
      return result;
    },
  },
};

export default UserQueries;
Enter fullscreen mode Exit fullscreen mode

the GraphQL server should then raise this error

Expected Iterable, but did not find one for field Query.users.
Enter fullscreen mode Exit fullscreen mode

This happens if the coercion of the List modifier does not comply But what happens if some of the items in the list do not coerce properly? In that case, we handle the error in a similar manner. We return null instead of the value returned from the resolver function and add an error to the response.

Input coercion

When discussing input coercion of List modifiers we can take into account the createUsers mutation and describe the behavior that raises an error. In contrast to the result coercion, where some items from the result array can be obtained even if one item is not coerced properly, in input coercion we will not be able to execute the whole mutation if one payload cannot be coerced. Let’s take a look at the following example, where we would like to pass a list of two payloads, but one payload does not comply with the input type and does not have the required username field. Upon executing this mutation we receive the following error:

Argument "input" has invalid value [{username: "testtest", email: "test@test.com", firstName: "test", lastName: "test"}, {email: "test@test.com", firstName: "test", lastName: "test"}].
In element #1: In field "username": Expected "String!", found null.
Enter fullscreen mode Exit fullscreen mode

The whole mutation fails even if only the input coercion in the input object type in one item in the list does not comply. However, it is important to emphasize that if we pass null as follows, the whole mutation will be executed. However, this depends on whether or not we applied any additional modifiers and composed the modifiers in a more complex type. We will go through this topic in the last section of this article on Modifier composition.

Modifier composition

If we consider the definition of the modifier above, we know that the modifier basically creates a new type from the referenced type with additional functionality. In our case, we are adding behavior so that the result coercion will accept a list of items and not just the item itself. This is also similar to higher-order functions or the decorator pattern and in the same manner, we can chain higher-order functions or HOCs in React. We are also able to compose modifiers by applying a modifier to the type where the previous modifier is already applied. We can combine the Non-Null modifier with our List modifier in the following way. This way we basically combine three modifiers, which are chained as follows

new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User)))
Enter fullscreen mode Exit fullscreen mode

This creates a special type. When using only a list modifier we are allowed to return a null value from the resolver. We can even combine the items in the array to contain null values as in this array:

mutation {
  createUsers(input: [{username: "testtest", email: "test@test.com", firstName: "test", lastName: "test"}, null]) {
    id
    username
    firstName
  }
}
Enter fullscreen mode Exit fullscreen mode

But when we apply the composed modifier as above, we are only allowed to pass the array containing the objects that comply with the User type. The list above will, therefore, be rejected. The null value returned from the resolver will be also rejected. You can take a look at the table below, which contains what each modifier will allow in order to get a better idea of which combinations of modifiers are suitable for different use cases. The only rule in chaining modifiers applies to Non-null modifiers. It declares that we cannot wrap one Non-Null modifier with another Non-Null modifier.

[User] [UserObject, null] Valid
[User] null Valid
[User] [null] Valid
[User] [UserObject] Valid
[User!] [UserObject,null] Invalid
[User!] [null] Invalid
[User!] null Valid
[User!] [UserObject] Valid
[User!]! [UserObject, null] Invalid
[User!]! null Invalid
[User!]! [UserObject] Valid
User!! - Invalid

UserObject in this table can be equal for example to

{ lastName: "Test", firstName: "test", username: "t1est"}
Enter fullscreen mode Exit fullscreen mode

For simplicity, we did not cover the differences between input and output coercion for these more complex types. The behavior is different only as we discussed in the result and input coercion section. If there would be different UserObject, which do not comply with User type coercion (e.g. does not have username property), there would be additional rules.

Summary

In this article, we have covered one special group of types in GraphQL called Modifiers. With modifiers, we are allowed to inject special behavior into the referenced GraphQL type, add a List and other required fields, and even combine these use cases to build more complex types. Modifiers are a great tool to make elegant GraphQL schemas.

Did you like this post? You can clone the whole repository with examples. Feel
free to send any questions about the topic to david@atheros.ai.

Top comments (0)