UNPKG

@envelop/rate-limiter

Version:

This plugins uses [`graphql-rate-limit`](https://github.com/teamplanes/graphql-rate-limit#readme) in order to limit the rate of calling queries and mutations.

118 lines (117 loc) 5.54 kB
import { responsePathAsArray } from 'graphql'; import { minimatch } from 'minimatch'; import { useOnResolve } from '@envelop/on-resolve'; import { createGraphQLError, getDirectiveExtensions } from '@graphql-tools/utils'; import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import { getGraphQLRateLimiter } from './get-graphql-rate-limiter.js'; import { InMemoryStore } from './in-memory-store.js'; import { RateLimitError } from './rate-limit-error.js'; import { RedisStore } from './redis-store.js'; import { Store } from './store.js'; export { InMemoryStore, RateLimitError, RedisStore, Store, }; export const DIRECTIVE_SDL = /* GraphQL */ ` directive @rateLimit( max: Int window: String message: String identityArgs: [String] arrayLengthField: String readOnly: Boolean uncountRejected: Boolean ) on FIELD_DEFINITION `; export const defaultInterpolateMessageFn = (message, identifier) => interpolateByArgs(message, { id: identifier }); export const useRateLimiter = (options) => { const rateLimiterFn = getGraphQLRateLimiter({ ...options, identifyContext: options.identifyFn, }); const interpolateMessage = options.interpolateMessage || defaultInterpolateMessageFn; return { onPluginInit({ addPlugin }) { addPlugin(useOnResolve(({ root, args, context, info }) => { const field = info.parentType.getFields()[info.fieldName]; if (field) { const directives = getDirectiveExtensions(field); const rateLimitDefs = directives?.rateLimit; let rateLimitDef = rateLimitDefs?.[0]; let identifyFn = options.identifyFn; let fieldIdentity = false; if (!rateLimitDef) { const foundConfig = options.configByField?.find(({ type, field }) => minimatch(info.parentType.name, type) && minimatch(info.fieldName, field)); if (foundConfig) { rateLimitDef = foundConfig; if (foundConfig.identifyFn) { identifyFn = foundConfig.identifyFn; fieldIdentity = true; } } } if (rateLimitDef) { const message = rateLimitDef.message; const max = rateLimitDef.max && Number(rateLimitDef.max); const window = rateLimitDef.window; const identifier = identifyFn(context); return handleMaybePromise(() => rateLimiterFn({ parent: root, args: fieldIdentity ? { ...args, identifier } : args, context, info, }, { max, window, identityArgs: fieldIdentity ? ['identifier', ...(rateLimitDef.identityArgs || [])] : rateLimitDef.identityArgs, arrayLengthField: rateLimitDef.arrayLengthField, uncountRejected: rateLimitDef.uncountRejected, readOnly: rateLimitDef.readOnly, message: message && identifier ? interpolateMessage(message, identifier, { root, args, context, info, }) : undefined, }), errorMessage => { if (errorMessage) { if (options.onRateLimitError) { options.onRateLimitError({ error: errorMessage, identifier, context, info, }); } if (options.transformError) { throw options.transformError(errorMessage); } throw createGraphQLError(errorMessage, { extensions: { http: { statusCode: 429, headers: { 'Retry-After': window, }, }, }, path: responsePathAsArray(info.path), nodes: info.fieldNodes, }); } }); } } })); }, onContextBuilding({ extendContext }) { extendContext({ rateLimiterFn, }); }, }; }; function interpolateByArgs(message, args) { return message.replace(/\{{([^)]*)\}}/g, (_, key) => args[key.trim()]); }