Welcome to graphql-utilities’s documentation!¶
Introduction¶
graphql-utilities tries to secure your GraphQL API from malicious queries and provides utilities to make using graphql-core easier.
- It comes with a configurable ExtendedExecutionContext class that is capable of performing:
- query cost analysis: define the cost of your queries using the @cost() directive provided, graphql-utilities provides helper functions and extended execution context to protect you from overly complex or costly queries.
- depth limiting: limit the maximum depth of queries, it’s especially useful with object types with recursive relationship
- It also ships decorators for:
- resource-level/one-shot middleware: middleware in graphql-core is run at field-level, it is handly when you need your middleware to run only once, especially auth-related middleware.
Getting Started¶
You can install graphql-utilities using pip:
pip install graphql-utilities
Alternatively, if you are using pipenv:
pipenv install graphql-utilities
Note that graphql-utilities requires graphql-core>=3.0, which also means you need Python 3.6 or above,
With the package installed, you are ready to go. Depending on your need, you may refer to each page:
Using Query Cost Analysis¶
The most exciting part of this library is query cost analysis. It calculates the complexity of queries received and halt execution if the complexity exceeds the permitted value.
Cost analysis works with simple queries, queries with fragments, and mutations.
See also
For more comprehensive usage, check out Advanced Usage of Query Cost Analysis.
Step 1: Build Schema with Cost Directive¶
First, you need a custom GraphQL directive: @cost() provided by the library.
You can:
import them from `graphql-utilities`, then create a schema along with the directive.
from graphql import build_schema from graphql_utilities import cost_directive_source_doc build_schema(source=cost_directive_source_doc + """ type Post @cost(complexity: 5) { postId: ID! title: String } """)
or use a helper function `build_schema_with_cost()`:
from graphql_utilities import build_schema_with_cost build_schema_with_cost(source=""" type Post @cost(complexity: 5) { postId: ID! title: String } """)
build_schema_with_cost() is a wrapper of graphql.build_schema(). It stitches the cost directive with your schema before invoking build_schema(). Hence, the function signature of graphql_utilities.build_schema_with_cost() is exactly the same as graphql.build_schema():
1 2 3 4 5 6 | def build_schema_with_cost(
source: Union[str, Source],
assume_valid=False,
assume_valid_sdl=False,
no_location=False,
experimental_fragment_variables=False):
|
Step 2: Define Complexity Of Queries/Objects/Fields (Basic Usage)¶
To define the complexity of a query, simple add a @cost() directive to any Object or Field Definition.
Complexity of Object¶
To define the complexity of an Object Type, add @cost(complexity: <Int>) directive after the type identifier (post, in the case below).
type Post @cost(complexity: 5) {
postId: ID!
title: String
}
type Query {
post(postId: ID!): Post
}
The complexity of each Post object is 5. The complexity of querying the schema post(postId: ID!) is also 5.
Complexity of Field¶
You can always define field-specific complexity by adding a @cost() directive to a field:
type Post {
postId: ID!
title: String
lastSnapshot: String @cost(complexity: 8)
}
type Query {
post(postId: ID!): Post
}
The complexity of the query below will be 8.
{
posts(postId: "XXXXXX") {
postId
lastSnapshot
}
}
Defining Complexity For Both Object and Fields¶
In situation when complexity needs to be defined in Object as well as specific Field of the Object, such as:
type Post @cost(complexity: 5) {
postId: ID!
title: String
lastSnapshot: String @cost(complexity: 8)
}
The total complexity of querying an Object with the field lastSnapshot will be the sum of complexities defined in both @cost() directives (5 + 8 = 13).
{
posts(postId: "XXXXXX") {
postId
lastSnapshot
}
}
The cost of the query above is 5 + 8 = 13.
Step 3: Enable Cost Analysis¶
To enable cost analysis, you must use the ExtendedExecutionContext provided by the library.
All you need to do are:
Pass graphql_utilities.ExtendedExecutionContext as the value of execution_context_class into any of graphql.graphql_sync(), graphql.graphql(), or graphql.execute()
Pass the following context_value:
{"cost_analysis": { "max_complexity": 5 # Maximum complexity allowed }}
Example:¶
from graphql_utilities import ExtendedExecutionContext, build_schema_with_cost
schema = build_schema_with_cost(source="""
type Post @cost(complexity: 5) {
postId: ID!
title: String @cost(complexity: 20)
}
type Query {
post(postId: ID!): Post
}
""")
query = """
{
post(postId: "XXXXX") {
postId
title
}
}
"""
results = graphql_sync(schema=schema,
source=query,
execution_context_class=ExtendedExecutionContext,
context_value={
"cost_analysis": {
"max_complexity": 8
}
})
results # ExecutionResult(data=None, errors=[GraphQLError("25 exceeded max complexity of 8")])
Advanced Usage¶
For more advanced usage of query cost analysis, refer to Advanced Usage of Query Cost Analysis.
Advanced Usage of Query Cost Analysis¶
Basic complexity calculation may be insufficient. graphql-utilities cost analysis also supports:
- complexity multiplier: useful when number of items requested needs to be factored into total complexity
- overriding Object cost: useful when the complexity of requesting multiple objects diminishes as the number of items requested increases
Cost analysis works with simple queries, queries with fragments, and mutations.
Complexity Multiplier - Why and How?¶
Complexity multiplier allows you to multiply the complexity of Object/Type by the quantity requested.
Why?¶
For a query that resolves to a list of objects. You probably want to factor the number of items requested into the query complexity.
For instance, you have a posts query that returns a list of Post objects.
type Post @cost(complexity: 5) {
postId: ID!
title: String
}
type Query {
posts(first: Int): [Post] // The complexity should be `first * 5`
}
The complexity should be the value of first multiplied by 5.
How?¶
All you need to do is to add the @cost directive to your Query and specify the multipliers as a list of string
type Post @cost(complexity: 5) {
postId: ID!
title: String
}
type Query {
posts(first: Int): [Post] @cost(multiplier: ["first"])
}
With the schema above, the complexity of the query below will be 4 * 5 = 20.
{
posts(first: 4) {
postId
title
}
}
What If There Is Field-specific Cost?¶
If your Object type imposed extra complexities for specific fields as such:
type Post @cost(complexity: 5) {
postId: ID!
title: String @cost(complexity: 4)
}
type Query {
posts(first: Int): [Post] @cost(multiplier: ["first"])
}
and with such query:
{
posts(first: 4) {
postId
title
}
}
The complexity will be 4 * (5 + 4) = 36.
Overriding Cost of Object Type¶
You can also override the complexity of Object type defined. All you need is to add the complexity argument into the @cost directive to the Query type that returns the Object you’re overriding.
type Post @cost(complexity: 5) {
postId: ID!
title: String @cost(complexity: 4)
}
type Query {
posts(first: Int!): [Post] @cost(multiplier: ["first"], complexity: 2)
post(postId: ID!): Post // The complexity remains unchanged
}
The cost of Post (5) will be overridden with 2 when querying posts(first: Int!): [Post].
For instance, the total complexity will be 4 * (2 + 4) = 24
{
posts(first: 4) {
postId
title
}
}
Limiting Query Depth¶
The ExtendedExecutionContext execution context class provided in graphql-utilities is capable of limiting the maximum depth of queries, it’s especially useful with object types with recursive relationship.
All you need to do are:
Pass graphql_utilities.ExtendedExecutionContext as the value of execution_context_class into any of graphql.graphql_sync(), graphql.graphql(), or graphql.execute()
Pass the following context_value:
{"depth_analysis": { "max_depth": 5 # Maximum depth allowed }}
Example:¶
from graphql_utilities import ExtendedExecutionContext
query = """
{
posts(first: 5) {
postId
author {
authorId
posts(first: 5) {
postId
author {
authorId
posts(first: 5) {
postId
author {
authorId
posts(first: 5) {
postId
// Depth: 7
}
}
}
}
}
}
}
}
"""
results = graphql_sync(schema=schema,
source=query,
execution_context_class=ExtendedExecutionContext,
context_value={
"depth_analysis": {
"max_depth": 5
}
})
results # ExecutionResult(data=None, errors=[GraphQLError("Reached max depth of 5")])
Operation-level/One-shot Middleware¶
The problem of using middleware in graphql-core in Python is it run at field-level. If you use a middleware to perform authentication, you probably don’t want the authentication logic run when the engine tries to resolve every single field.s
from graphql_utilities.decorators import run_only_once
class AuthMiddleware:
@run_only_once
def resolve(self, next_, root, info, *args, **kwargs):
# middleware logic
return next_(root, info, *args, **kwargs)
Introduction¶
graphql-utilities tries to secure your GraphQL API from malicious queries and provides utilities to make using graphql-core easier.
- It comes with a configurable ExtendedExecutionContext class that is capable of performing:
- query cost analysis: define the cost of your queries using the @cost() directive provided, graphql-utilities provides helper functions and extended execution context to protect you from overly complex or costly queries.
- depth limiting: limit the maximum depth of queries, it’s especially useful with object types with recursive relationship
- It also ships decorators for:
- resource-level/one-shot middleware: middleware in graphql-core is run at field-level, it is handly when you need your middleware to run only once, especially auth-related middleware.
Getting Started¶
You can install graphql-utilities using pip:
pip install graphql-utilities
Alternatively, if you are using pipenv:
pipenv install graphql-utilities
Note that graphql-utilities requires graphql-core>=3.0, which also means you need Python 3.6 or above,
With the package installed, you are ready to go. Depending on your need, you may refer to each page:
\ Sort by:\ best rated\ newest\ oldest\
\\
Add a comment\ (markup):
\``code``
, \ code blocks:::
and an indented block after blank line