@envelop/rate-limiter 10.0.0-alpha-20251212093955-081ac8d8cd90f8b5d037dd416f9eca842de513d1 → 10.0.0-alpha-20251212111337-eb48e39e96d1f746a95199be597e2c18e4734915

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59,9 +59,9 @@ userConfig) => {
59
59
  * @param args - pass the resolver args as an object
60
60
  * @param config - field level config
61
61
  */
62
- const rateLimiter = (fieldName,
62
+ const rateLimiter = (
63
63
  // Resolver args
64
- { args, context, },
64
+ { args, context, info, },
65
65
  // Field level config (e.g. the directive parameters)
66
66
  { arrayLengthField, identityArgs, max, window, message, readOnly, uncountRejected, }) => {
67
67
  // Identify the user or client on the context
@@ -69,7 +69,7 @@ userConfig) => {
69
69
  // User defined window in ms that this field can be accessed for before the call is expired
70
70
  const windowMs = (window ? (0, ms_1.default)(window) : DEFAULT_WINDOW);
71
71
  // String key for this field
72
- const fieldIdentity = getFieldIdentity(fieldName, identityArgs || DEFAULT_FIELD_IDENTITY_ARGS, args);
72
+ const fieldIdentity = getFieldIdentity(info.fieldName, identityArgs || DEFAULT_FIELD_IDENTITY_ARGS, args);
73
73
  // User configured maximum calls to this field
74
74
  const maxCalls = max || DEFAULT_MAX;
75
75
  // Call count could be determined by the lenght of the array value, but most commonly 1
@@ -107,7 +107,7 @@ userConfig) => {
107
107
  formatError({
108
108
  contextIdentity,
109
109
  fieldIdentity,
110
- fieldName,
110
+ fieldName: info.fieldName,
111
111
  max: maxCalls,
112
112
  window: windowMs,
113
113
  });
package/cjs/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useRateLimiter = exports.defaultInterpolateMessageFn = exports.DIRECTIVE_SDL = exports.Store = exports.RedisStore = exports.InMemoryStore = void 0;
3
+ exports.useRateLimiter = exports.defaultInterpolateMessageFn = exports.DIRECTIVE_SDL = exports.Store = exports.RedisStore = exports.RateLimitError = exports.InMemoryStore = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const types_1 = require("util/types");
6
5
  const graphql_1 = require("graphql");
7
6
  const picomatch_1 = tslib_1.__importDefault(require("picomatch"));
8
7
  const utils_1 = require("@graphql-tools/utils");
@@ -10,6 +9,8 @@ const promise_helpers_1 = require("@whatwg-node/promise-helpers");
10
9
  const get_graphql_rate_limiter_js_1 = require("./get-graphql-rate-limiter.js");
11
10
  const in_memory_store_js_1 = require("./in-memory-store.js");
12
11
  Object.defineProperty(exports, "InMemoryStore", { enumerable: true, get: function () { return in_memory_store_js_1.InMemoryStore; } });
12
+ const rate_limit_error_js_1 = require("./rate-limit-error.js");
13
+ Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return rate_limit_error_js_1.RateLimitError; } });
13
14
  const redis_store_js_1 = require("./redis-store.js");
14
15
  Object.defineProperty(exports, "RedisStore", { enumerable: true, get: function () { return redis_store_js_1.RedisStore; } });
15
16
  const store_js_1 = require("./store.js");
@@ -27,9 +28,6 @@ exports.DIRECTIVE_SDL = `
27
28
  `;
28
29
  const defaultInterpolateMessageFn = (message, identifier) => interpolateByArgs(message, { id: identifier });
29
30
  exports.defaultInterpolateMessageFn = defaultInterpolateMessageFn;
30
- const getTypeInfo = (0, utils_1.memoize1)(function getTypeInfo(schema) {
31
- return new graphql_1.TypeInfo(schema);
32
- });
33
31
  const useRateLimiter = (options) => {
34
32
  const rateLimiterFn = (0, get_graphql_rate_limiter_js_1.getGraphQLRateLimiter)({
35
33
  ...options,
@@ -42,144 +40,81 @@ const useRateLimiter = (options) => {
42
40
  type: (0, picomatch_1.default)(config.type),
43
41
  field: (0, picomatch_1.default)(config.field),
44
42
  },
45
- })) || [];
43
+ }));
46
44
  const directiveName = options.rateLimitDirectiveName ?? 'rateLimit';
