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.

122 lines (121 loc) 5.96 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getFieldIdentity = exports.getGraphQLRateLimiter = void 0; const tslib_1 = require("tslib"); const lodash_get_1 = tslib_1.__importDefault(require("lodash.get")); const ms_1 = tslib_1.__importDefault(require("ms")); const promise_helpers_1 = require("@whatwg-node/promise-helpers"); const batch_request_cache_js_1 = require("./batch-request-cache.js"); const in_memory_store_js_1 = require("./in-memory-store.js"); // Default field options const DEFAULT_WINDOW = 60 * 1000; const DEFAULT_MAX = 5; const DEFAULT_FIELD_IDENTITY_ARGS = []; /** * Returns a string key for the given field + args. With no identityArgs are provided, just the fieldName * will be used for the key. If an array of resolveArgs are provided, the values of those will be built * into the key. * * Example: * (fieldName = 'books', identityArgs: ['id', 'title'], resolveArgs: { id: 1, title: 'Foo', subTitle: 'Bar' }) * => books:1:Foo * * @param fieldName * @param identityArgs * @param resolveArgs */ const getFieldIdentity = (fieldName, identityArgs, resolveArgs) => { const argsKey = identityArgs.map(arg => (0, lodash_get_1.default)(resolveArgs, arg)); return [fieldName, ...argsKey].join(':'); }; exports.getFieldIdentity = getFieldIdentity; /** * This is the core rate limiting logic function, APIs (directive, sheild etc.) * can wrap this or it can be used directly in resolvers. * @param userConfig - global (usually app-wide) rate limiting config */ const getGraphQLRateLimiter = ( // Main config (e.g. the config passed to the createRateLimitDirective func) userConfig) => { // Default directive config const defaultConfig = { enableBatchRequestCache: false, formatError: ({ fieldName }) => { return `You are trying to access '${fieldName}' too often`; }, // Required identifyContext: () => { throw new Error('You must implement a createRateLimitDirective.config.identifyContext'); }, store: new in_memory_store_js_1.InMemoryStore(), }; const { enableBatchRequestCache, identifyContext, formatError, store } = { ...defaultConfig, ...userConfig, }; const batchRequestCache = enableBatchRequestCache ? (0, batch_request_cache_js_1.getWeakMapCache)() : (0, batch_request_cache_js_1.getNoOpCache)(); /** * Field level rate limiter function that returns the error message or undefined * @param args - pass the resolver args as an object * @param config - field level config */ const rateLimiter = ( // Resolver args { args, context, info, }, // Field level config (e.g. the directive parameters) { arrayLengthField, identityArgs, max, window, message, readOnly, uncountRejected, }) => { // Identify the user or client on the context const contextIdentity = identifyContext(context); // User defined window in ms that this field can be accessed for before the call is expired const windowMs = (window ? (0, ms_1.default)(window) : DEFAULT_WINDOW); // String key for this field const fieldIdentity = getFieldIdentity(info.fieldName, identityArgs || DEFAULT_FIELD_IDENTITY_ARGS, args); // User configured maximum calls to this field const maxCalls = max || DEFAULT_MAX; // Call count could be determined by the lenght of the array value, but most commonly 1 const callCount = (arrayLengthField && (0, lodash_get_1.default)(args, [arrayLengthField, 'length'])) || 1; // Allinclusive 'identity' for this resolver call const identity = { contextIdentity, fieldIdentity }; // Timestamp of this call to be save for future ref const timestamp = Date.now(); // Create an array of callCount length, filled with the current timestamp const newTimestamps = [...new Array(callCount || 1)].map(() => timestamp); // We set these new timestamps in a temporary memory cache so we can enforce // ratelimits across queries batched in a single request. const batchedTimestamps = batchRequestCache.set({ context, fieldIdentity, newTimestamps, }); // Fetch timestamps from previous requests out of the store // and get all the timestamps that haven't expired return (0, promise_helpers_1.handleMaybePromise)(() => store.getForIdentity(identity), accessTimestamps => { const filteredAccessTimestamps = accessTimestamps.filter(t => { return t + windowMs > Date.now(); }); // Flag indicating requests limit reached const limitReached = filteredAccessTimestamps.length + batchedTimestamps.length > maxCalls; // Confogure access timestamps to save according to uncountRejected setting const timestampsToStore = [ ...filteredAccessTimestamps, ...(!uncountRejected || !limitReached ? batchedTimestamps : []), ]; // Save these access timestamps for future requests. return (0, promise_helpers_1.handleMaybePromise)(() => readOnly ? undefined : store.setForIdentity(identity, timestampsToStore, windowMs), () => { // Field level custom message or a global formatting function const errorMessage = message || formatError({ contextIdentity, fieldIdentity, fieldName: info.fieldName, max: maxCalls, window: windowMs, }); // Returns an error message or undefined if no error return limitReached ? errorMessage : undefined; }); }); }; return rateLimiter; }; exports.getGraphQLRateLimiter = getGraphQLRateLimiter;