How to integrate GraphQL with SailsJS application
Tags: sailsjs, graphql, javascript, node
Published: 2020-03-12 21:57:38

## Introduction
This article is an attempt to explain how to integrate `graphql` with `SailsJS` application. For the last 3 years I am actively working on projects that are based on NodeJS technology stack. For server-side development, the platform of choice is `SailsJS` and for client-side development I use mainly `Angular` and `Vue`. Graphql being so powerful, I wanted to leverage its power in my projects so as to reduce and eliminate the shortcomings of the `Waterline` ORM, that drives the database operations, such as missing multi-level referential entity fetch, etc. I could not find any article on how to do it. After lot of trial and error, I have an implementation that is working properly with custom directives for authentication and authorization on graphql resources and I believe is release worthy now.

**CAUTION:** *The goal of this article is to explain how to integrate GraphQL with SailsJS projects. It is not my intention to teach GraphQL here. There are many good articles and documentation available on net for the same.*
## Prerequisites
The following should be pre-installed on your PC/workstation
- NodeJS - v10+
- SailsJS CLI - latest version, globally installed

**CAUTION:** *I will be working on a Linux machine so any commands that use paths will use the linux/unix style. If you work on Windows machine then change the paths accordingly.*
## Project
From now on I would address myself as we, I am considering the reader i.e., `you` as a member of the team. So learn and enjoy with me. 

The example project that we will work on in this article will not use a web application rather it will be an `API` server only. For the frontend, we will use `Postman` for calling various graphql queries and mutations. We will not use the third operation supported by graphql that is `subscriptions`. It is left for you to try in your own projects, in case you need `pub-sub` functionality.

We will define 2 waterline models 
- Book
- Author

And write the associated graphql schema, user security & access control custom directives, queries, mutations and resolvers to implement CRUD operations. Although user security and access control is not required for this example project but it is essential to learn how to do it in a real project, hence, we will implement this feature too.
## Create a Sails project
In your terminal/command window type and execute the following command to create a minimal project.
```
sails new sails-graphql --without=session,views
```
Now, we will install the graphql npm packages that are relevant for our project.
```
cd sails-graphql
npm install graphql graphql-tools express-graphql
```
For database support, we will use the pre-configured `sails-disk` ORM adapter. Set the `migrate` property to `alter` in `config/models.js` before lifting the sails server.
## Define SailsJS models
Create the following two models in `api/models/` folder of your project.
1. `Book.js` 
2. `Author.js`
```
/**
 * Book.js
 *
 * @description :: A model definition.  Represents a database table/collection/etc.
 * @docs        :: https://sailsjs.com/docs/concepts/models-and-orm/models
 */

module.exports = {

  attributes: {

    //  ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦  ╦╔═╗╔═╗
    //  ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
    //  ╩  ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝

    title: {
      type: 'string',
      required: true,
      unique: true
    },

    yearPublished: {
      type: 'string',
      required: true
    },

    genre: {
      type: 'string',
      isIn: ['ADVENTURE', 'COMICS', 'FANTASY', 'UNKNOWN'],
      defaultsTo: 'UNKNOWN'
    },

    //  ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
    //  ║╣ ║║║╠╩╗║╣  ║║╚═╗
    //  ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝


    //  ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
    //  ╠═╣╚═╗╚═╗║ ║║  ║╠═╣ ║ ║║ ║║║║╚═╗
    //  ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝

    author: {
      model: 'Author',
      required: true
    }

  }

};

```
```
/**
 * Author.js
 *
 * @description :: A model definition.  Represents a database table/collection/etc.
 * @docs        :: https://sailsjs.com/docs/concepts/models-and-orm/models
 */

module.exports = {

  attributes: {

    //  ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦  ╦╔═╗╔═╗
    //  ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
    //  ╩  ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝

    name: {
      type: 'string',
      required: true,
      unique: true
    },

    country: {
      type: 'string',
      defaultsTo: 'UNKNOWN'
    },

    //  ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
    //  ║╣ ║║║╠╩╗║╣  ║║╚═╗
    //  ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝


    //  ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
    //  ╠═╣╚═╗╚═╗║ ║║  ║╠═╣ ║ ║║ ║║║║╚═╗
    //  ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝

    books: {
      collection: 'Book',
      via: 'author'
    }

  }

};

```
## Define GraphQL Schema, Policies and Helpers
Create the following folder structure where our various artifacts will live.
```
  api
	|
	-- graphql
		|
		-- helpers
		|
		-- policies
		|
		-- schemas
```
### Policies
Let us first define our policies and place the artifacts in the `api/graphql/policies` folder. We will implement JWT authentication and role-based authorization strategy, the sample code should be enhanced or totally changed as per your project requirement, the idea is to make you understand how and where to implement them. You are free to change to your own strategy. Create two files 
1. `auth.js`
2. `permission.js`
```
/**
 * auth.js
 *
 * A simple policy that
 *  a) establishes identity of a user based on a jwt token
 *  b) allow access to resources based on role-based ACL
 *
 */

 const { checkPermission } = require('./permission');

module.exports = {
  _authenticate: async (context) => {

    let req = context.req;

    /* Uncomment this sample code and adapt to implement your own JWT authentication
    let message = 'Access denied. You need to be loggedin to access this resource.';

    if (
      !req ||
      !req.headers ||
      (!req.headers.authorization && !req.headers.Authorization)
    ) {
      return {
        errors: [
          {
            code: 'I_AUTHTOKEN_MISSING',
            message: message
          }
        ]
      };
    }

    let token = req.headers.authorization || req.headers.Authorization;
    // Check presence of Auth Token and decode
    if (!token) {
      // Otherwise, this request did not come from a logged-in user.
      return {
        errors: [
          {
            code: 'I_AUTHTOKEN_MISSING',
            message: message
          }
        ]
      };
    }

    if (!token.startsWith('Bearer ')) {
      // Otherwise, this request did not come from a logged-in user.
      return {
        errors: [
          {
            code: 'E_AUTHTYPE_INVALID',
            message: message
          }
        ]
      };
    }

    token = token.substring(7);
    let result = {};
    try {
      result = await TokenService.decode({token: token});
    } catch (err) {
      sails.log.error('auth._authenticate: Error encountered: ', err);
      return {
        errors: [
          {
            code: 'E_DECODE',
            message: message
          }
        ]
      };
    }

    const now = Date.now() / 1000;
    if (result.exp <= now) {
      sails.log.info(`auth._authenticate: Access denied for: [${result.userName}] as the Auth Token has expired.`);
      return {
        errors: [
          {
            code: 'I_TOKEN_EXPIRED',
            message: message
          }
        ]
      };
    }
    */

    // When you implement your own authentication mechanism, 
    // remove the hard-coded result variable below.
    let result = {
      id: 1,
      fullName: 'Test',
      emailAddress: 'test@test.test',
      isRoleAdmin: false,
      roleId: 1
    };

    // Set the user object in graphql object for reference in subsequent processing
    context.user = result;
    return result;
  }, // end _authenticate()

  _authorize: async (user, expectedScope) => {
    let isAllowed = false;

    const scopeSplit = expectedScope.toLowerCase().split(':');
    const resource = scopeSplit[0].trim();
    const permission = scopeSplit[1].trim();
    if (scopeSplit.length > 2) {
      if (scopeSplit[2] === 'admin') {
        if (user.isRoleAdmin) {
          isAllowed = await checkPermission(user.roleId, permission, resource);
        }
      }
    } else {
      isAllowed = await checkPermission(user.roleId, permission, resource);
    }

    if (!isAllowed) {
      sails.log.info('auth._authorize: Access denied for: ');
      sails.log.info('  User:', user.fullName, '(' + user.emailAddress + ')');
      sails.log.info('  Valid Resource:Scope is: ', expectedScope);
    }
    return isAllowed;
  } // end _authorize()

};
```
```
/**
 * permission.js
 *
 * A simple policy for implementing RBAC
 *
 */

module.exports = {
  checkPermission: (roleId, permission, resource) => {
    console.log(`checkPermission() Role Id: ${roleId}, Permission: ${permission}, Resource: ${resource}`);

    // add your RBAC code here and return true for allow or false for disallow

    return true; // allow
  }
};
```
The above code is simple and self explanatory. The `auth.js` defines two functions `_authenticate` that gets the JWT from the HTTP Request header and decodes it. The second `_authorize` checks for RBAC permissions on the said resource/artifact. 
The `permission.js` defines a single function called `checkPermission` that is supposed to implement how you want to define your resource/artifact permission matrix for each role and then appropriately return `true` for allow access or `false` for deny access. 

