Giter Club home page Giter Club logo

graphql-cost-analysis's Introduction

GraphQL Query Cost Analysis for graphql-js

Travis npm version

A GraphQL request cost analyzer.

This can be used to protect your GraphQL servers against DoS attacks, compute the data consumption per user and limit it.

This package parses the request content and computes its cost with your GraphQL server cost configuration.

Backend operations have different complexities and dynamic arguments (like a limit of items to retrieve). With this package you can define a cost setting on each GraphQL field/type with directives or a Type Map Object.

Works with graphql-js reference implementation

Type Map Object: An object containing types supported by your GraphQL server.

Installation

Install the package with npm

$ npm install --save graphql-cost-analysis

Simple Setup

Init the cost analyzer

import costAnalysis from 'graphql-cost-analysis'

const costAnalyzer = costAnalysis({
  maximumCost: 1000,
})

Then add the validation rule to the GraphQL server (apollo-server, express-graphql...)

Setup with express-graphql

app.use(
  '/graphql',
  graphqlHTTP((req, res, graphQLParams) => ({
    schema: MyGraphQLSchema,
    graphiql: true,
    validationRules: [
      costAnalysis({
        variables: graphQLParams.variables,
        maximumCost: 1000,
      }),
    ],
  }))
)

Setup with apollo-server-express

app.use(
  '/graphql',
  graphqlExpress(req => {
    return {
      schema,
      rootValue: null,
      validationRules: [
        costAnalysis({
          variables: req.body.variables,
          maximumCost: 1000,
        }),
      ],
    }
  })
)

costAnalysis Configuration

The costAnalysis function accepts the following options:

Argument Description Type Default Required
maximumCost The maximum allowed cost. Queries above this threshold will be rejected. Int undefined yes
variables The query variables. This is needed because the variables are not available in the visitor of the graphql-js library. Object undefined no
defaultCost Fields without cost setting will have this default value. Int 0 no
costMap A Type Map Object where you can define the cost setting of each field without adding cost directives to your schema.
If this object is defined, cost directives will be ignored.
Each field in the Cost Map Object can have 3 args: multipliers, useMultipliers, complexity.
Object undefined no
complexityRange An optional object defining a range the complexity must respect. It throws an error if it's not the case. Object: {min: number, max: number} undefined no
onComplete(cost) Callback function to retrieve the determined query cost. It will be invoked whether the query is rejected or not.
This can be used for logging or to implement rate limiting (for example, to store the cost by session and define a max cost the user can have in a specific time).
Function undefined no
createError(maximumCost, cost) Function to create a custom error. Function undefined no

A Custom Cost for Each Field/Type

Now that your global configuration is set, you can define the cost calculation for each of your schema Field/Type.

2 Ways of defining Field/Type cost settings:

  • with a @cost directive
  • by passing a Type Map Object to the costAnalysis function (see costMap argument)

Cost Settings Arguments

Argument Description Type Default Required
multipliers An array containing names of parameters present in the GraphQL field. Use parameters values to compute the field's cost dynamically.
N.B: if the parameter is an array, its multiplier value will be the length of the array (cf EG2).

E.g: GraphQL field is getUser(filters: {limit: 5}). The multipliers array could be ["filters.limit"].

E.g 2: posts(first: 5, last: 5, list: ["my", "list"]). The multipliers array could be ["first", "last", "list"]. Then the cost would be complexity * (first + last + list.length).
Array undefined no
useMultipliers Defines if the field's cost depends on the parent multipliers and field's multipliers. Boolean true no
complexity The level of complexity to resolve the current field.
If the field needs to call an expensive service to resolve itself, then the complexity should be at a high level but if the field is easy to resolve and not an expensive operation, the complexity should be at a low level.
Object {min: number, max: number} {min: 1} no

Defining the Cost Settings via Directives

To define the cost settings of fields for which you want a custom cost calculation, just add a cost directive to the concerned fields directly to your GraphQL schema.

Example:

# you can define a cost directive on a type
type TypeCost @cost(complexity: 3) {
  string: String
  int: Int
}

