smartgraphql

GraphQL Query Cost & Depth Complexity Analysis

Usage no npm install needed!

<script type="module">
  import smartgraphql from 'https://cdn.skypack.dev/smartgraphql';
</script>

README

SmartGraphQL

Travis (.org) branch

GraphQL Query Cost & Depth Complexity Analysis

The SmartGraphQL library enables users to limit the depth and complexity of queries to their GraphQL server, preventing resource exhaustion.

Compatible with Express and Apollo-Server validation rules.

Installation

Install this package via npm

npm install -s smartgraphql 

Usage

Set a limit for Cost Complexity by creating a object with the following properties:

const ruleCost = {

  // All queries with a cost above this limit will be rejected and throw an error
  costLimit: 500,

  // Optional onSuccess method which will confirm the query has successfully passed the cost limit check with a customizable 	  message
  onSuccess: cost => `Complete, query cost is ${cost}`,

// Optional onError method to alert user that the query has been rejected with a customizable message
  onError: (cost, costLim) => `Error: Cost is ${cost} but cost limit is set to ${costLim}`,

};

Set a limit for query depth by creating a object with the following properties.

const ruleDepth = {

// All queries with a depth above 'depthLimit' will be rejected and throw a GraphQLError before resolving.
  depthLimit: 100,

// Optional onSuccess method which will confirm the query has successfully passed the cost limit check with a customizable      message.
  onSuccess: depth => `Complete, query depth is ${depth}`,

// Optional onError method to alert user that the query has been rejected with a customizable GraphQLError
  onError: (depth, maximumDepth) => `Error: Current depth is ${depth} but max depth is 
       ${maximumDepth}`,

};

Depth Calculation

Depth is calculated by how nested the query is. For example the following queries are incrementally increasing:

// ** depth = 1
query{
  Author(id:1) {
    Name
  }
}

// ** depth = 2
query{
  Author(id:1) {
    Name
    Books{
       Name
    }
  }
}

// ** depth = 3
query{
  Author(id:1) {
    Name
    Books{
      Name
      Genre{
     Books 
  }
}

Inline Fragments and Fragments will not cause the query depth to increase. For example in both the following cases the query depth will remain 1:

//Inline Fragment
query{
  Author(id:1) {
    Name
    ... on Books{
    Pages
    }
  }
}

//Fragment
query{
  Author(id:1) {
    Name
    ...books
  }
}

fragment books on Author{
      Name
      Year
      Genre
}

Cyclical Queries can cause servers to crash by being nested to a large amount, and this is where setting a depth limit becomes useful. A depth limit can reject cyclical queries such as the following:

query{
  Artists{
    Name
    Songs{
      Name
      Artist{
        Name
        Songs{
          Name
           etc...
        }
      }
    }
  }
}

Cost Calculation

Cost is calculated based on the number of times a resolve function makes a connection to the database. For example:

query{
  artists(first: 100){
    name
    songs(first: 50){
      name
      genre{
        name
        songs(first: 10){
          name
        }
      }
    }
  }
}

This query would result in a cost of 5101, which can be broken down into the following steps:

  • The initial request is 1 because although we’re return the first 100 artists, there will only be one connection to the database.
  • For songs we will need to connect once for each artist to get a list of their first 50 songs, so that will be 100 connections.
  • For the 'songs' field inside genre, you will need to make one connection for the 5000 songs, so that will be 5000

Total Cost is 5101

Usage with express-graphql

Integrating the rules inside the validation rules will look like this, the limit will be manadatory, but the onSuccess and onError function are optional

const ruleCost = {
  costLimit: 10000,
  onSuccess: cost => `Complete, query cost is ${cost}`,
  onError: (cost, costLim) => `Error: Cost is ${cost} but cost limit is set to ${costLim}`,
};

const ruleDepth = {
  depthLimit: 100,
  onSuccess: depth => `Complete, query depth is ${depth}`,
  onError: (depth, maximumDepth) => `Error: Current depth is ${depth} but max depth is ${maximumDepth}`,
};

app.use(
  '/graphql',
  graphqlHTTP(() => ({
    schema,
    graphiql: true,
    validationRules: [depthComplexity(ruleDepth), costLimit(ruleCost)],
  })),
);

Credits

Developed by Julia, Manjeet, Mark & Seth