graphql-depth-guard v0.1.2
GraphQL Depth Guard
A GraphQL directive to enforce query depth limits with optional caching support (using Redis or in-memory cache).
Features
- Depth Limiting: Prevent overly deep GraphQL queries.
- Caching: Cache depth calculations for performance optimization.
- Customizable Storage: Supports Redis, in-memory cache.
- Flexible Limits: Apply limits globally or on a per-query basis.
Why GraphQL Depth Guard?
Complexity-based limiters often struggle to precisely identify thresholds for various use cases. This library was created to provide an intuitive mechanism for limiting query depths. By restricting response depth, it aims to:
- Prevent excessive and repetitive database queries.
- Simplify configuration compared to complexity-based approaches.
- Support both global and query-specific limits for better control.
Depth Definition
Query depth is determined by the structure of the response fields, including nested fields and fragments. Here's how depth is defined:
Example Queries with Depth Calculation
Depth: 0
query {
hello
}
Depth: 1
query {
userDetails {
name
}
}
Depth: 2 (with nested fields)
query {
userDetails {
name
posts {
title
}
}
}
Depth: 2 (with fragments)
fragment postInfo on Post {
title
comments {
content
author
}
}
query {
userDetails {
posts {
...postInfo
}
}
}
Fragment Behavior
- Inline Fragments and Named Fragments are fully traversed during depth calculation.
- Fragments do not reset or reduce the depth; they are evaluated as part of the query structure.
Example with Nested Fragments
fragment commentInfo on Comment {
content
author
}
fragment postInfo on Post {
title
comments {
...commentInfo
}
}
query {
viewer {
users {
posts {
...postInfo
}
}
}
}
Depth Calculation:
viewer
(depth 0)users
(depth 1)posts
(depth 2)postInfo
(depth 3 fortitle
andcomments
)commentInfo
(depth 4 forcontent
andauthor
)
Total Depth: 4
Installation
NPM
npm install graphql-depth-guard graphql @graphql-tools/utils
Yarn
yarn add graphql-depth-guard graphql @graphql-tools/utils
Usage
Basic Setup
- Import the directive and apply it to your schema:
import { makeExecutableSchema } from '@graphql-tools/schema';
import depthLimitDirective from 'depth-limit-directive';
const typeDefs = `
type Query {
hello: String @depthLimit(limit: 3)
nestedField: NestedType @depthLimit(limit: 2)
}
type NestedType {
name: String
child: NestedType
}
`;
const resolvers = {
Query: {
hello: () => 'Hello, world!',
nestedField: () => ({ name: 'Level 1', child: { name: 'Level 2' } }),
},
};
const depthDirective = depthLimitDirective({
globalLimit: 5, // Optional global limit
});
const schema = depthDirective.transformer(
makeExecutableSchema({
typeDefs: [depthDirective.typeDefs, typeDefs],
resolvers,
}),
);
Caching Support
1. Using In-Memory Cache
The library uses an in-memory cache (MemoryCache
) by default, which stores cached depths for 60 seconds.
import depthLimitDirective, { MemoryCache } from 'graphql-depth-guard';
const depthDirective = depthLimitDirective({
globalLimit: 5,
store: new MemoryCache(60 * 1000),
});
2. Using redis Cache
To use redis
for caching:
import { createClient } from 'redis';
import depthLimitDirective, { RedisCache } from 'graphql-depth-guard';
const redisClient: RedisClientType = createClient({
url: 'redis://localhost:6379',
});
await redisClient.connect();
const redisCache = new RedisCache(redisClient, 60 * 1000); // TTL: 60 seconds
const depthDirective = depthLimitDirective({
store: redisCache, // Pass Redis cache instance
globalLimit: 5,
});
3. Using ioredis Cache
To use ioredis
for caching:
import Redis from 'ioredis';
import depthLimitDirective, { RedisCache } from 'graphql-depth-guard';
const ioredisClient = new Redis('redis://localhost:6379');
const redisCache = new RedisCache(ioredisClient, 60 * 1000); // TTL: 60 seconds
const depthDirective = depthLimitDirective({
store: redisCache, // Pass Redis cache instance
globalLimit: 5,
});
4. No Cache
If no store is provided in the options, caching is disabled, and the directive calculates the depth for every query without caching it.
const depthDirective = depthLimitDirective({
globalLimit: 5, // Global limit without caching
});
Custom Error Handling
You can provide a custom errorHandler
to control how errors are reported:
const depthDirective = depthLimitDirective({
globalLimit: 5,
errorHandler: ({ depth, limit, message, isGlobalLimit }) => {
return new Error(
`Custom Error: Depth of ${depth} exceeds limit of ${limit}${
isGlobalLimit ? ' (global limit)' : ''
}`,
);
},
});
API
depthLimitDirective(options?: DepthLimitDirectiveOptions)
Option | Type | Description |
---|---|---|
globalLimit | number (optional) | The global depth limit for queries. |
errorHandler | function (optional) | Custom function to handle errors when the depth limit is exceeded. |
store | ICache (optional) | Cache store implementation (MemoryCache , RedisCache , or custom store). |
Example Schema with Depth Directive
directive @depthLimit(limit: Int!, message: String) on FIELD_DEFINITION
type Query {
hello: String @depthLimit(limit: 3)
nestedField: NestedType @depthLimit(limit: 2)
}
type NestedType {
name: String
child: NestedType
}