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.

130 lines (129 loc) • 7.11 kB
"use strict"; 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()]); }