76 lines
2.7 KiB
Markdown
76 lines
2.7 KiB
Markdown
# `guards/depth-and-complexity` — query-shape rejection
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
|
|
## 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.
|
|
- `assertDepthAndComplexity` evaluates 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
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
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): number`
|
|
- `queryComplexity(doc: DocumentNode): number`
|
|
- `assertDepthAndComplexity(doc, { maxDepth, maxComplexity }): void` —
|
|
throws `GraphqlApolloError` on 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).
|