47
- const getRateLimitConfig = (0, utils_1.memoize4)(function getFieldConfigs(configByField, schema, type, field) {
48
- const fieldConfigs = configByField?.filter(({ isMatch }) => isMatch.type(type.name) && isMatch.field(field.name));
49
- if (fieldConfigs && fieldConfigs.length > 1) {
50
- throw new Error(`Config error: field '${type.name}.${field.name}' has multiple matching configuration`);
51
- }
52
- const fieldConfig = fieldConfigs?.[0];
53
- const rateLimitDirective = (0, utils_1.getDirectiveExtensions)(field, schema)[directiveName]?.[0];
54
- if (rateLimitDirective && fieldConfig) {
55
- throw new Error(`Config error: field '${type.name}.${field.name}' has both a configuration and a directive`);
56
- }
57
- const rateLimitConfig = rateLimitDirective || fieldConfig;
58
- if (!rateLimitConfig) {
59
- return undefined;
60
- }
61
- rateLimitConfig.max = Number(rateLimitConfig.max);
62
- if (rateLimitConfig?.identifyFn) {
63
- rateLimitConfig.identityArgs = ['identifier', ...(rateLimitConfig.identityArgs ?? [])];
64
- }
65
- return rateLimitConfig;
66
- });
67
45
  return {
68
- onExecute({ args, setResultAndStopExecution }) {
69
- const { document, schema, contextValue: context, variableValues, rootValue: root } = args;
70
- const typeInfo = getTypeInfo(schema);
71
- const rateLimitCalls = new Set();
72
- const errors = [];
73
- args.document = (0, graphql_1.visit)(document, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
74
- Field(node, _key, _parent, path, _ancestors) {
75
- const type = typeInfo.getParentType();
76
- const field = typeInfo.getFieldDef();
77
- if (type != null && field != null) {
78
- const rateLimitConfig = getRateLimitConfig(configByField, schema, type, field);
79
- if (!rateLimitConfig) {
80
- return;
81
- }
82
- const resolverRateLimitConfig = { ...rateLimitConfig };
83
- const identifier = (rateLimitConfig?.identifyFn ?? options.identifyFn)(context);
84
- let args = null;
85
- function getArgValues() {
86
- if (!args) {
87
- if (field) {
88
- args = (0, utils_1.getArgumentValues)(field, node, variableValues);
89
- }
90
- }
91
- return args;
92
- }
93
- const executionArgs = {
94
- identifier,
95
- root,
96
- get args() {
97
- return {
98
- ...(getArgValues() || {}),
99
- identifier,
100
- };
101
- },
102
- context,
103
- type,
104
- field,
105
- };
106
- if (resolverRateLimitConfig.message && identifier) {
107
- resolverRateLimitConfig.message = interpolateMessage(resolverRateLimitConfig.message, identifier, executionArgs);
46
+ onSchemaChange({ schema: _schema }) {
47
+ if (!_schema) {
48
+ return;
49
+ }
50
+ const schema = _schema;
51
+ for (const type of Object.values(schema.getTypeMap())) {
52
+ if (!(0, graphql_1.isObjectType)(type)) {
53
+ continue;
54
+ }
55
+ for (const field of Object.values(type.getFields())) {
56
+ const fieldConfigs = configByField?.filter(({ isMatch }) => isMatch.type(type.name) && isMatch.field(field.name));
57
+ if (fieldConfigs && fieldConfigs.length > 1) {
58
+ throw new Error(`Config error: field '${type.name}.${field.name}' has multiple matching configuration`);
59
+ }
60
+ const fieldConfig = fieldConfigs?.[0];
61
+ const rateLimitDirective = (0, utils_1.getDirectiveExtensions)(field, schema)[directiveName]?.[0];
62
+ if (rateLimitDirective && fieldConfig) {
63
+ throw new Error(`Config error: field '${type.name}.${field.name}' has both a configuration and a directive`);
64
+ }
65
+ const baseConfig = rateLimitDirective ?? fieldConfig;
66
+ if (baseConfig) {
67
+ const rateLimitConfig = { ...baseConfig };
68
+ rateLimitConfig.max = rateLimitConfig.max && Number(rateLimitConfig.max);
69
+ if (fieldConfig?.identifyFn) {
70
+ rateLimitConfig.identityArgs = [
71
+ 'identifier',
72
+ ...(rateLimitConfig.identityArgs ?? []),
73
+ ];
108
74
  }
109
- const rateLimitResult = (0, promise_helpers_1.handleMaybePromise)(() => rateLimiterFn(field.name, executionArgs, resolverRateLimitConfig), rateLimitError => {
110
- if (!rateLimitError) {
111
- return true;
75
+ const originalResolver = field.resolve ?? graphql_1.defaultFieldResolver;
76
+ field.resolve = (parent, args, context, info) => {
77
+ const resolverRateLimitConfig = { ...rateLimitConfig };
78
+ const executionArgs = { parent, args, context, info };
79
+ const identifier = (fieldConfig?.identifyFn ?? options.identifyFn)(context);
80
+ if (fieldConfig?.identifyFn) {
81
+ executionArgs.args = { identifier, ...args };
112
82
  }
113
- if (options.onRateLimitError) {
114
- options.onRateLimitError({
115
- error: rateLimitError,
116
- ...executionArgs,
117
- });
83
+ if (resolverRateLimitConfig.message && identifier) {
84
+ const messageArgs = { root: parent, args, context, info };
85
+ resolverRateLimitConfig.message = interpolateMessage(resolverRateLimitConfig.message, identifier, messageArgs);
118
86
  }
119
- if (options.transformError) {
120
- throw options.transformError(rateLimitError);
121
- }
122
- const resolvePath = [];
123
- let curr = document;
124
- const operationAST = (0, utils_1.getOperationASTFromDocument)(document);
125
- let currType = (0, utils_1.getDefinedRootType)(schema, operationAST.operation);
126
- for (const pathItem of path) {
127
- curr = curr[pathItem];
128
- if (curr?.kind === 'Field') {
129
- const fieldName = curr.name.value;
130
- const responseKey = curr.alias?.value ?? fieldName;
131
- let field;
132
- if ((0, graphql_1.isObjectType)(currType)) {
133
- field = currType.getFields()[fieldName];
134
- }
135
- else if ((0, graphql_1.isAbstractType)(currType)) {
136
- for (const possibleType of schema.getPossibleTypes(currType)) {
137
- field = possibleType.getFields()[fieldName];
138
- if (field) {
139
- break;
140
- }
141
- }
142
- }
143
- if ((0, graphql_1.isListType)(field?.type)) {
144
- resolvePath.push('@');
145
- }
146
- resolvePath.push(responseKey);
147
- if (field?.type) {
148
- currType = (0, graphql_1.getNamedType)(field.type);
149
- }
87
+ return (0, promise_helpers_1.handleMaybePromise)(() => rateLimiterFn(executionArgs, resolverRateLimitConfig), rateLimitError => {
88
+ if (!rateLimitError) {
89
+ return originalResolver(parent, args, context, info);
150
90
  }
151
- }
152
- const errorOptions = {
153
- extensions: { http: { statusCode: 429 } },
154
- path: resolvePath,
155
- nodes: [node],
156
- };
157
- if (resolverRateLimitConfig.window) {
158
- errorOptions.extensions.http.headers = {
159
- 'Retry-After': resolverRateLimitConfig.window,
91
+ if (options.onRateLimitError) {
92
+ options.onRateLimitError({
93
+ error: rateLimitError,
94
+ identifier,
95
+ context,
96
+ info,
97
+ });
98
+ }
99
+ if (options.transformError) {
100
+ throw options.transformError(rateLimitError);
101
+ }
102
+ const errorOptions = {
103
+ extensions: { http: { statusCode: 429 } },
104
+ path: (0, graphql_1.responsePathAsArray)(info.path),
105
+ nodes: info.fieldNodes,
160
106
  };
161
- }
162
- errors.push((0, utils_1.createGraphQLError)(rateLimitError, errorOptions));
163
- return false;
164
- });
165
- if ((0, types_1.isPromise)(rateLimitResult)) {
166
- rateLimitCalls.add(rateLimitResult);
167
- return node;
168
- }
169
- else if (rateLimitResult === false) {
170
- return null;
171
- }
107
+ if (resolverRateLimitConfig.window) {
108
+ errorOptions.extensions.http.headers = {
109
+ 'Retry-After': resolverRateLimitConfig.window,
110
+ };
111
+ }
112
+ throw (0, utils_1.createGraphQLError)(rateLimitError, errorOptions);
113
+ });
114
+ };
172
115
  }
173
- return node;
174
- },
175
- }));
176
- return (0, promise_helpers_1.handleMaybePromise)(() => (rateLimitCalls.size ? Promise.all(rateLimitCalls) : undefined), () => {
177
- if (errors.length) {
178
- setResultAndStopExecution({
179
- errors,
180
- });
181
116
  }
182
- });
117
+ }
183
118
  },
184
119
  onContextBuilding({ extendContext }) {
185
120
  extendContext({
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RateLimitError = void 0;
4
+ class RateLimitError extends Error {
5
+ isRateLimitError = true;
6
+ constructor(message) {
7
+ super(message);
8
+ Object.setPrototypeOf(this, RateLimitError.prototype);
9
+ }
10
+ }
11
+ exports.RateLimitError = RateLimitError;
@@ -54,9 +54,9 @@ userConfig) => {
54
54
  * @param args - pass the resolver args as an object
55
55
  * @param config - field level config
56
56
  */
57
- const rateLimiter = (fieldName,
57
+ const rateLimiter = (
58
58
  // Resolver args
59
- { args, context, },
59
+ { args, context, info, },
60
60
  // Field level config (e.g. the directive parameters)
61
61
  { arrayLengthField, identityArgs, max, window, message, readOnly, uncountRejected, }) => {
62
62
  // Identify the user or client on the context
@@ -64,7 +64,7 @@ userConfig) => {
64
64
  // User defined window in ms that this field can be accessed for before the call is expired
65
65
  const windowMs = (window ? ms(window) : DEFAULT_WINDOW);
66
66
  // String key for this field
67
- const fieldIdentity = getFieldIdentity(fieldName, identityArgs || DEFAULT_FIELD_IDENTITY_ARGS, args);
67
+ const fieldIdentity = getFieldIdentity(info.fieldName, identityArgs || DEFAULT_FIELD_IDENTITY_ARGS, args);
68
68
  // User configured maximum calls to this field
69
69
  const maxCalls = max || DEFAULT_MAX;
70
70
  // Call count could be determined by the lenght of the array value, but most commonly 1
@@ -102,7 +102,7 @@ userConfig) => {
102
102
  formatError({
103
103
  contextIdentity,
104
104
  fieldIdentity,
105
- fieldName,
105
+ fieldName: info.fieldName,
106
106
  max: maxCalls,
107
107
  window: windowMs,
108
108
  });
package/esm/index.js CHANGED
@@ -1,13 +1,13 @@
1
- import { isPromise } from 'util/types';
2
- import { getNamedType, isAbstractType, isListType, isObjectType, TypeInfo, visit, visitWithTypeInfo, } from 'graphql';
1
+ import { defaultFieldResolver, isObjectType, responsePathAsArray, } from 'graphql';
3
2
  import picomatch from 'picomatch';
4
- import { createGraphQLError, getArgumentValues, getDefinedRootType, getDirectiveExtensions, getOperationASTFromDocument, memoize1, memoize4, } from '@graphql-tools/utils';
3
+ import { createGraphQLError, getDirectiveExtensions } from '@graphql-tools/utils';
5
4
  import { handleMaybePromise } from '@whatwg-node/promise-helpers';
6
5
  import { getGraphQLRateLimiter } from './get-graphql-rate-limiter.js';
7
6
  import { InMemoryStore } from './in-memory-store.js';
7
+ import { RateLimitError } from './rate-limit-error.js';
8
8
  import { RedisStore } from './redis-store.js';
9
9
  import { Store } from './store.js';
10
- export { InMemoryStore, RedisStore, Store, };
10
+ export { InMemoryStore, RateLimitError, RedisStore, Store, };
11
11
  export const DIRECTIVE_SDL = /* GraphQL */ `
12
12
  directive @rateLimit(
13
13
  max: Int
@@ -20,9 +20,6 @@ export const DIRECTIVE_SDL = /* GraphQL */ `
20
20
  ) on FIELD_DEFINITION
21
21
  `;
22
22
  export const defaultInterpolateMessageFn = (message, identifier) => interpolateByArgs(message, { id: identifier });
23
- const getTypeInfo = memoize1(function getTypeInfo(schema) {
24
- return new TypeInfo(schema);
25
- });
26
23
  export const useRateLimiter = (options) => {
27
24
  const rateLimiterFn = getGraphQLRateLimiter({
28
25
  ...options,
@@ -35,144 +32,81 @@ export const useRateLimiter = (options) => {
35
32
  type: picomatch(config.type),
36
33
  field: picomatch(config.field),
37
34
  },
38
- })) || [];
35
+ }));
39
36
  const directiveName = options.rateLimitDirectiveName ?? 'rateLimit';
40
- const getRateLimitConfig = memoize4(function getFieldConfigs(configByField, schema, type, field) {
41
- const fieldConfigs = configByField?.filter(({ isMatch }) => isMatch.type(type.name) && isMatch.field(field.name));
42
- if (fieldConfigs && fieldConfigs.length > 1) {
43
- throw new Error(`Config error: field '${type.name}.${field.name}' has multiple matching configuration`);
44
- }
45
- const fieldConfig = fieldConfigs?.[0];
46
- const rateLimitDirective = getDirectiveExtensions(field, schema)[directiveName]?.[0];
47
- if (rateLimitDirective && fieldConfig) {
48
- throw new Error(`Config error: field '${type.name}.${field.name}' has both a configuration and a directive`);
49
- }
50
- const rateLimitConfig = rateLimitDirective || fieldConfig;
51
- if (!rateLimitConfig) {
52
- return undefined;
53
- }
54
- rateLimitConfig.max = Number(rateLimitConfig.max);
55
- if (rateLimitConfig?.identifyFn) {
56
- rateLimitConfig.identityArgs = ['identifier', ...(rateLimitConfig.identityArgs ?? [])];
57
- }
58
- return rateLimitConfig;
59
- });
60
37
  return {
61
- onExecute({ args, setResultAndStopExecution }) {
62
- const { document, schema, contextValue: context, variableValues, rootValue: root } = args;
63
- const typeInfo = getTypeInfo(schema);
64
- const rateLimitCalls = new Set();
65
- const errors = [];
66
- args.document = visit(document, visitWithTypeInfo(typeInfo, {
67
- Field(node, _key, _parent, path, _ancestors) {
68
- const type = typeInfo.getParentType();
69
- const field = typeInfo.getFieldDef();
70
- if (type != null && field != null) {
71
- const rateLimitConfig = getRateLimitConfig(configByField, schema, type, field);
72
- if (!rateLimitConfig) {
73
- return;
74
- }
75
- const resolverRateLimitConfig = { ...rateLimitConfig };
76
- const identifier = (rateLimitConfig?.identifyFn ?? options.identifyFn)(context);
77
- let args = null;
78
- function getArgValues() {
79
- if (!args) {
80
- if (field) {
81
- args = getArgumentValues(field, node, variableValues);
82
- }
83
- }
84
- return args;
85
- }
86
- const executionArgs = {
87
- identifier,
88
- root,
89
- get args() {
90
- return {
91
- ...(getArgValues() || {}),
92
- identifier,
93
- };
94
- },
95
- context,
96
- type,
97
- field,
98
- };
99
- if (resolverRateLimitConfig.message && identifier) {
100
- resolverRateLimitConfig.message = interpolateMessage(resolverRateLimitConfig.message, identifier, executionArgs);
38
+ onSchemaChange({ schema: _schema }) {
39
+ if (!_schema) {
40
+ return;
41
+ }
42
+ const schema = _schema;
43
+ for (const type of Object.values(schema.getTypeMap())) {
44
+ if (!isObjectType(type)) {
45
+ continue;
46
+ }
47
+ for (const field of Object.values(type.getFields())) {
48
+ const fieldConfigs = configByField?.filter(({ isMatch }) => isMatch.type(type.name) && isMatch.field(field.name));
49
+ if (fieldConfigs && fieldConfigs.length > 1) {
50
+ throw new Error(`Config error: field '${type.name}.${field.name}' has multiple matching configuration`);
51
+ }
52
+ const fieldConfig = fieldConfigs?.[0];
53
+ const rateLimitDirective = getDirectiveExtensions(field, schema)[directiveName]?.[0];
54
+ if (rateLimitDirective && fieldConfig) {
55
+ throw new Error(`Config error: field '${type.name}.${field.name}' has both a configuration and a directive`);
56
+ }
57
+ const baseConfig = rateLimitDirective ?? fieldConfig;
58
+ if (baseConfig) {
59
+ const rateLimitConfig = { ...baseConfig };
60
+ rateLimitConfig.max = rateLimitConfig.max && Number(rateLimitConfig.max);
61
+ if (fieldConfig?.identifyFn) {
62
+ rateLimitConfig.identityArgs = [
63
+ 'identifier',
64
+ ...(rateLimitConfig.identityArgs ?? []),
65
+ ];
101
66
  }
102
- const rateLimitResult = handleMaybePromise(() => rateLimiterFn(field.name, executionArgs, resolverRateLimitConfig), rateLimitError => {
103
- if (!rateLimitError) {
104
- return true;
67
+ const originalResolver = field.resolve ?? defaultFieldResolver;
68
+ field.resolve = (parent, args, context, info) => {
69
+ const resolverRateLimitConfig = { ...rateLimitConfig };
70
+ const executionArgs = { parent, args, context, info };
71
+ const identifier = (fieldConfig?.identifyFn ?? options.identifyFn)(context);
72
+ if (fieldConfig?.identifyFn) {
73
+ executionArgs.args = { identifier, ...args };
105
74
  }
106
- if (options.onRateLimitError) {
107
- options.onRateLimitError({
108
- error: rateLimitError,
109
- ...executionArgs,
110
- });
75
+ if (resolverRateLimitConfig.message && identifier) {
76
+ const messageArgs = { root: parent, args, context, info };
77
+ resolverRateLimitConfig.message = interpolateMessage(resolverRateLimitConfig.message, identifier, messageArgs);
111
78
  }
112
- if (options.transformError) {
113
- throw options.transformError(rateLimitError);
114
- }
115
- const resolvePath = [];
116
- let curr = document;
117
- const operationAST = getOperationASTFromDocument(document);
118
- let currType = getDefinedRootType(schema, operationAST.operation);
119
- for (const pathItem of path) {
120
- curr = curr[pathItem];
121
- if (curr?.kind === 'Field') {
122
- const fieldName = curr.name.value;
123
- const responseKey = curr.alias?.value ?? fieldName;
124
- let field;
125
- if (isObjectType(currType)) {
126
- field = currType.getFields()[fieldName];
127
- }
128
- else if (isAbstractType(currType)) {
129
- for (const possibleType of schema.getPossibleTypes(currType)) {
130
- field = possibleType.getFields()[fieldName];
131
- if (field) {
132
- break;
133
- }
134
- }
135
- }
136
- if (isListType(field?.type)) {
137
- resolvePath.push('@');
138
- }
139
- resolvePath.push(responseKey);
140
- if (field?.type) {
141
- currType = getNamedType(field.type);
142
- }
79
+ return handleMaybePromise(() => rateLimiterFn(executionArgs, resolverRateLimitConfig), rateLimitError => {
80
+ if (!rateLimitError) {
81
+ return originalResolver(parent, args, context, info);
143
82
  }
144
- }
145
- const errorOptions = {
146
- extensions: { http: { statusCode: 429 } },
147
- path: resolvePath,
148
- nodes: [node],
149
- };
150
- if (resolverRateLimitConfig.window) {
151
- errorOptions.extensions.http.headers = {
152
- 'Retry-After': resolverRateLimitConfig.window,
83
+ if (options.onRateLimitError) {
84
+ options.onRateLimitError({
85
+ error: rateLimitError,
86
+ identifier,
87
+ context,
88
+ info,
89
+ });
90
+ }
91
+ if (options.transformError) {
92
+ throw options.transformError(rateLimitError);
93
+ }
94
+ const errorOptions = {
95
+ extensions: { http: { statusCode: 429 } },
96
+ path: responsePathAsArray(info.path),
97
+ nodes: info.fieldNodes,
153
98
  };
154
- }
155
- errors.push(createGraphQLError(rateLimitError, errorOptions));
156
- return false;
157
- });
158
- if (isPromise(rateLimitResult)) {
159
- rateLimitCalls.add(rateLimitResult);
160
- return node;
161
- }
162
- else if (rateLimitResult === false) {
163
- return null;
164
- }
99
+ if (resolverRateLimitConfig.window) {
100
+ errorOptions.extensions.http.headers = {
101
+ 'Retry-After': resolverRateLimitConfig.window,
102
+ };
103
+ }
104
+ throw createGraphQLError(rateLimitError, errorOptions);
105
+ });
106
+ };
165
107
  }
166
- return node;
167
- },
168
- }));
169
- return handleMaybePromise(() => (rateLimitCalls.size ? Promise.all(rateLimitCalls) : undefined), () => {
170
- if (errors.length) {
171
- setResultAndStopExecution({
172
- errors,
173
- });
174
108
  }
175
- });
109
+ }
176
110
  },
177
111
  onContextBuilding({ extendContext }) {
178
112
  extendContext({
@@ -0,0 +1,8 @@
1
+ class RateLimitError extends Error {
2
+ isRateLimitError = true;
3
+ constructor(message) {
4
+ super(message);
5
+ Object.setPrototypeOf(this, RateLimitError.prototype);
6
+ }
7
+ }
8
+ export { RateLimitError };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@envelop/rate-limiter",
3
- "version": "10.0.0-alpha-20251212093955-081ac8d8cd90f8b5d037dd416f9eca842de513d1",
3
+ "version": "10.0.0-alpha-20251212111337-eb48e39e96d1f746a95199be597e2c18e4734915",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
6
  "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
7
- "@envelop/core": "^5.5.0-alpha-20251212093955-081ac8d8cd90f8b5d037dd416f9eca842de513d1"
7
+ "@envelop/core": "^5.5.0-alpha-20251212111337-eb48e39e96d1f746a95199be597e2c18e4734915"
8
8
  },
9
9
  "dependencies": {
10
10
  "@graphql-tools/utils": "^10.5.4",
@@ -14,7 +14,7 @@
14
14
  "ms": "^2.1.3",
15
15
  "picomatch": "^4.0.3",
16
16
  "tslib": "^2.5.0",
17
- "@envelop/on-resolve": "^8.0.0-alpha-20251212093955-081ac8d8cd90f8b5d037dd416f9eca842de513d1"
17
+ "@envelop/on-resolve": "^8.0.0-alpha-20251212111337-eb48e39e96d1f746a95199be597e2c18e4734915"
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
@@ -1,3 +1,4 @@
1
+ import type { GraphQLResolveInfo } from 'graphql';
1
2
  import { MaybePromise } from '@whatwg-node/promise-helpers';
2
3
  import type { GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs } from './types.cjs';
3
4
  /**
@@ -19,8 +20,10 @@ declare const getFieldIdentity: (fieldName: string, identityArgs: readonly strin
19
20
  * can wrap this or it can be used directly in resolvers.
20
21
  * @param userConfig - global (usually app-wide) rate limiting config
21
22
  */
22
- declare const getGraphQLRateLimiter: (userConfig: GraphQLRateLimitConfig) => ((fieldName: string, { args, context, }: {
23
+ declare const getGraphQLRateLimiter: (userConfig: GraphQLRateLimitConfig) => (({ args, context, info, }: {
24
+ parent: any;
23
25
  args: Record<string, any>;
24
26
  context: any;
27
+ info: GraphQLResolveInfo;
25
28
  }, { arrayLengthField, identityArgs, max, window, message, uncountRejected, }: GraphQLRateLimitDirectiveArgs) => MaybePromise<string | undefined>);
26
29
  export { getGraphQLRateLimiter, getFieldIdentity };
@@ -1,3 +1,4 @@
1
+ import type { GraphQLResolveInfo } from 'graphql';
1
2
  import { MaybePromise } from '@whatwg-node/promise-helpers';
2
3
  import type { GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs } from './types.js';
3
4
  /**
@@ -19,8 +20,10 @@ declare const getFieldIdentity: (fieldName: string, identityArgs: readonly strin
19
20
  * can wrap this or it can be used directly in resolvers.
20
21
  * @param userConfig - global (usually app-wide) rate limiting config
21
22
  */
22
- declare const getGraphQLRateLimiter: (userConfig: GraphQLRateLimitConfig) => ((fieldName: string, { args, context, }: {
23
+ declare const getGraphQLRateLimiter: (userConfig: GraphQLRateLimitConfig) => (({ args, context, info, }: {
24
+ parent: any;
23
25
  args: Record<string, any>;
24
26
  context: any;
27
+ info: GraphQLResolveInfo;
25
28
  }, { arrayLengthField, identityArgs, max, window, message, uncountRejected, }: GraphQLRateLimitDirectiveArgs) => MaybePromise<string | undefined>);
26
29
  export { getGraphQLRateLimiter, getFieldIdentity };
@@ -1,20 +1,19 @@
1
- import { GraphQLField, GraphQLNamedOutputType } from 'graphql';
1
+ import { GraphQLResolveInfo } from 'graphql';
2
2
  import type { Plugin } from '@envelop/core';
3
3
  import { getGraphQLRateLimiter } from './get-graphql-rate-limiter.cjs';
4
4
  import { InMemoryStore } from './in-memory-store.cjs';
5
+ import { RateLimitError } from './rate-limit-error.cjs';
5
6
  import { RedisStore } from './redis-store.cjs';
6
7
  import { Store } from './store.cjs';
7
8
  import { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, Options } from './types.cjs';
8
- export { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, InMemoryStore, Options, RedisStore, Store, };
9
+ export { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, InMemoryStore, Options, RateLimitError, RedisStore, Store, };
9
10
  export type IdentifyFn<ContextType = unknown> = (context: ContextType) => string;
10
- interface RateLimitExecutionParams<ContextType = unknown> {
11
+ export type MessageInterpolator<ContextType = unknown> = (message: string, identifier: string, params: {
11
12
  root: unknown;
12
13
  args: Record<string, unknown>;
13
14
  context: ContextType;
14
- type: GraphQLNamedOutputType;
15
- field: GraphQLField<any, any>;
16
- }
17
- export type MessageInterpolator<ContextType = unknown> = (message: string, identifier: string, params: RateLimitExecutionParams<ContextType>) => string;
15
+ info: GraphQLResolveInfo;
16
+ }) => string;
18
17
  export declare const DIRECTIVE_SDL = "\n directive @rateLimit(\n max: Int\n window: String\n message: String\n identityArgs: [String]\n arrayLengthField: String\n readOnly: Boolean\n uncountRejected: Boolean\n ) on FIELD_DEFINITION\n";
19
18
  export type RateLimitDirectiveArgs = {
20
19
  max?: number;
@@ -32,7 +31,9 @@ export type RateLimiterPluginOptions = {
32
31
  onRateLimitError?: (event: {
33
32
  error: string;
34
33
  identifier: string;
35
- } & RateLimitExecutionParams) => void;
34
+ context: unknown;
35
+ info: GraphQLResolveInfo;
36
+ }) => void;
36
37
  interpolateMessage?: MessageInterpolator;
37
38
  configByField?: ConfigByField[];
38
39
  } & Omit<GraphQLRateLimitConfig, 'identifyContext'>;
@@ -1,20 +1,19 @@
1
- import { GraphQLField, GraphQLNamedOutputType } from 'graphql';
1
+ import { GraphQLResolveInfo } from 'graphql';
2
2
  import type { Plugin } from '@envelop/core';
3
3
  import { getGraphQLRateLimiter } from './get-graphql-rate-limiter.js';
4
4
  import { InMemoryStore } from './in-memory-store.js';
5
+ import { RateLimitError } from './rate-limit-error.js';
5
6
  import { RedisStore } from './redis-store.js';
6
7
  import { Store } from './store.js';
7
8
  import { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, Options } from './types.js';
8
- export { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, InMemoryStore, Options, RedisStore, Store, };
9
+ export { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, InMemoryStore, Options, RateLimitError, RedisStore, Store, };
9
10
  export type IdentifyFn<ContextType = unknown> = (context: ContextType) => string;
10
- interface RateLimitExecutionParams<ContextType = unknown> {
11
+ export type MessageInterpolator<ContextType = unknown> = (message: string, identifier: string, params: {
11
12
  root: unknown;
12
13
  args: Record<string, unknown>;
13
14
  context: ContextType;
14
- type: GraphQLNamedOutputType;
15
- field: GraphQLField<any, any>;
16
- }
17
- export type MessageInterpolator<ContextType = unknown> = (message: string, identifier: string, params: RateLimitExecutionParams<ContextType>) => string;
15
+ info: GraphQLResolveInfo;
16
+ }) => string;
18
17
  export declare const DIRECTIVE_SDL = "\n directive @rateLimit(\n max: Int\n window: String\n message: String\n identityArgs: [String]\n arrayLengthField: String\n readOnly: Boolean\n uncountRejected: Boolean\n ) on FIELD_DEFINITION\n";
19
18
  export type RateLimitDirectiveArgs = {
20
19
  max?: number;
@@ -32,7 +31,9 @@ export type RateLimiterPluginOptions = {
32
31
  onRateLimitError?: (event: {
33
32
  error: string;
34
33
  identifier: string;
35
- } & RateLimitExecutionParams) => void;
34
+ context: unknown;
35
+ info: GraphQLResolveInfo;
36
+ }) => void;
36
37
  interpolateMessage?: MessageInterpolator;
37
38
  configByField?: ConfigByField[];
38
39
  } & Omit<GraphQLRateLimitConfig, 'identifyContext'>;
@@ -0,0 +1,5 @@
1
+ declare class RateLimitError extends Error {
2
+ readonly isRateLimitError = true;
3
+ constructor(message: string);
4
+ }
5
+ export { RateLimitError };
@@ -0,0 +1,5 @@
1
+ declare class RateLimitError extends Error {
2
+ readonly isRateLimitError = true;
3
+ constructor(message: string);
4
+ }
5
+ export { RateLimitError };
@@ -89,5 +89,11 @@ export interface GraphQLRateLimitConfig {
89
89
  * Custom error messages.
90
90
  */
91
91
  readonly formatError?: (input: FormatErrorInput) => string;
92
+ /**
93
+ * Return an error.
94
+ *
95
+ * Defaults to new RateLimitError.
96
+ */
97
+ readonly createError?: (message: string) => Error;
92
98
  readonly enableBatchRequestCache?: boolean;
93
99
  }
@@ -89,5 +89,11 @@ export interface GraphQLRateLimitConfig {
89
89
  * Custom error messages.
90
90
  */
91
91
  readonly formatError?: (input: FormatErrorInput) => string;
92
+ /**
93
+ * Return an error.
94
+ *
95
+ * Defaults to new RateLimitError.
96
+ */
97
+ readonly createError?: (message: string) => Error;
92
98
  readonly enableBatchRequestCache?: boolean;
93
99
  }