UNPKG

graphql-rate-limit-directive

Version:

Fixed window rate-limiting directive for GraphQL. Use to limit repeated requests to queries and mutations.

176 lines (175 loc) 8.81 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.millisecondsToSeconds = millisecondsToSeconds; exports.getSchemaCoordinate = getSchemaCoordinate; exports.defaultKeyGenerator = defaultKeyGenerator; exports.defaultPointsCalculator = defaultPointsCalculator; exports.defaultOnLimit = defaultOnLimit; exports.defaultSetState = defaultSetState; exports.rateLimitDirective = rateLimitDirective; const graphql_1 = require("graphql"); const utils_1 = require("@graphql-tools/utils"); const rate_limiter_flexible_1 = require("rate-limiter-flexible"); /** * Convert milliseconds to seconds. * @param duration Milliseconds. */ function millisecondsToSeconds(duration) { return Math.ceil(duration / 1000); // round up to over estimate for client } /** * Human readable string that uniquely identifies a schema element within a GraphQL Schema. * @param info Holds field-specific information relevant to the current operation as well as the schema details. */ function getSchemaCoordinate(info) { return `${info.parentType.name}.${info.fieldName}`; } /** * Get a value to uniquely identify a field in a schema. * @param directiveArgs The arguments defined in the schema for the directive. * @param source The previous result returned from the resolver on the parent field. * @param args The arguments provided to the field in the GraphQL operation. * @param context Contains per-request state shared by all resolvers in a particular operation. * @param info Holds field-specific information relevant to the current operation as well as the schema details. */ function defaultKeyGenerator(directiveArgs, source, args, context, info) { return getSchemaCoordinate(info); } /** * Calculate the number of points to consume. * @param directiveArgs The arguments defined in the schema for the directive. * @param source The previous result returned from the resolver on the parent field. * @param args The arguments provided to the field in the GraphQL operation. * @param context Contains per-request state shared by all resolvers in a particular operation. * @param info Holds field-specific information relevant to the current operation as well as the schema details. */ function defaultPointsCalculator( /* eslint-disable @typescript-eslint/no-unused-vars */ directiveArgs, source, args, context, info) { return 1; } /** * Raise a rate limit error when there are too many requests. * @param response The current rate limit information for this field. * @param directiveArgs The arguments defined in the schema for the directive. * @param source The previous result returned from the resolver on the parent field. * @param args The arguments provided to the field in the GraphQL operation. * @param context Contains per-request state shared by all resolvers in a particular operation. * @param info Holds field-specific information relevant to the current operation as well as the schema details. */ function defaultOnLimit(response, /* eslint-disable @typescript-eslint/no-unused-vars */ directiveArgs, source, args, context, info) { throw new graphql_1.GraphQLError(`Too many requests, please try again in ${millisecondsToSeconds(response.msBeforeNext)} seconds.`); } /** * Write directive state into context. * @param name Key of rate limit state in context, likely the directive's name. */ function defaultSetState(name = 'rateLimit') { return (response, directiveArgs, source, args, context, info) => { let state = context[name]; if (!state) { state = {}; context[name] = state; } state[getSchemaCoordinate(info)] = response; }; } /** * Create an implementation of a rate limit directive. */ function rateLimitDirective({ name = 'rateLimit', defaultLimit = '60', defaultDuration = '60', keyGenerator = defaultKeyGenerator, pointsCalculator = defaultPointsCalculator, onLimit = defaultOnLimit, setState, limiterClass = rate_limiter_flexible_1.RateLimiterMemory, limiterOptions = {}, } = {}) { const limiters = new Map(); const getLimiter = ({ limit, duration }) => { const limiterKey = `${limit}/${duration}s`; let limiter = limiters.get(limiterKey); if (limiter === undefined) { limiter = new limiterClass(Object.assign(Object.assign({}, limiterOptions), { keyPrefix: limiterOptions.keyPrefix === undefined ? name // change the default behaviour which is to use 'rlflx' : limiterOptions.keyPrefix, points: limit, duration: duration })); limiters.set(limiterKey, limiter); } return limiter; }; const rateLimit = (directive, field) => { const directiveArgs = directive; const limiter = getLimiter(directiveArgs); const { extensions: fieldExtensions, resolve = graphql_1.defaultFieldResolver } = field; const directiveExtensions = fieldExtensions ? fieldExtensions[name] : undefined; const { keyGenerator: fieldKeyGenerator = keyGenerator, pointsCalculator: fieldPointsCalculator = pointsCalculator, onLimit: fieldOnLimit = onLimit, setState: fieldSetState = setState, } = directiveExtensions !== null && directiveExtensions !== void 0 ? directiveExtensions : {}; field.resolve = (source, args, context, info) => __awaiter(this, void 0, void 0, function* () { const pointsToConsume = yield fieldPointsCalculator(directiveArgs, source, args, context, info); if (pointsToConsume !== 0) { const key = yield fieldKeyGenerator(directiveArgs, source, args, context, info); try { const response = yield limiter.consume(key, pointsToConsume); if (fieldSetState) fieldSetState(response, directiveArgs, source, args, context, info); } catch (e) { if (e instanceof Error) { throw e; } const response = e; if (fieldSetState) fieldSetState(response, directiveArgs, source, args, context, info); return fieldOnLimit(response, directiveArgs, source, args, context, info); } } return resolve(source, args, context, info); }); }; return { rateLimitDirectiveTypeDefs: `""" Controls the rate of traffic. """ directive @${name}( """ Number of occurrences allowed over duration. """ limit: Int! = ${defaultLimit} """ Number of seconds before limit is reset. """ duration: Int! = ${defaultDuration} ) on OBJECT | FIELD_DEFINITION`, rateLimitDirectiveTransformer: (schema) => (0, utils_1.mapSchema)(schema, { [utils_1.MapperKind.OBJECT_TYPE]: (type, schema) => { var _a; const rateLimitDirective = (_a = (0, utils_1.getDirective)(schema, type, name)) === null || _a === void 0 ? void 0 : _a[0]; if (rateLimitDirective) { // Wrap fields of object for limiting that don't have their own directive applied const fields = type.getFields(); Object.values(fields).forEach((field) => { const overrideDirective = (0, utils_1.getDirective)(schema, field, name); if (overrideDirective === undefined) { rateLimit(rateLimitDirective, field); } }); } return type; }, [utils_1.MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName, schema) => { var _a; const rateLimitDirective = (_a = (0, utils_1.getDirective)(schema, fieldConfig, name)) === null || _a === void 0 ? void 0 : _a[0]; if (rateLimitDirective) { rateLimit(rateLimitDirective, fieldConfig); } return fieldConfig; }, }), }; } //# sourceMappingURL=index.js.map