type Query {
  # will have the default cost value
  defaultCost: Int

  # will have a cost of 2 because this field does not depend on its parent fields
  customCost: Int @cost(useMultipliers: false, complexity: 2)

  # complexity should be between 1 and 10
  badComplexityArgument: Int @cost(complexity: 12)

  # the cost will depend on the `limit` parameter passed to the field
  # then the multiplier will be added to the `parent multipliers` array
  customCostWithResolver(limit: Int): Int
    @cost(multipliers: ["limit"], complexity: 4)

  # for recursive cost
  first(limit: Int): First
    @cost(multipliers: ["limit"], useMultipliers: true, complexity: 2)

  # you can override the cost setting defined directly on a type
  overrideTypeCost: TypeCost @cost(complexity: 2)
  getCostByType: TypeCost

  # You can specify several field parameters in the `multipliers` array
  # then the values of the corresponding parameters will be added together.
  # here, the cost will be `parent multipliers` * (`first` + `last`) * `complexity
  severalMultipliers(first: Int, last: Int): Int
    @cost(multipliers: ["first", "last"])
}

type First {
  # will have the default cost value
  myString: String

  # the cost will depend on the `limit` value passed to the field and the value of `complexity`
  # and the parent multipliers args: here the `limit` value of the `Query.first` field
  second(limit: Int): String @cost(multipliers: ["limit"], complexity: 2)

  # the cost will be the value of the complexity arg even if you pass a `multipliers` array
  # because `useMultipliers` is false
  costWithoutMultipliers(limit: Int): Int
    @cost(useMultipliers: false, multipliers: ["limit"])
}

Defining the Cost Settings in a Type Map Object

Use a Type Map Object when you don't want to contaminate your GraphQL schema definition, so every cost setting field will be reported in a specific object.

If you dispatch your GraphQL schema in several modules, you can divide your Cost Map Object into several objects to put them in their specific modules and then merge them into one Cost Map object that you can pass to the costAnalysis function.

Create a type Map Object representing your GraphQL schema and pass cost settings to each field for which you want a custom cost.

Example:

const myCostMap = {
  Query: {
    first: {
      multipliers: ['limit'],
      useMultipliers: true,
      complexity: 3,
    },
  },
}

app.use(
  '/graphql',
  graphqlHTTP({
    schema: MyGraphQLSchema,
    validationRules: [
      costAnalysis({
        maximumCost: 1000,
        costMap: myCostMap,
      }),
    ],
  })
)

Using complex types (UnionType or InterfaceType)

When using a UnionType or Interfaces, the highest of the nested fragments cost is used.

Common interface fields outside of fragments are treated like regular fields.

Given types:

interface CommonType {
  common: Int @cost(useMultipliers: false, complexity: 3)
}

type First implements CommonType {
  common: Int
  firstField: String @cost(useMultipliers: false, complexity: 5)
}

type Second implements CommonType {
  common: Int
  secondField: String @cost(useMultipliers: false, complexity: 8)
}

union FirstOrSecond = First | Second

type Query {
  firstOrSecond: FirstOrSecond
  commonType: CommonType
}

and a query like

query {
  firstOrSecond {
    ... on First {
      firstField
    }
    ...secondFields
  }
  commonType {
    common
    ...secondFields
  }
}

fragment secondFields on Second {
  secondField
}

the complexity of the query will be 8,

  • firstOrSecond has a complexity of 8
    • Second.secondField field has a defined complexity of 8 which exceeds the complexity of 5 for First.firstField
  • commonType has a complexity of 11
    • secondFields has a complexity of 8
    • common has a complexity of 3 and is added to the previous value of 8

So the whole query has a complexity of 19

Note

If you just need a simple query complexity analysis without the GraphQL Schema Language and without multipliers and/or depth of parent multipliers, I suggest you install graphql-query-complexity

License

graphql-cost-analysis is MIT-licensed.

graphql-cost-analysis's People

Contributors

mxstbr avatar pa-bru avatar zcei avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

graphql-cost-analysis's Issues

how to calculate the cost of multiple recursion of the same level

Please advise how to calculate the cost of the below query

  query {
    first(limit: ${limit}) {
      second(limit: ${limit}) {
        int
      }
      anotherSecond(limit: ${limit}) {
        int
      }
    }
  }

Given here the cost map

type Query {
  first (limit: Int): First @cost(
    multipliers: ["limit"], useMultipliers: true, complexity: ${firstComplexity}
  )
}