If you have used GraphQL before you may know that the standard libraries generate and send very cryptic and confusing error messages to the client. Therefore, to simplify and provide a consistent interface to the client, result and error objects will be sent in the body of the POST response.

Please pay attention to the following code fragment that returns an object for error instead of throwing `GraphQLError`.
```
      return {
        errors: [
          {
            code: 'E_AUTHTYPE_INVALID',
            message: message
          }
        ]
      };
```
This way we can send rich and clear error message to the client.
### Schema
#### Common Schema Artifacts
First we will define the common atrtifacts of our schema that will be used by each SailsJS model schema and place them in `api/graphql/schemas/schema.js`. A seperate schema file will be created for each model in our project. Finally we will import the sections of the model schemas in `schema.js`. Therefore, incomplete schema.js is given below for understanding the common artifacts.
```
/**
 * schema.js (Incomplete)
 */
const { makeExecutableSchema } = require('graphql-tools');
const { _authenticate, _authorize } = require('../policies/auth');

// Construct a schema using the GraphQL schema language
const typeDefs = `
  directive @authenticate on FIELD_DEFINITION | FIELD
  directive @authorize(scope: String!) on FIELD_DEFINITION | FIELD

  type Error {
    code: String!
    message: String!
    attrName: String
    row: Int
    moduleError: ModuleError
  }

  type ModuleError {
    code: String!
    message: String!
    attrNames: [String]
  }

  type ErrorResponse {
    errors: [Error]
  }
  
  # model types will be added here
  # TODO

  type Query {
    # model query declaration will be added here
    # TODO
  }

  type Mutation {
    # model mutation declaration will be added here
    # TODO
  }
`;

// Provide resolver functions for your schema fields
const resolvers = {
  Query: {
    # model query resolver code will be added here
    # TODO
  },

  Mutation: {
    # model mutation resolver code will be added here
    # TODO
  },
  
  # model references resolvers code will be added here
  # TODO
};

const directiveResolvers = {
  // Will be called when a @authenticate directive is applied to a field or field definition.
  async authenticate(resolve, parent, directiveArgs, context, info) {
    if (context.user === undefined) {
      user = await _authenticate(context);
      if (user.errors !== undefined) {
        return user; // user authentication failed
      }
    }
    return resolve();
  },

  // Will be called when a @authorize directive is applied to a field or field definition.
  async authorize(resolve, parent, directiveArgs, context, info) {
    if (!await _authorize(context.user, directiveArgs.scope)) {
      return {
        errors: [
          {
            code: 'E_NO_PERMISSION',
            message: 'Expected resource Authorization: ' + directiveArgs.scope
          }
        ]
      };
    }
    return resolve();
  }
};

// Get a GraphQL.js Schema object
module.exports.schema = makeExecutableSchema({
  typeDefs,
  resolvers,
  directiveResolvers
});

```
Let us attempt to explain sections of this schema definition.
##### Custom Directives
We have declared two custom directives in the `typeDefs` section named `@authenticate` and `@authorize`.
```
  directive @authenticate on FIELD_DEFINITION | FIELD
  directive @authorize(scope: String!) on FIELD_DEFINITION | FIELD

```
@authenticate has no arguments that means when you refer to it in your code you will not pass any parameters to it. The JWT is extracted from the HTTP request headers and the `req` object will be supplied by graphql runtime in the `context` variable. We can define what context is when we register graphql as a middleware in SailsJS.

@authorize has one argument named `scope` that is of `String` type. Notice that it has a trailing `!`, this means it is required (mandatory). You will pass the constraint to be checked, for example, `book:read` which translates to "if the logged in user has read access to book then allow access otherwise deny access". The structure of the constraint value is `resource:constraint_type:admin`. As you can see it has 3 parts delimited by a colon, first is the resource/artifact name, second is the constraint and the third is optional and is fixed as `admin` to declare that only the role administrator can have access to the resource and constraint type in question. We have implemented four constraint types viz. `read`, `add`, `update` and `delete`.

