2.7 KiB
guards/depth-and-complexity — query-shape rejection
(no submodules)
Purpose
depth-and-complexity walks a parsed DocumentNode and computes two
coarse static metrics — the maximum selection-set nesting depth and the
total field-count cost. The engine consults both before handing the
document to Apollo Server; queries that exceed maxQueryDepth or
maxQueryComplexity are rejected with the corresponding
GRAPHQL_APOLLO.DEPTH_EXCEEDED / COMPLEXITY_EXCEEDED codes.
The walk follows fragment spreads through their definitions and includes inline-fragment subselections, so a clever query that hides depth or cost behind a deeply nested fragment cannot bypass the guards. The heuristic matches the Yoga sibling so the two engines reject the same query set under identical limits.
Features
- Depth counter increments per nested
selectionSet, never on fragment spreads or inline fragments themselves (matches conventional depth-limiter implementations). - Complexity counter adds 1 per selected field, plus the recursive cost of its subselection, plus the expansion of fragment spreads. Missing fragment spreads contribute zero.
assertDepthAndComplexityevaluates depth first; the more localised failure wins so operators see the rule they triggered.- Pure-functional — no instance state, no
this. Safe to call from any request lifecycle phase.
Usage: Basic
import { parse } from "graphql";
import { assertDepthAndComplexity } from "@suaveplan/api-graphql-apollo/guards";
const doc = parse("{ a { b { c } } }");
assertDepthAndComplexity(doc, { maxDepth: 10, maxComplexity: 1000 });
Usage: Advanced
Run the metrics in isolation to feed an OTel histogram before the engine even routes the document:
import { parse } from "graphql";
import {
maxQueryDepth,
queryComplexity,
} from "@suaveplan/api-graphql-apollo/guards";
const doc = parse(req.body.query);
metrics.histogram("graphql.depth").record(maxQueryDepth(doc));
metrics.histogram("graphql.complexity").record(queryComplexity(doc));
API Reference
maxQueryDepth(doc: DocumentNode): numberqueryComplexity(doc: DocumentNode): numberassertDepthAndComplexity(doc, { maxDepth, maxComplexity }): void— throwsGraphqlApolloErroron first violation.
Implementation Notes
Fragment definitions are collected up-front into a Map so spread
resolution is O(1). Inline fragments share the parent depth — they do
not add a level. The complexity heuristic intentionally undercounts
list multiplicity; production deployments wire a richer estimator via a
host-supplied costExtension (out of scope for the foundation engine).