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
JavaScript
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
;