UNPKG

@envelop/rate-limiter

Version:
199 lines (198 loc) • 10.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Store = exports.RedisStore = exports.InMemoryStore = exports.useRateLimiter = exports.defaultInterpolateMessageFn = exports.DIRECTIVE_SDL = void 0; const tslib_1 = require("tslib"); const types_1 = require("node:util/types"); const graphql_1 = require("graphql"); const lodash_get_1 = tslib_1.__importDefault(require("lodash.get")); 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"); 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 getTypeInfo = (0, utils_1.memoize1)(function getTypeInfo(schema) { return new graphql_1.TypeInfo(schema); }); const useRateLimiter = (options) => { const rateLimiterFn = (0, get_graphql_rate_limiter_js_1.getGraphQLRateLimiter)({ ...options, identifyContext: context => options.identifyFn(context, {}), }); 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'; const getRateLimitConfig = (0, utils_1.memoize4)(function getFieldConfigs(configByField, schema, type, field) { 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 rateLimitConfig = rateLimitDirective || fieldConfig; if (!rateLimitConfig) { return undefined; } rateLimitConfig.max = Number(rateLimitConfig.max); if (rateLimitConfig?.identifyFn || rateLimitConfig?.identifier) { rateLimitConfig.identityArgs = ['identifier', ...(rateLimitConfig.identityArgs ?? [])]; } return rateLimitConfig; }); return { onExecute({ args, setResultAndStopExecution }) { const { document, schema, contextValue: context, variableValues, rootValue: root } = args; const typeInfo = getTypeInfo(schema); const rateLimitCalls = new Set(); const errors = []; args.document = (0, graphql_1.visit)(document, (0, graphql_1.visitWithTypeInfo)(typeInfo, { Field(node, _key, _parent, path, _ancestors) { const type = typeInfo.getParentType(); const field = typeInfo.getFieldDef(); if (type != null && field != null) { const rateLimitConfig = getRateLimitConfig(configByField, schema, type, field); if (!rateLimitConfig) { return; } const resolverRateLimitConfig = { ...rateLimitConfig }; let args = null; function getArgValues() { if (!args && field) { args = (0, utils_1.getArgumentValues)(field, node, variableValues); } return args ?? {}; } const identifier = rateLimitConfig?.identifier ? resolveIdentifierTemplate(rateLimitConfig.identifier, getArgValues, context) : (rateLimitConfig?.identifyFn ?? options.identifyFn)(context, getArgValues()); const executionArgs = { identifier, root, get args() { return { ...getArgValues(), identifier, }; }, context, type, field, }; if (resolverRateLimitConfig.message && identifier) { resolverRateLimitConfig.message = interpolateMessage(resolverRateLimitConfig.message, identifier, executionArgs); } const rateLimitResult = (0, promise_helpers_1.handleMaybePromise)(() => rateLimiterFn(field.name, executionArgs, resolverRateLimitConfig), rateLimitError => { if (!rateLimitError) { return true; } if (options.onRateLimitError) { options.onRateLimitError({ error: rateLimitError, ...executionArgs, }); } if (options.transformError) { throw options.transformError(rateLimitError); } const resolvePath = []; let curr = document; const operationAST = (0, utils_1.getOperationASTFromDocument)(document); let currType = (0, utils_1.getDefinedRootType)(schema, operationAST.operation); for (const pathItem of path) { curr = curr[pathItem]; if (curr?.kind === 'Field') { const fieldName = curr.name.value; const responseKey = curr.alias?.value ?? fieldName; let field; if ((0, graphql_1.isObjectType)(currType)) { field = currType.getFields()[fieldName]; } else if ((0, graphql_1.isAbstractType)(currType)) { for (const possibleType of schema.getPossibleTypes(currType)) { field = possibleType.getFields()[fieldName]; if (field) { break; } } } if ((0, graphql_1.isListType)(field?.type)) { resolvePath.push('@'); } resolvePath.push(responseKey); if (field?.type) { currType = (0, graphql_1.getNamedType)(field.type); } } } const errorOptions = { extensions: { http: { statusCode: 429 } }, path: resolvePath, nodes: [node], }; if (resolverRateLimitConfig.window) { errorOptions.extensions.http.headers = { 'Retry-After': resolverRateLimitConfig.window, }; } errors.push((0, utils_1.createGraphQLError)(rateLimitError, errorOptions)); return false; }); if ((0, types_1.isPromise)(rateLimitResult)) { rateLimitCalls.add(rateLimitResult); return node; } if (rateLimitResult === false) { return null; } } return node; }, })); return (0, promise_helpers_1.handleMaybePromise)(() => (rateLimitCalls.size ? Promise.all(rateLimitCalls) : undefined), () => { if (errors.length) { setResultAndStopExecution({ errors, }); } }); }, onContextBuilding({ extendContext }) { extendContext({ rateLimiterFn, }); }, }; }; exports.useRateLimiter = useRateLimiter; function interpolateByArgs(message, args) { return message.replace(/\{{([^)]*)\}}/g, (_, key) => args[key.trim()]); } function resolveIdentifierTemplate(template, getArgValues, context) { return template.replace(/\{([^}]+)\}/g, (_, path) => String((0, lodash_get_1.default)({ args: getArgValues(), context }, path.trim()) ?? '')); } var 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; } }); var redis_store_js_1 = require("./redis-store.js"); Object.defineProperty(exports, "RedisStore", { enumerable: true, get: function () { return redis_store_js_1.RedisStore; } }); var store_js_1 = require("./store.js"); Object.defineProperty(exports, "Store", { enumerable: true, get: function () { return store_js_1.Store; } });