first commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# `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).
|
||||
Reference in New Issue
Block a user