**NOTE:** For this example project, we have a scalar constraint but it is possible to enhance the functionality to say passing an array of constraints.
##### Global Schema Types
```
  type Error {
    code: String!
    message: String!
    attrName: String
    row: Int
    moduleError: ModuleError
  }

  type ModuleError {
    code: String!
    message: String!
    attrNames: [String]
  }

  type ErrorResponse {
    errors: [Error]
  }
```
We have defined a global error response type `ErrorResponse` that is an array of `Error` type objects. We will return this response type for all our application errors. `Error` type fields are explained below:
- `code` - application specific error classifiers (mandatory)
- `message` - application specific error message (mandatory)
- `attrName` - name of field / attribute that has erroneous value (optional)
- `row` - row number of the attribute if the input is an array (optional)
- `moduleError` - this is a special object that holds the error message generated by sails/waterline for any framework related exceptions (optional)

##### Custom Directive Resolvers
This section of the code defines the functions for each custom directive declared before in the schema.
```
const directiveResolvers = {
  // Will be called when a @authenticate directive is applied to a field or field definition.
  async authenticate(resolve, parent, directiveArgs, context, info) {
    if (context.user === undefined) {
      user = await _authenticate(context);
      if (user.errors !== undefined) {
        return user; // user authentication failed
      }
    }
    return resolve();
  },

  // Will be called when a @authorize directive is applied to a field or field definition.
  async authorize(resolve, parent, directiveArgs, context, info) {
    if (!await _authorize(context.user, directiveArgs.scope)) {
      return {
        errors: [
          {
            code: 'E_NO_PERMISSION',
            message: 'Expected resource Authorization: ' + directiveArgs.scope
          }
        ]
      };
    }
    return resolve();
  }
};
```
The code here is self explanatory. The only specific thing to learn is the function signature which is explained below:
- `resolve` - It is the default field resolver that is coming from the graphql library
- `parent` - It is the parent node's data object. If you need any value from the parent node then you can get it from here
- `directiveArgs` - This is the object that contains your directive parameters. In our case `@authorize(scope: "book:read")` declaration will get passed as `{ scope: "book:read" }`
- `context` - This is the global graphql context and will contain whatever you set while registering the `express-graphql` middleware
- `info` - This contains lot of information and AST of your query. Usually we do not use it. Refer to graphql documentation for a complete explanation

**NOTE:** Where you call the default resolve function in your custom code depends upon the functionality of your directive. In both our directives either we return an error or at the end return with a call to the default resolve function. However, there may be cases where you need the value of your current node then you will call the default resolve function first to get the value and then manipulate it as per the functionality of your directive. For example, @uppercase, here you will call the default resolve first and then convert the resultant value to uppercase and then return it.
##### Compile the declarative schema into executable one
This section explains how to compile the declarative schema to a state that the graphql runtime understands.
```
// Get a GraphQL.js Schema object
module.exports.schema = makeExecutableSchema({
  typeDefs,
  resolvers,
  directiveResolvers
});
```
`makeExecutableSchema` comes from the `graphql-tools` library package. We pass only three parameters that are relevant to our project. You may have a look at the detailed number of parameters that it can accept at `graphql-tools` github page.
#### Author Schema
```
/**
 * AuthorSchema.js
 */
const { _getAuthor, _addAuthor, _updateAuthor, _deleteAuthor } = require('../helpers/AuthorHelper');
const { _getBook } = require('../helpers/BookHelper');

module.exports = {
  typeDefs: {
    types: `
      # model=Author
      type Author {
        # Unique identifier (Primary key in database for this model entity)
        id: Int!
        # Name
        name: String!
        # Country
        country: String
        # Books
        books: [Book] @authorize(scope: "book:read")
      }

      input AuthorInput {
        name: String
        country: String
      }

      # define unions
      union AuthorResponse = Author | ErrorResponse

    `, // end of types

    queries: `
      getAuthors(filter: String): [AuthorResponse] @authorize(scope: "author:read") @authenticate
      getAuthor(id: Int!): AuthorResponse @authorize(scope: "author:read") @authenticate
    `, // end of queries

    mutations: `
      addAuthor(data: AuthorInput!): AuthorResponse @authorize(scope: "author:add") @authenticate
      updateAuthor(id: Int!, data: AuthorInput!): AuthorResponse @authorize(scope: "author:update") @authenticate
      deleteAuthor(id: Int!): AuthorResponse @authorize(scope: "author:delete") @authenticate
    `, // end of mutations
  }, // end of typeDefs

  resolvers: {
    queries: {
      getAuthors: async (parent, args, context) => {
        const result = await _getAuthor({ where: args.filter });
        if (!(result instanceof Array)) {
          return [ result ];
        }
        if (result.length === 0) {
          return [ { errors: [ { code: 'I_INFO', message: 'No data matched your selection criteria'}]} ];
        }
        return result;
      },
      getAuthor: async (parent, args, context) => {
        return await _getAuthor(args);
      },
    },

    mutations: {
      addAuthor: async (parent, args, context) => {
        return await _addAuthor(args.data);
      },
      updateAuthor: async (parent, args, context) => {
        return await _updateAuthor(args.id, args.data);
      },
      deleteAuthor: async (parent, args, context) => {
        return await _deleteAuthor(args.id);
      },
    },

    references: {

      Author: {
        books: async (author, _, context) => {
          if (author === null) {
            return null;
          }
          const args = {
            where: {
              author: author.id
            }
          };
          const result = await _getBook(args);
          if (!(result instanceof Array)) {
            return [ result ];
          }
          return result;
        },

      },

      AuthorResponse: {
        __resolveType(obj, context, info) {
          if (obj.errors) {
            return 'ErrorResponse';
          } else {
            return 'Author';
          }
        },
      },

    } // end of references
  } // end of resolvers
};
```
Let us dissect the author schema, the `Author` type mimics the attributes and properties of each attribute directly from your waterline model, it is 1-to-1 correspondence. The `@authorize(scope: "book:read")` directive on the collection of books seems ridiculous and I agree. I have declared it just to illustrate that it can be done to stop access to book collection owned by the author requested in your query. If you want to allow access to all and sundry then remove the directive declaration.

For mutations we need to explicitly define `input` type hence `AuthorInput`. One thing I want to highlight is that we have not made any field mandatory, This has been done deliberately to use the same input type for `add` as well as `update` mutations. For add, we need to pass all the fields where as for update, only selective fields will be passed. So, effectively I am bypassing the graphql validation rules and handling field validations in my schema resolver helper functions. Remember, I had mentioned, the errors thrown by graphql are very cryptic and to circumvent that we defined our own global error type. Alternatively, if you are not happy with this arrangement, you can define two input types, one for add with the mandatory fields marked and second for update without marking any field mandatory.