type First implements BasicInterface {
  string: String
  int: Int
  second (limit: Int): Second @cost(
    multipliers: ["limit"], useMultipliers: true, complexity: ${secondComplexity}
  )
  anotherSecond (limit: Int): Second @cost(
    multipliers: ["limit"], useMultipliers: true, complexity: ${secondComplexity}
  )
}

type Second implements BasicInterface {
  int: Int
}

I believe this below is the correct test case, correct me if I was wrong

const firstCost = limit * firstComplexity
const secondCost = limit * limit * secondComplexity
const anotherSecondCost = limit * limit * secondComplexity

const result = firstCost + secondCost + anotherSecondCost
expect(visitor.cost).toEqual(result)

Reference:
iamake@2df09da

Syntax for Setting up with Apollo Server 2?

Anyone who has implemented this with Apollo Server 2? There is no example showing how to set up this.

Tried this but keeps throwing undefined errors

const server = new ApolloServer({
    typeDefs: importSchema('./src/schema.graphql'),
    resolvers: resolvers as any,
    introspection: true,
    playground: process.env.NODE_ENV === 'development',
    debug: process.env.NODE_ENV === 'development',
    context: async ({req}) => {
        return {
            req,
            db
        }
    },
    formatError: error => {
        console.log('Error', error);
        switch (error.extensions.code) {
            case 'INTERNAL_SERVER_ERROR':
                delete error.extensions.exception;
                error.message = 'Internal server error. Try again later';
                return error;
            default:
                return error;
        }
    },
    validationRules: [
        costAnalysis({
            variables: graphQLParams.variables,
            maximumCost: 1000,
        }),
    ],
});

Any help would be greatly appreciated.

Questions about complexity and batch operations

I've been looking at the documentation, and this seems like a decent tool for our use case.

However, we have certain complexity characteristics, that I'm not sure if we can calculate correctly. Please advise if these scenarios are supported:

1. Groups of fields with combined complexity

We're using apollo server as a gateway, that resolves all queries by making API calls to a back-end. For some types, there are some fields that are always included with the resource (basically zero cost), and there are other fields that require an additional round-trip to the back-end. If one or more of these fields are included, we need to make that extra round-trip, so basically any additional field will have zero cost:

type User {
  # "zero-cost" fields
  id: String!
  name: String!
  email: String!

  # one or more of these fields will add a cost, but if we include one,
  # it doesn't matter if we include all of them
  address: String!
  phone: String!
  birthDate: String!
}

We want a significant complexity value to be added, if any of the fields address, phone or birthDate are included in the query, but we don't want it to add up, if several of these fields are included.

2. Batch requests for nested resources

Another scenario is like this:

type Query {
  allUsers: [User!]!
}

type User {
  id: String!
  messages: [Message!]!
}

type Message {
  # always included
  id: String!
  text: String!

  # extra round-trip needed
  seenBy: [User!]!
}

In this scenario, we have a back-end API method that returns all users. We have another API method that returns all messages to all users. If we have a query like this:

