@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
JavaScript
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()]);
}