@samveys/graphql
From packages/graphql/README.md in the framework repository.
A GraphQL endpoint for @samveys/http with limits that run before any resolver. One runtime dependency: graphql (graphql-js).
| Protection | Option | Default | Error extensions.code |
|---|---|---|---|
| Depth | maxDepth | 10 | DEPTH_LIMIT |
| Field count (aliases and fragments expanded) | maxCount | 200 | COUNT_LIMIT |
| Cost | maxCost, costs, defaultCost | 1000, 1 per field | COST_LIMIT |
| Rate limit per object type and client | rateLimits | none | RATE_LIMITED (HTTP 429) |
| Batching of sensitive operations | sensitiveOperations, batching, maxBatchSize | no sensitive ops, batches of up to 10 | BATCHING_DISABLED |
| Introspection and the GraphiQL page | none: on only when app.env === 'local' | off elsewhere | INTROSPECTION_DISABLED |
A refused query gets HTTP 400 with a standard GraphQL errors array, and its resolvers never run. Syntax errors are GRAPHQL_PARSE_FAILED, other validation errors GRAPHQL_VALIDATION_FAILED. Resolver errors that are not GraphQL errors are masked as INTERNAL_SERVER_ERROR outside local.
Set up
import { http } from '@samveys/http'
import { graphql } from '@samveys/graphql'
import { GraphQLInt, GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'
const Report = new GraphQLObjectType({ name: 'Report', fields: { title: { type: GraphQLString } } })
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
reports: {
type: new GraphQLList(Report),
args: { first: { type: GraphQLInt } },
resolve: () => [{ title: 'Q1' }],
},
},
}),
})
export const plugins = [
http(),
graphql({
schema,
path: '/graphql',
maxDepth: 8,
maxCost: 500,
costs: { 'Query.reports': 10 },
rateLimits: { Report: { limit: 100, windowMs: 60_000 } },
sensitiveOperations: ['Mutation.login'],
}),
]Cost
A field costs costs['Type.field'], else defaultCost. A list field multiplies the cost of its children by its first or limit argument (a literal or a variable; change the names with listArguments), else by defaultListSize (10). So reports(first: 100) { title } costs 10 + 100 × 1 = 110.
Rate limits
rateLimits counts one hit per request for each listed object type the query touches, per client (ctx.clientIp), in a fixed window. The counter is rateLimitStore, else the 'cache' binding of @samveys/cache (shared across processes), else process memory.
import { createMemoryCounter } from '@samveys/graphql'
export const counter = createMemoryCounter()Batching
A request body that is an array is a batch. A batch that contains an operation named in sensitiveOperations (by operation name, or by root field such as Mutation.login) is refused whole; the same operation sent alone is fine. batching: false refuses every batch.
Introspection and GraphiQL
Both are available only when the app runs in the local environment. In test, staging and production, __schema and __type are refused, and GET {path} is not routed.
// Over the limit: refused before the resolver runs.
// POST /graphql { "query": "{ reports(first: 1000) { title } }" }
// 400 { "errors": [{ "message": "Query cost 1010 exceeds the limit of 500", "extensions": { "code": "COST_LIMIT" } }] }