{ allUsers {
  id
  messages { id text }
}

We only need to make two API calls. So the cost of including the messages doesn't depend on the number of users. This seems like a use-case for setting useMultipliers to false.

However, if we include the seenBy field, it will trigger an extra API call for every message, so in that case, we do want multipliers for the number of users and messages to apply. Is this scenario possible with this library?

Multiplier complexity analysis based on array length

I have a schema like this:

type PayrollType {
  mou: String!
  eou: String!
  ecode: String!
  payElements(filter: [String]): [PayElementType]
}

Here, I want to calculate the cost of payElements attribute where I pass the filter as an array of payelements for which I want to return the data like this: payElements(filter: ["ELEM1","ELEM2"])

So, here I want to base the multiplier based on length but as far as I know the filter can contain only integers as per this library. Any way to calculate multiplier cost by array length?

Here, the cost will be complexity*2 where 2 is the length of the array.

And before I forget to tell this, amazing work with the library. It works like a charm.

DoS by using invalid queries

When creating invalid queries, e.g. by using fields that do not exist we can bypass the complexity costs.

Let's say the request contains 30k very small and invalid queries, then we will have at least 30k errors in the response. I would have expected that this goes into the maximumCost calculation when using defaultCost: 1.

Feature request

According to Docs, Cost multiply by array's length:

N.B: if the parameter is an array, its multiplier value will be the length of the array (cf EG2).

Can we have this for Objects too? For example:

input PagerOptions {
	before: String
	after: String
	page: Int = 1
	limit: Int = 10
}

type Query {
  sections ( pager: PagerOptions = {} ):
    SectionsConnection @cost(multipliers: ["pager.limit", "pager"], complexity: {min: 3})
}

If we do so:

query GetSections {
  sections (pager: {
    limit: 50, # Count: 1
    page: 10, # Count: 2
  }) {
    nodes {
      id
      status
    }
  }
}

Then final counts should be:
50 + 2 (50: limit, 1+1: pager fields) = 52

What you say? Is it worthy?

Remove arbitrary complexity limit

Why is complexity limited to 10? I could have an outlier query that has a complexity of 100. Why doesn't graphql-cost-analysis leave the complexity numbers to me?

Variables dependency incompatible with Apollo Server 2

Currently, the validation rule requires providing the variables object from the request because

This is needed because the variables are not available in the visitor of the graphql-js library.

Unfortunately, version 2 of Apollo Server no longer allows dynamic configuration of all of its server options per-request, but rather only the context. validationRules must be provided when the server is initialized and not within the context of the middleware. Therefore, all that each validation rule has access to is the ValidationContext and there's no built-in way to inject the request variables.

I am reasonably certain that this is an intentional change.

It seems possible to remove graphql-cost-analysis's dependence on the full variables object, given that the ValidationContext provides information about variable usages and also about field arguments.

Is this feasible?

typescript

are there any plans to switch from flow to typescript? i like your tool but its one of a few dependencies that neither has @types/ -package nor has direct typescript-declaration in shipped npmjs.com-artifact. that prevents me from doing fully strict typescript-type-checking, cause such dependencies get implictAny type 😬

Bug with multiplied siblings

I think the cost calculated for nested siblings is treating the second sibling like it is a child. Here is a repro:

type Query {
  things (limit: Int = 50): [Thing!]! @cost(multipliers: ["limit"], complexity: 1)
}

type Thing {
  name: String!
  subThingsA (limit: Int = 50): [SubThing!]! @cost(multipliers: ["limit"], complexity: 1)
  subThingsB (limit: Int = 50): [SubThing!]! @cost(multipliers: ["limit"], complexity: 1)
}

type SubThing {
  name: String!
}

Sample query:

query {
  things {
    subThingsA
    subThingsB
  }
}

Expected cost: 5050 (1 * 50) + (1 * 50 * 50) + (1 * 50 * 50)
Actual cost: 127550 (1 * 50) + (1 * 50 * 50) + (1 * 50 * 50 * 50)

Multiplier for default optional value

Hello,

is there any way how to tell that if first variable is not provided, it would default to 50, so multiplier should be 50 even without providing it ?

Default useMultipliers to true if a multiplier is specified

Specifying both a multiplier and having to set useMultipliers to true seems redundant—surely if I set a multiplier I want to use it:

type Whatever {
  # 🤔
  resolver(first: Int) Something @cost(multiplier: "first", useMultipliers: true)
}

Multiple multipliers?

In a typical connection one can paginate both fore-and backwards with the first or last arguments, respectively. How can I tell graphql-cost-analysis that both first and last are multipliers?

type Community {
  # Get a list of posts in a community
  # How can I tell graphql-cost-analysis that both first and last are multipliers?
  postConnection(first: Int, after: String, last: Int, before: String): PostConnection 
}

Use cost in resolver

Is it possible to pass the cost to the resolver, or to create a new type (for example, to take query{remaining {cost, remainingCost}})?

Multiple query/mutation and onComplete behavior

I don't know if it is a bug or not

For this test case

test('should consider default cost with operationName', done => {
  const ast = parse(
    `
    query operationA {
      defaultCost
    }

    query operationB {
      defaultCost
    }
  `)

  const context = new ValidationContext(schema, ast, typeInfo)
  const visitor = new CostAnalysis(context, {
    maximumCost: 100,
    defaultCost: 12,
    onComplete: cost => {
      console.log('cost', cost)
      done()
    }
  })

  visit(ast, visitWithTypeInfo(typeInfo, visitor))
})

I have 2 queries, onComplete will be called 2 times
First time, cost is 12, second time 24. 🤔

After that, i use operationName, like "operationName":"operationB"
Should operationA be analysed and count in the total cost ?

Mutations support?

Does this support mutations and subscriptions? I do care for mutations.

Feature request - simple interface to ask for cost, independent of graphql server

What is the best way to ask from costAnalysis API what is the cost, without any graphql server instance?

We're resorting currently to this, but would be nice to have easy API to get the cost..

const { validate, parse, ValidationContext, TypeInfo } = require('graphql');
const costAnalysis = require('graphql-cost-analysis').default;

function getCurrentCost(query, schema, variables) {
	const queryAST = parse(query);
	const validationContext = new ValidationContext(schema, queryAST, new TypeInfo(schema));
	const costAnalyser = costAnalysis({ variables, ...costLimits })(validationContext);

	validate(schema, queryAST, [() => costAnalyser]);

	return costAnalyser.cost;
}

Error: Unknown directive "cost".

Using this with the same way spectrum does:

class ProtectedApolloServer extends ApolloServer {
  async createGraphQLServerOptions(req, res) {
    const options = await super.createGraphQLServerOptions(req, res)

    return {
      ...options,
      validationRules: [
        ...options.validationRules,
        costAnalysis({
          maximumCost: 10,
          defaultCost: 1,
          variables: req.body.variables,
          createError: (max, actual) => {
            const err = new ForbiddenError(
              `GraphQL query exceeds maximum complexity, please remove some nesting or fields and try again. (max: ${max}, actual: ${actual})`
            )
            return err
          },
        }),
      ],
    }
  }
}

But when I add @cost(multipliers: ["first", "last"], complexity: 5) to my types I get the error Error: Unknown directive "cost"..

Also using typescript and ziet/ncc to compile my backend.

Any thoughts?

Invalid options provided to ApolloServer: costAnalysis is not a function

I'm trying to use graphql-cost-analysis. but it just said costAnalysis is not a function.
I can not show you entire code. but it's like this.

const { graphqlExpress } = require('graphql-server-express');
const costAnalysis = require('graphql-cost-analysis')
...
      graphqlExpress(req => {
        return {
          schema,
          rootValue: null,
          validationRules: [
            costAnalysis({
              variables: req.body.variables,
              maximumCost: 10
            }),
          ],
        }
      })
...

and the result of console.log(costAnalysis) is like this.

{ default: [Function: createCostAnalysis] }

What am I missing?

thanks

HowTo use costMap with non top-level objects?

Hello everyone,

given a sample schema like:

type Query {
  articles(limit: int): [Article]
}

type Article{
  id: ID!
  no: String!
  country: ListEntry
}

type ListEntry {
  id: ID!
  descript: String
}

Howto define costs for Article and ListEntry types using the costMap? (can't use the SDL way as I'm going code first, not SDL first).

Until now I only understood how to define costs for top-level queries in general without taking the accessed fields into regard (that's what the example in the readme does).

But what I wish to define is:

articles: {
   multipliers: ['limit'],
   useMultipliers: true
},
Article: {
   complexity: 1
},
ListEntry: {
   complexity: 1
},

so that the query

query {
   articles(10) {
      no,
      country {
         descript
      }
   }
}

will fail when maximumCost is set to 15, but will be fine when set to 20.

Any way to solve that with graphql-cost-analysis using costMap?

Regards,
Michael

Warning: Apollo Server >= 2.4 caches validation result

A feature added to Apollo Server 2.4 (apollographql/apollo-server#2111) introduces a document store that caches successfully parsed and validated documents for future requests (LRU).

This can lead to a case where a query with good variables passes the dynamic validation cost check (see #12) and subsequent requests with the same query but different, larger variables would not trigger the validation rule due to the usage of the cache.

I don't have a sample reproduction repository, but here is an example with maximumCost: 10:

Schema:

type Query {
  "List businesses."
  businesses(page: Int! = 1, pageSize: Int! = 10): BusinessConnection
    @cost(complexity: 1, multipliers: ["pageSize"])
}

Query:

query ($pageSize: Int! = 10) {
  businesses(pageSize: $pageSize) {
    edges {
      node {
        id
        name
      }
    }
  }
}

First request query variables (validation is run) - passes validation:

{
  "pageSize": 10
}

Second request query variables (validation is skipped) - should fail validation but passes

{
  "pageSize": 100
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.