@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.
130 lines (129 loc) • 7.11 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useRateLimiter = exports.defaultInterpolateMessageFn = exports.DIRECTIVE_SDL = exports.Store = exports.RedisStore = exports.RateLimitError = exports.InMemoryStore = void 0;
const tslib_1 = require("tslib");
const graphql_1 = require("graphql");
const picomatch_1 = tslib_1.__importDefault(require("picomatch"));
const utils_1 = require("@graphql-tools/utils");
const promise_helpers_1 = require("@whatwg-node/promise-helpers");
const get_graphql_rate_limiter_js_1 = require("./get-graphql-rate-limiter.js");
const in_memory_store_js_1 = require("./in-memory-store.js");
Object.defineProperty(exports, "InMemoryStore", { enumerable: true, get: function () { return in_memory_store_js_1.InMemoryStore; } });
const rate_limit_error_js_1 = require("./rate-limit-error.js");
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return rate_limit_error_js_1.RateLimitError; } });
const redis_store_js_1 = require("./redis-store.js");
Object.defineProperty(exports, "RedisStore", { enumerable: true, get: function () { return redis_store_js_1.RedisStore; } });
const store_js_1 = require("./store.js");
Object.defineProperty(exports, "Store", { enumerable: true, get: function () { return store_js_1.Store; } });
exports.DIRECTIVE_SDL = `
directive @rateLimit(
max: Int
window: String
message: String
identityArgs: [String]
arrayLengthField: String
readOnly: Boolean
uncountRejected: Boolean
) on FIELD_DEFINITION
`;
const defaultInterpolateMessageFn = (message, identifier) => interpolateByArgs(message, { id: identifier });
exports.defaultInterpolateMessageFn = defaultInterpolateMessageFn;
const useRateLimiter = (options) => {
const rateLimiterFn = (0, get_graphql_rate_limiter_js_1.getGraphQLRateLimiter)({
...options,
identifyContext: options.identifyFn,
});
const interpolateMessage = options.interpolateMessage || exports.defaultInterpolateMessageFn;
const configByField = options.configByField?.map(config => ({
...config,
isMatch: {
type: (0, picomatch_1.default)(config.type),
field: (0, picomatch_1.default)(config.field),
},
}));
const directiveName = options.rateLimitDirectiveName ?? 'rateLimit';
return {
onSchemaChange({ schema: _schema }) {
if (!_schema) {
return;
}
const schema = _schema;
for (const type of Object.values(schema.getTypeMap())) {
if (!(0, graphql_1.isObjectType)(type)) {
continue;
}
for (const field of Object.values(type.getFields())) {
const fieldConfigs = configByField?.filter(({ isMatch }) => isMatch.type(type.name) && isMatch.field(field.name));
if (fieldConfigs && fieldConfigs.length > 1) {
throw new Error(`Config error: field '${type.name}.${field.name}' has multiple matching configuration`);
}
const fieldConfig = fieldConfigs?.[0];
const rateLimitDirective = (0, utils_1.getDirectiveExtensions)(field, schema)[directiveName]?.[0];
if (rateLimitDirective && fieldConfig) {
throw new Error(`Config error: field '${type.name}.${field.name}' has both a configuration and a directive`);
}
const baseConfig = rateLimitDirective ?? fieldConfig;
if (baseConfig) {
const rateLimitConfig = { ...baseConfig };
rateLimitConfig.max = rateLimitConfig.max && Number(rateLimitConfig.max);
if (fieldConfig?.identifyFn) {
rateLimitConfig.identityArgs = [
'identifier',
...(rateLimitConfig.identityArgs ?? []),
];
}
const originalResolver = field.resolve ?? graphql_1.defaultFieldResolver;
field.resolve = (parent, args, context, info) => {
const resolverRateLimitConfig = { ...rateLimitConfig };
const executionArgs = { parent, args, context, info };
const identifier = (fieldConfig?.identifyFn ?? options.identifyFn)(context);
if (fieldConfig?.identifyFn) {
executionArgs.args = { identifier, ...args };
}
if (resolverRateLimitConfig.message && identifier) {
const messageArgs = { root: parent, args, context, info };
resolverRateLimitConfig.message = interpolateMessage(resolverRateLimitConfig.message, identifier, messageArgs);
}
return (0, promise_helpers_1.handleMaybePromise)(() => rateLimiterFn(executionArgs, resolverRateLimitConfig), rateLimitError => {
if (!rateLimitError) {
return originalResolver(parent, args, context, info);
}
if (options.onRateLimitError) {
options.onRateLimitError({
error: rateLimitError,
identifier,
context,
info,
});
}
if (options.transformError) {
throw options.transformError(rateLimitError);
}
const errorOptions = {
extensions: { http: { statusCode: 429 } },
path: (0, graphql_1.responsePathAsArray)(info.path),
nodes: info.fieldNodes,
};
if (resolverRateLimitConfig.window) {
errorOptions.extensions.http.headers = {
'Retry-After': resolverRateLimitConfig.window,
};
}
throw (0, utils_1.createGraphQLError)(rateLimitError, errorOptions);
});
};
}
}
}
},
onContextBuilding({ extendContext }) {
extendContext({
rateLimiterFn,
});
},
};
};
exports.useRateLimiter = useRateLimiter;
function interpolateByArgs(message, args) {
return message.replace(/\{{([^)]*)\}}/g, (_, key) => args[key.trim()]);
}