Field restriction with GraphQL Apollo directives

Let’s see the story behind this article: I want to build an application, let's say it is a social media platform. I have a user entity model in my backend application that stores all relevant data about the user. The users can check other users profile page but there is some information which is visible only if the two users are friends on the platform, it is exactly like on Facebook. The problem is that I don’t want to write two different GraphQL queries and use the first one if the users are not friends and the second one if they are friends but I also want to protect the private fields. I want one query that returns the private field only if the users are friends. In this article, I am going to show you how easy to create this kind of restriction with NodeJS and Apollo.

At the end of this article, we would like to able to resolve this kind of GraphQL type:

type Query {
    user: User
}

type User {
    name:       String
    profileUrl: String
    email:      String @relation(requires: FRIEND)
    age:        Number @relation(requires: FRIEND)
}

Of course, this is only a simplified part of a social application but it is enough to discover the field restrictions. We will have a user query that returns the data of a specific user and we will have a User type that describes these data. The name and the profileUrl are public fields, everyone can get and see them but the email and the age are private, these only available for friends.

Directives in GraphQL

The strange @relation syntax in the GraphQL schema is a so-called directive in GraphQL. The GraphQL specification says about the directive:

directive.png

It means you have to start a directive with the directive word and a @ character, then you have to name the directive after that comes the arguments and at last, you need to define which structures can use this directive. In this example, we are going to use our directive only on fields but is possible to define directives that are useable on Queries, Mutations, Fragments, Enums, and so on. Here you can find the whole list.

In the specification, there are some predefined directives that are freely usable in any GraphQL project. Of course, the specification by default does not make available this feature in a NodeJS or any other backend application but the awesome specification-based GraphQL implementations contain these pre-defined directives too. For example with Apollo you can use the predefined directives like this:

type ExampleType {
  oldField: String @deprecated(reason: "Use `newField`.")
  newField: String
}

GraphQL definitions

Now we have become familiar with the definition of the directives, let’s construct our own for this demo project.

directive @relation(
  requires: Relation = FRIEND,
) on FIELD_DEFINITION

This is our directive definition. We have the required keywords, we have a name (relation), we have one parameter which has a default value in our case. And at the end of the definition, we have the location part, which declares where we can use the directive.

Here is the schema file that contains the directive definition, the Relation enum type, the User type, and one query that returns one User type.

directive @relation(
  requires: Relation = FRIEND
) on FIELD_DEFINITION

enum Relation {
  FRIEND
}

type User {
  name:       String
  profileUrl: String
  email:      String @relation(requires: FRIEND)
  age:        Number @relation(requires: FRIEND)
}

type Query {
  user: User
}

The Relation enum defines the possible relation types. Right now we have only the FRIEND here but we can easily define more if we want to differentiate more relation types. In this case, because we have only one, not important to create an enum type, we could create a simple @isFriend directive to decide if two users are friends, but I wanted to build a more generic solution if I would use more relation types in the future.

It is possible to write restrictions that are available not only on fields but objects or enums. In this example, we would use this extended restriction if you have entire entities which are available only for friends, like the list of the user’s posts.

Directive implementation

There are some default directives in the GraphQL specification, but by default, this directive is not, so we need to implement our own.

const { SchemaDirectiveVisitor } = require("apollo-server");
const { defaultFieldResolver } = require('graphql');

class RelationDirective extends SchemaDirectiveVisitor {

 visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;

    field.resolve = async function (source, { requires, otherArgs }, context, info) {
      const currentUser = await getUser(context.headers.authToken);
      const connection = await getConnection(currentUser.id, source.id);

      const _requires = requires || this.args.requires;

      if (connection.type !== _requires) {
        throw new Error("the users aren't related");
      }

      return resolve.apply(this, [source, otherArgs, context, info]);
    };
  }
}

In this example, if the required relation doesn't exist between the two users, the GraphQL query will throw an error. In most cases, we don’t want that because we want to return with the public data and hide the private data, so we need valid return data. For this, we need to modify the visitFieldDefinition function.

visitFieldDefinition(field) {
    …
    field.resolve = async function (source, { requires, otherArgs }, context, info) {
      …
      if (connection.type !== _requires) {
        return null
      }
      …
    };
}

In this case, if the relation between the users isn't correct, simply return a null or any other specific value it is up to you. For example, you can define a specific error field that contains the reason for the failed resolve. The last thing we need to do is to register the created directive in our Apollo Server:

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

const server = new ApolloServer({
  typeDefs,
  schemaDirectives: {
    relation: RelationDirective,
  }
});

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

Alternatively, if you want to modify an existing schema object, you can use the SchemaDirectiveVisitor.visitSchemaDirectives interface directly:

SchemaDirectiveVisitor.visitSchemaDirectives(schema, {
  relation: RelationDirective
});

And this is it, now you define private and public field in you GraphQL schema and you don’t have to define separate queries, return types for users with different permissions.

Summary

In this article, we built a custom directive that makes it possible to define private fields on our GraphQL schema. It makes it easy to handle query return types differently based on the current user’s permissions. Because the directives are part of the GraphQL specification, any other well-constructed GraphQL implementation makes possible to build your directives, it is not a NodeJS or Apollo-specific feature, so if you are using other libraries or another programing language you can check how the toolset supports the GraphQL directives I am sure you will find a solution for that.

If you want to read more about GraphQL schema directives, here are some useful links. Happy learning guys! 🤓