We have defined `AuthorResponse` as a union of two types to return either a valid Author object or an `ErrorResponse`. Therefore, we need to tell graphql runtime what kind of reply we will return so that the front-end application can interpret what kind of object has been received as result. The following code snippet implements the code that graphql will call to identify the object type of the response.
```
      AuthorResponse: {
        __resolveType(obj, context, info) {
          if (obj.errors) {
            return 'ErrorResponse';
          } else {
            return 'Author';
          }
        },
      },
```
The argument `obj` is essentially the result that our query is returning. Recall that we return our application errors as `{ errors: [ {}, ...] }`, hence we check the existence of `errors` key in the object, if it exists then we return `ErrorResponse` else we return `Author`.

The rest of the code for `queries`, `mutations`declaration and implementation of the corresponding `resolvers` is pretty standard graphql, no need to explain. However, we will discuss an issue with multiple directive declaration on the same field in `graphql-tools`. Examine closely the following query declaration, do you see any problem/peculiarity?
```
getAuthors(filter: String): [AuthorResponse] @authorize(scope: "author:read") @authenticate
```
To a sane person the order of directive declaration should be `@authenticate @authorize(scope: "author:read")` isn't it? First authenticate the user and then check for permissions. But in the code we have reversed them because graphql-tools scans them from LTR but executes them RTL. This [bug](https://github.com/apollographql/graphql-tools/issues/630#issuecomment-365158687) was raised way back in February, 2018. Unfortunately, after two years it is still not fixed.

Examine the following code snippets.
```
getAuthors(filter: String): [AuthorResponse] @authorize(scope: "author:read") @authenticate
```
```
      getAuthors: async (parent, args, context) => {
        const result = await _getAuthor({ where: args.filter });
        if (!(result instanceof Array)) {
          return [ result ];
        }
        if (result.length === 0) {
          return [ { errors: [ { code: 'I_INFO', message: 'No data matched your selection criteria'}]} ];
        }
        return result;
      },
```
The first snippet declares the `getAuthors` and second implements it. The declaration says the function must return an array of `AuthorResponse`. Therefore the implementation checks the returned result from the helper function,
- if it is not an array then it converts into an array. When will such a situation arrise? When the helper function returns an `Error` object which will certainly not be an array.
- if the array is empty then it returns an array of Error object. As far as the helper function is concerned it will return an empty array, if no data matches for the filter passed but there are fields in Author type that are mandatory (`id`, `name`). So if we return an empty array, graphql runtime will throw an error.

Examine the following code snippet in the `references`.
```
      Author: {
        books: async (author, _, context) => {
          if (author === null) {
            return null;
          }
          const args = {
            where: {
              author: author.id
            }
          };
          const result = await _getBook(args);
          if (!(result instanceof Array)) {
            return [ result ];
          }
          return result;
        },

      },
```
This is equivalent to a `populate()` call in SailsJS.
At present, we cannot get data from second level onwards using populate() and there are other shortcomings of populate() such as it does not allow selection of field lists.
The good thing about graphql is that it resolves each node of a query one-by-one starting from the root of the query, therefore, we can fetch data from multiple levels of references irrespective of the depth. Also, we can select data fields on each node as per the query request.
#### Book Schema
```
 /**
 * BookSchema.js
 */
const { _getBook, _addBook, _updateBook, _deleteBook } = require('../helpers/BookHelper');
const { _getAuthor } = require('../helpers/AuthorHelper');

module.exports = {
  typeDefs: {
    types: `
      # model=Book
      type Book {
        # Unique identifier (Primary key in database for this model entity)
        id: Int!
        # Title
        title: String!
        # Year Published
        yearPublished: String!
        # Genre
        genre: String
        # Author
        author: Author! @authorize(scope: "author:read")
      }

      input BookInput {
        title: String
        yearPublished: String
        genre: String
        authorId: Int
      }

      # define unions
      union BookResponse = Book | ErrorResponse

    `, // end of types

    queries: `
      getBooks(filter: String): [BookResponse] @authorize(scope: "book:read") @authenticate 
      getBook(id: Int!): BookResponse @authorize(scope: "book:read") @authenticate
    `, // end of queries

    mutations: `
      addBook(data: BookInput!): BookResponse @authorize(scope: "book:add") @authenticate
      updateBook(id: Int!, data: BookInput!): BookResponse @authorize(scope: "book:update") @authenticate
      deleteBook(id: Int!): BookResponse @authorize(scope: "book:delete") @authenticate
    `, // end of mutations
  }, // end of typeDefs

  resolvers: {
    queries: {
      getBooks: async (parent, args, context) => {
        const result = await _getBook({ where: args.filter });
        if (!(result instanceof Array)) {
          return [ result ];
        }
        if (result.length === 0) {
          return [ { errors: [ { code: 'I_INFO', message: 'No data matched your selection criteria'}]} ];
        }
        return result;
      },
      getBook: async (parent, args, context) => {
        return await _getBook(args);
      },
    },

    mutations: {
      addBook: async (parent, args, context) => {
        return await _addBook(args.data);
      },
      updateBook: async (parent, args, context) => {
        return await _updateBook(args.id, args.data);
      },
      deleteBook: async (parent, args, context) => {
        return await _deleteBook(args.id);
      },
    },

    references: {

      Book: {
        author: async (book, _, context) => {
          if (book === null) {
            return null;
          }
          const args = {
            id: book.author
          };
          return await _getAuthor(args);
        },

      },

      BookResponse: {
        __resolveType(obj, context, info) {
          if (obj.errors) {
            return 'ErrorResponse';
          } else {
            return 'Book';
          }
        },
      },

    } // end of references
  } // end of resolvers
};
```
The Book schema is similar to Author schema, therefore, does not need any explanation.
#### Import the model schemas
Now, we will import the model schema artifacts in the main `schema.js` file.
Add the import of the models
```
const book = require('./BookSchema');
const author = require('./AuthorSchema');
```
Now, import the model artifacts. Add the following code in the `typeDefs` variable.
```
  ${book.typeDefs.types}
  ${author.typeDefs.types}

  type Query {
    ${book.typeDefs.queries}
    ${author.typeDefs.queries}
  }

  type Mutation {
    ${book.typeDefs.mutations}
    ${author.typeDefs.mutations}
  }

```
Add the model query, mutation and refrences resolvers to the `resolvers` variable.
```
const resolvers = {
  Query: {
    ...book.resolvers.queries,
    ...author.resolvers.queries
  },

  Mutation: {
    ...book.resolvers.mutations,
    ...author.resolvers.mutations
  },
  ...book.resolvers.references,
  ...author.resolvers.references
};
```
So here is the complete code of schema.js.
```
/**
 * schema.js
 */
const { makeExecutableSchema } = require('graphql-tools');
const { _authenticate, _authorize } = require('../policies/auth');
const book = require('./BookSchema');
const author = require('./AuthorSchema');

// Construct a schema using the GraphQL schema language
const typeDefs = `
  directive @authenticate on FIELD_DEFINITION | FIELD
  directive @authorize(scope: String!) on FIELD_DEFINITION | FIELD

  type Error {
    code: String!
    message: String!
    attrName: String
    row: Int
    moduleError: ModuleError
  }

  type ModuleError {
    code: String!
    message: String!
    attrNames: [String]
  }

  type ErrorResponse {
    errors: [Error]
  }

  ${book.typeDefs.types}
  ${author.typeDefs.types}

  type Query {
    ${book.typeDefs.queries}
    ${author.typeDefs.queries}
  }

  type Mutation {
    ${book.typeDefs.mutations}
    ${author.typeDefs.mutations}
  }
`;

// Provide resolver functions for your schema fields
const resolvers = {
  Query: {
    ...book.resolvers.queries,
    ...author.resolvers.queries
  },

  Mutation: {
    ...book.resolvers.mutations,
    ...author.resolvers.mutations
  },
  ...book.resolvers.references,
  ...author.resolvers.references
};

const directiveResolvers = {
  // Will be called when a @authenticate directive is applied to a field or field definition.
  async authenticate(resolve, parent, directiveArgs, context, info) {
    if (context.user === undefined) {
      user = await _authenticate(context);
      if (user.errors !== undefined) {
        return user; // user authentication failed
      }
    }
    return resolve();
  },

  // Will be called when a @authorize directive is applied to a field or field definition.
  async authorize(resolve, parent, directiveArgs, context, info) {
    if (!await _authorize(context.user, directiveArgs.scope)) {
      return {
        errors: [
          {
            code: 'E_NO_PERMISSION',
            message: 'Expected resource Authorization: ' + directiveArgs.scope
          }
        ]
      };
    }
    return resolve();
  }
};

// Get a GraphQL.js Schema object
module.exports.schema = makeExecutableSchema({
  typeDefs,
  resolvers,
  directiveResolvers
});
```
### Helpers
Helper functions are equivalent to SailsJS's controller/actions that are called by the graphql resolvers to interact with the underlying database layer to implement CRUD operations. Each of the helper implements four functions and each function does its own input validations.
#### BookHelper
```
 /**
 * BookHelper.js
 *
 * @description :: Server-side actions for handling incoming requests.
 */

module.exports = {

  /*
   * @Function:     _addBook(input)
   * @Description:  Add one record of Book
   * @Params:       input - dictionary of fields to be added
   * @Return:       Book | ErrorResponse
   */
  _addBook: async (input) => {
    let validValuesArray = [];
    const title = input.title;
    const yearPublished = input.yearPublished;
    const genre = input.genre || 'UNKNOWN';
    const authorId = parseInt(input.authorId);

    let payLoad = {};

    // Validate user input

    if (title === undefined) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'title',
            message: 'Title is required and should be of type "string"'
          }
        ]
      };
    }

    if (typeof title !== 'string') {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'title',
            message: 'Title should be of type "string"'
          }
        ]
      };
    }

    if (yearPublished === undefined) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'yearPublished',
            message: 'Year Published is required and should be of type "string"'
          }
        ]
      };
    }

    if (typeof yearPublished !== 'string') {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'yearPublished',
            message: 'Year Published should be of type "string"'
          }
        ]
      };
    }

    if (genre === undefined) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'genre',
            message: 'Genre is required and should be one of "\'ADVENTURE\', \'COMICS\', \'FANTASY\', \'UNKNOWN\'"'
          }
        ]
      };
    }

    if (typeof genre !== 'string') {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'genre',
            message: 'Genre should be of type "string"'
          }
        ]
      };
    }

    validValuesArray = ['ADVENTURE','COMICS','FANTASY','UNKNOWN'];
    if (validValuesArray.find((val) => genre === val) === undefined) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'genre',
            message: 'Genre should be one of "\'ADVENTURE\', \'COMICS\', \'FANTASY\', \'UNKNOWN\'"'
          }
        ]
      };
    }

    if (authorId === undefined || Number.isNaN(authorId)) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'authorId',
            message: 'Author Id is required and should be of type "integer"'
          }
        ]
      };
    }

    // All input validated, now set the payLoad values
    payLoad.title = title;
    payLoad.yearPublished = yearPublished;
    payLoad.genre = genre;
    payLoad.author = authorId;

    try {
      let result = null;
      // insert new record
      result = await Book.create(payLoad).fetch();

      // Success
      sails.log.debug(`BookHelper._addBook: Book successfully added:`, result);
      return result;
    } catch (err) {
      sails.log.debug('BookHelper._addBook: Exception encountered:', err);
      return {
        errors: [
          {
            code: 'E_API_ERROR',
            message: `Book add request failed.`,
            moduleError: {
              code: err.code || 'E_ERROR',
              attrNames: err.attrNames || [],
              message: err.message
            }
          }
        ]
      };
    } // end try {}
  }, // end _addBook()

  /*
   * @Function:     _updateBook(id, input)
   * @Description:  Update one record of Book
   * @Params:       id - Book Id
   *                input - dictionary of rest of fields to be updated
   * @Return:       Book | ErrorResponse
   */
  _updateBook: async (id, input) => {
    let validValuesArray = [];

    // for new or update record
    const title = input.title;
    const yearPublished = input.yearPublished;
    const genre = input.genre;
    const authorId = input.authorId ?  parseInt(input.authorId) : undefined;

    if (!id) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'id',
            message: 'Id is required for updation.'
          }
        ]
      };
    }

    let valueNotSet = true;
    let payLoad = {};
    // now set the payLoad value(s)

    if (title !== undefined) {

      if (typeof title !== 'string') {
        return {
          errors: [
            {
              code: 'E_BAD_INPUT',
              attrName: 'title',
              message: 'Title should be of type "string"'
            }
          ]
        };
      }

      valueNotSet = false;
      payLoad.title = title;
    } // end if

    if (yearPublished !== undefined) {

      if (typeof yearPublished !== 'string') {
        return {
          errors: [
            {
              code: 'E_BAD_INPUT',
              attrName: 'yearPublished',
              message: 'Year Published should be of type "string"'
            }
          ]
        };
      }

      valueNotSet = false;
      payLoad.yearPublished = yearPublished;
    } // end if

    if (genre !== undefined) {

      if (typeof genre !== 'string') {
        return {
          errors: [
            {
              code: 'E_BAD_INPUT',
              attrName: 'genre',
              message: 'Genre should be of type "string"'
            }
          ]
        };
      }

      validValuesArray = ['ADVENTURE','COMICS','FANTASY','UNKNOWN'];
      if (validValuesArray.find((val) => genre === val) === undefined) {
        return {
          errors: [
            {
              code: 'E_BAD_INPUT',
              attrName: 'genre',
              message: 'Genre should be one of "\'ADVENTURE\', \'COMICS\', \'FANTASY\', \'UNKNOWN\'"'
            }
          ]
        };
      }

      valueNotSet = false;
      payLoad.genre = genre;
    } // end if

    if (!(authorId === undefined || Number.isNaN(authorId))) {

      valueNotSet = false;
      payLoad.author = authorId;
    } // end if

    if (valueNotSet) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: '',
            message: 'No value(s) sent for updation.'
          }
        ]
      };
    }

    try {
      let result = await Book.updateOne()
        .set(payLoad)
        .where({
          id: id
        }); // .fetch() not required for updateOne() as it always returns the updated record or undefined if not found

      // Success
      result = result || { errors: [ { code: 'I_INFO', message: `No Book exists with the requested Id: ${id}`} ] };
      sails.log.debug(`BookHelper._updateBook: Book successfully updated:`, result);
      return result;
    } catch (err) {
      sails.log.debug('BookHelper._updateBook: Exception encountered:', err);
      return {
        errors: [
          {
            code: 'E_API_ERROR',
            message: `Book update request failed.`,
            moduleError: {
              code: err.code || 'E_ERROR',
              attrNames: err.attrNames || [],
              message: err.message
            }
          }
        ]
      };
    } // end try {}
  }, // end _updateBook()

  /*
   * @Function:     _deleteBook(id)
   * @Description:  Delete one record of Book
   * @Params:       id - Book Id
   * @Return:       Book | ErrorResponse
   */
  _deleteBook: async (id) => {
    if (!id) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'id',
            message: 'Id is required for deletion.'
          }
        ]
      };
    }

    try {
      let result = null;

      result = await Book.destroyOne({id});
      // Success
      result = result || { errors: [ { code: 'I_INFO', message: `No Book exists with the requested Id: ${id}`} ] };
      sails.log.debug(`BookHelper._deleteBook: Book successfully deleted:`, result);
      return result;
    } catch (err) {
      sails.log.debug('BookHelper._deleteBook: Exception encountered:', err);
      return {
        errors: [
          {
            code: 'E_API_ERROR',
            message: `Book delete request failed.`,
            moduleError: {
              code: err.code || 'E_ERROR',
              attrNames: err.attrNames || [],
              message: err.message
            }
          }
        ]
      };
    } // end try {}
  }, // end _deleteBook()

  /*
   * @Function:     _getBook(input)
   * @Description:  Fetch one or more record(s) of Book
   * @Params:       input - dictionary with either Book Id or a filter criteria
   * @Return:       Book | [Book] | ErrorResponse
   */
  _getBook: async (input) => {
    const id = input.id;
    let where = input.where || {};

    if (typeof where === 'string') {
      try {
        where = JSON.parse(where);
      } catch(err) {
        return {
          errors: [
            {
              code: 'E_BAD_INPUT',
              attrName: 'where',
              message: 'Where clause should be a valid JSON object.'
            }
          ]
        };
      } // end try
    }

    if (id) {
      where.id = id;
    }

    try {
      // Now fetch the record(s) from database
      let result = await Book.find().where(where);

      if (id) {
        if (result.length > 0) {
          result = result[0];
        } else {
          result = { errors: [ { code: 'I_INFO', message: `No Book exists with the requested Id: ${id}`} ] };
        }
      }

      // Success
      sails.log.debug(`BookHelper._getBook: Book(s) successfully retrieved:`, result);
      return result;
    } catch(err) {
      sails.log.debug('BookHelper._getBook: Exception encountered:', err);
      return {
        errors: [
          {
            code: 'E_API_ERROR',
            message: 'Book fetch request failed.',
            moduleError: {
              code: err.code || 'E_ERROR',
              attrNames: err.attrNames || [],
              message: err.message
            }
          }
        ]
      };
    } // end try {}
  }, // end _getBook()
};
```
#### AuthorHelper
```
/**
 * AuthorHelper.js
 *
 * @description :: Server-side actions for handling incoming requests.
 */

module.exports = {

  /*
   * @Function:     _addAuthor(input)
   * @Description:  Add one record of Author
   * @Params:       input - dictionary of fields to be added
   * @Return:       Author | ErrorResponse
   */
  _addAuthor: async (input) => {
    const name = input.name;
    const country = input.country || 'UNKNOWN';
    let payLoad = {};

    // Validate user input

    if (name === undefined) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'name',
            message: 'Name is required and should be of type "string"'
          }
        ]
      };
    }

    if (typeof name !== 'string') {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'name',
            message: 'Name should be of type "string"'
          }
        ]
      };
    }

    if (country === undefined) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'country',
            message: 'Country is required and should be of type "string"'
          }
        ]
      };
    }

    if (typeof country !== 'string') {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'country',
            message: 'Country should be of type "string"'
          }
        ]
      };
    }

    // All input validated, now set the payLoad values
    payLoad.name = name;
    payLoad.country = country;

    try {
      // insert new record
      let result = await Author.create(payLoad).fetch();

      // Success
      sails.log.debug(`AuthorHelper._addAuthor: Author successfully added:`, result);
      return result;
    } catch (err) {
      sails.log.debug('AuthorHelper._addAuthor: Exception encountered:', err);
      return {
        errors: [
          {
            code: 'E_API_ERROR',
            message: `Author add request failed.`,
            moduleError: {
              code: err.code || 'E_ERROR',
              attrNames: err.attrNames || [],
              message: err.message
            }
          }
        ]
      };
    } // end try {}
  }, // end _addAuthor()

  /*
   * @Function:     _updateAuthor(id, input)
   * @Description:  Update one record of Author
   * @Params:       id - Author Id
   *                input - dictionary of rest of fields to be updated
   * @Return:       Author | ErrorResponse
   */
  _updateAuthor: async (id, input) => {
    const name = input.name;
    const country = input.country;

    if (!id) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'id',
            message: 'Id is required for updation.'
          }
        ]
      };
    }

    let valueNotSet = true;
    let payLoad = {};
    // now set the payLoad value(s)
    if (name !== undefined) {

      if (typeof name !== 'string') {
        return {
          errors: [
            {
              code: 'E_BAD_INPUT',
              attrName: 'name',
              message: 'Name should be of type "string"'
            }
          ]
        };
      }

      valueNotSet = false;
      payLoad.name = name;
    } // end if

    if (country !== undefined) {

      if (typeof country !== 'string') {
        return {
          errors: [
            {
              code: 'E_BAD_INPUT',
              attrName: 'country',
              message: 'Country should be of type "string"'
            }
          ]
        };
      }

      valueNotSet = false;
      payLoad.country = country;
    } // end if

    if (valueNotSet) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: '',
            message: 'No value(s) sent for updation.'
          }
        ]
      };
    }

    try {
      let result = await Author.updateOne()
        .set(payLoad)
        .where({
          id: id
        }); // .fetch() not required for updateOne() as it always returns the updated record or undefined if not found

      // Success
      result = result || { errors: [ { code: 'I_INFO', message: `No Author exists with the requested Id: ${id}`} ] };
      sails.log.debug(`AuthorHelper._updateAuthor: Author successfully updated:`, result);
      return result;
    } catch (err) {
      sails.log.debug('AuthorHelper._updateAuthor: Exception encountered:', err);
      return {
        errors: [
          {
            code: 'E_API_ERROR',
            message: `Author update request failed.`,
            moduleError: {
              code: err.code || 'E_ERROR',
              attrNames: err.attrNames || [],
              message: err.message
            }
          }
        ]
      };
    } // end try {}
  }, // end _updateAuthor()

  /*
   * @Function:     _deleteAuthor(id)
   * @Description:  Delete one record of Author
   * @Params:       id - Author Id
   * @Return:       Author | ErrorResponse
   */
  _deleteAuthor: async (id) => {
    if (!id) {
      return {
        errors: [
          {
            code: 'E_BAD_INPUT',
            attrName: 'id',
            message: 'Id is required for deletion.'
          }
        ]
      };
    }

    try {
      let result = await Author.destroyOne({id});

      // Success
      result = result || { errors: [ { code: 'I_INFO', message: `No Author exists with the requested Id: ${id}`} ] };
      sails.log.debug(`AuthorHelper._deleteAuthor: Author successfully deleted:`, result);
      return result;
    } catch (err) {
      sails.log.debug('AuthorHelper._deleteAuthor: Exception encountered:', err);
      return {
        errors: [
          {
            code: 'E_API_ERROR',
            message: `Author delete request failed.`,
            moduleError: {
              code: err.code || 'E_ERROR',
              attrNames: err.attrNames || [],
              message: err.message
            }
          }
        ]
      };
    } // end try {}
  }, // end _deleteAuthor()

  /*
   * @Function:     _getAuthor(input)
   * @Description:  Fetch one or more record(s) of Author
   * @Params:       input - dictionary with either Author Id or a filter criteria
   * @Return:       Author | [Author] | ErrorResponse
   */
  _getAuthor: async (input) => {
    const id = input.id;
    let where = input.where || {};

    if (typeof where === 'string') {
      try {
        where = JSON.parse(where);
      } catch(err) {
        return {
          errors: [
            {
              code: 'E_BAD_INPUT',
              attrName: 'where',
              message: 'Where clause should be a valid JSON object.'
            }
          ]
        };
      } // end try
    }

    if (id) {
      where.id = id;
    }

    try {
      // Now fetch the record(s) from database
      let result = await Author.find().where(where);

      if (id) {
        if (result.length > 0) {
          result = result[0];
        } else {
          result = { errors: [ { code: 'I_INFO', message: `No Author exists with the requested Id: ${id}`} ] };
        }
      }

      // Success
      sails.log.debug(`AuthorHelper._getAuthor: Author(s) successfully retrieved:`, result);
      return result;
    } catch(err) {
      sails.log.debug('AuthorHelper._getAuthor: Exception encountered:', err);
      return {
        errors: [
          {
            code: 'E_API_ERROR',
            message: 'Author fetch request failed.',
            moduleError: {
              code: err.code || 'E_ERROR',
              attrNames: err.attrNames || [],
              message: err.message
            }
          }
        ]
      };
    } // end try {}
  }, // end _getAuthor()
};
```
## Register GraphQL middleware in Sails
Finally, having completed the groundwork, we are ready to register `express-graphql` middleware in Sails application. The best candidate to do this is `config/bootstrap.js` file. It gets executed when Sails load all hooks.
```
/**
 * Bootstrap
 * (sails.config.bootstrap)
 *
 * An asynchronous bootstrap function that runs just before your Sails app gets lifted.
 * > Need more flexibility?  You can also do this by creating a hook.
 *
 * For more information on bootstrapping your app, check out:
 * https://sailsjs.com/config/bootstrap
 */
const graphqlHTTP = require('express-graphql');
const { schema } = require('../api/graphql/schemas/schema');

module.exports.bootstrap = async function(done) {
  sails.hooks.http.app.use('/graphql',
    graphqlHTTP((req, res) => ({
      schema: schema,
      context: { req },
      graphiql: false
    }))
  );

  // Don't forget to trigger `done()` when this bootstrap function's logic is finished.
  // (otherwise your server will never lift, since it's waiting on the bootstrap)
  return done();

};
```
Please pay attention to the `context` parameter. We are passing an object with one value in it i.e., HTTP Request object. You can add more key-value pairs as per your project/application needs. GraphQL will pass this object literally to all resolvers and directives.
## How to invoke the GraphQL endpoint
We will discuss how to invoke the endpoint using `Postman` and `Axios`.
### Postman Application
#### Example 1
We will demostrate how to add an author using Postman.
##### Query
```
mutation ($input: AuthorInput!) {
    addAuthor(data: $input) {
        ... on Author {
            name
            country
        }
        ... on ErrorResponse {
            errors {
                code
                message
            }
        }
    }
}
```
##### Variables
```
{
	"input": 

	{
    		"name": "J. K. Rowling"
	}
}
```
##### Output
```
{
  "data": {
    "addAuthor": {
      "name": "J. K. Rowling",
      "country": "UNKNOWN"
    }
  }
}
```
##### Screenshot of Postman
![screenshot1](https://user-images.githubusercontent.com/29386513/76560783-d7b5a100-6499-11ea-86cf-d163365a89c4.png) 
#### Example 2
We will demonstrate an error condition thrown while adding a book. We will send a wrong genre. Notice that our BookHelper returns the error instead of graphql.
##### Query
```
mutation ($input: BookInput!) {
    addBook(data: $input) {
        ... on Book {
            title
            yearPublished
            genre
        }
        ... on ErrorResponse {
            errors {
                code
                message
            }
        }
    }
}
```
##### Variables
```
{
	"input": 

	{
    		"title": "Harry Potter and the Philosopher's Stone",
    		"yearPublished": "1998",
    		"genre": "FICTION",
    		"authorId": 1
	}
}
```
##### Output
```
{
  "data": {
    "addBook": {
      "errors": [
        {
          "code": "E_BAD_INPUT",
          "message": "Genre should be one of \"'ADVENTURE', 'COMICS', 'FANTASY', 'UNKNOWN'\""
        }
      ]
    }
  }
}
```
##### Screenshot of Postman
![screenshot2](https://user-images.githubusercontent.com/29386513/76560779-d6847400-6499-11ea-9300-e5938fbd22cb.png) 
#### Example 3
We will demonstrate a query on book and author.
##### Query
```
query ($id: Int!) {
    getBook(id: $id) {
        ... on Book {
            title
            yearPublished
            genre
            author {
                name
            }
        }
        ... on ErrorResponse {
            errors {
                code
                message
            }
        }
    }
}
```
##### Variables
```
{
	"id": 1 
}
```
##### Output
```
{
  "data": {
    "getBook": {
      "title": "Harry Potter and the Philosopher's Stone",
      "yearPublished": "1998",
      "genre": "FANTASY",
      "author": {
        "name": "J. K. Rowling"
      }
    }
  }
}
```
##### Screenshot of Postman
![screenshot3](https://user-images.githubusercontent.com/29386513/76560778-d4bab080-6499-11ea-9688-45318ad6ca8e.png) 
### Front-end applications
We will provide examples of code using `axios` to execute graphql queries. If you use some other package to place your HTTP requests then adapt the example code to your package of choice.
#### Example 1
Example of a query
##### Query
```
this.$axios({
  url: '/graphql',
  method: 'POST',
  data: {
    query: `query ($filter: String) {
      getBooks(filter: $filter) {
        ... on Book {
            title
            yearPublished
            genre
            author {
                name
                country
            }
        }
        ... on ErrorResponse {
            errors {
                code
                message
            }
        }
      }
    }
    `,
    variables: {
      filter: JSON.stringify({
        genre: this.genre  // genre = 'FANTASY'
      })
    }
  }
}).then((response) => {
  let resp = response.data.data.getBooks
  if (resp.length > 0) {
    if (resp[0].errors) {
      // api threw an array of error objects
      const err = {
        response: {
          data: resp[0].errors[0]
        }
      }
      console.log(err)
    } else {
      // success
      console.log(resp)
    }
  }
}).catch((err) => {
  console.log(err)
})
```
##### Output
```
{
  "data": {
    "getBooks": [
      {
        "title": "Harry Potter and the Philosopher's Stone",
        "yearPublished": "1998",
        "genre": "FANTASY",
        "author": {
          "name": "J. K. Rowling",
          "country": "UNKNOWN"
        }
      }
    ]
  }
}
```
#### Example 2
Example of a mutation
##### Query
```
this.$axios({
  url: '/graphql',
  method: 'POST',
  data: {
    query: `mutation ($id: Int!, $data: AuthorInput!) {
      updateAuthor(id: $id, data: $data) {
        ... on Author {
            name
            country
        }
        ... on ErrorResponse {
            errors {
              code
              message
            }
        }
      }
    }
    `,
    variables: {
      filter: JSON.stringify({
        id: this.id, // id = 1
        data: { 
          country: this.country // country = 'United Kingdom'
        }
      })
    }
  }
}).then((response) => {
  let resp = response.data.data.updateAuthor
  if (resp.length > 0) {
    if (resp[0].errors) {
      // api threw an array of error objects
      const err = {
        response: {
          data: resp[0].errors[0]
        }
      }
      console.log(err)
    } else {
      // success
      console.log(resp)
    }
  }
}).catch((err) => {
  console.log(err)
})
```
##### Output
```
{
  "data": {
    "updateAuthor": {
      "name": "J. K. Rowling",
      "country": "United Kingdom"
    }
  }
}
```
## Piece of Advice
The graphql runtime error messages are very vague when you develop the schema of your project. It will not pinpoint exactly where you have an error in your schema definition. It will simply spit out the expected token and what it found while parsing your schema. So to make your life a bit easier, I would suggest to add the following `console.log()` in the file `node_modules/graphql/language/parser.js` line# 95. This will give you better insight into your schema for taking remedial action.
```
...
  _proto.parseName = function parseName() {
    var token = this.expectToken(_tokenKind.TokenKind.NAME);

    console.log(`Line: ${this.loc(token).startToken.line}, Column: ${this.loc(token).startToken.column}, Value: ${this.loc(token).startToken.value}`);

    return {
      kind: _kinds.Kind.NAME,
      value: token.value,
      loc: this.loc(token)
    };
  } // Implements the parsing rules in the Document section.
...
```
**CAUTION:** *Please add this `console.log()` in development environment only. It will clutter your sails server log.*
## Closing words
I would recommend and encourage readers to learn GraphQL. We have touched upon a subset of GraphQL schema features. GraphQL provides three types of operations - queries, mutations and subscriptions. We have seen first two in action, I leave it to the reader to try subscriptions in case your application needs `pub-sub` type of interaction.

The complete project is available on [Github](https://github.com/aflatoon2874/sails-graphql).

Please write your comments, good or bad, whatever you feel like.
Thanks for your time. Hope you enjoyed!!