@envelop/rate-limiter 10.0.0-alpha-20251212015846-91d81e2860e085b5f5e1a780e4cab2f58e349142 → 10.0.0-alpha-20251212093955-081ac8d8cd90f8b5d037dd416f9eca842de513d1
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.
- package/cjs/get-graphql-rate-limiter.js +4 -4
- package/cjs/index.js +134 -69
- package/esm/get-graphql-rate-limiter.js +4 -4
- package/esm/index.js +136 -70
- package/package.json +3 -3
- package/typings/get-graphql-rate-limiter.d.cts +1 -4
- package/typings/get-graphql-rate-limiter.d.ts +1 -4
- package/typings/index.d.cts +8 -9
- package/typings/index.d.ts +8 -9
- package/typings/types.d.cts +0 -6
- package/typings/types.d.ts +0 -6
- package/cjs/rate-limit-error.js +0 -11
- package/esm/rate-limit-error.js +0 -8
- package/typings/rate-limit-error.d.cts +0 -5
- package/typings/rate-limit-error.d.ts +0 -5
|
@@ -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 = (
|
|
62
|
+
const rateLimiter = (fieldName,
|
|
63
63
|
// Resolver args
|
|
64
|
-
{ args, context,
|
|
64
|
+
{ args, context, },
|
|
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(
|
|
72
|
+
const fieldIdentity = getFieldIdentity(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,
|
|
111
111
|
max: maxCalls,
|
|
112
112
|
window: windowMs,
|
|
113
113
|
});
|
package/cjs/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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.
|
|
3
|
+
exports.useRateLimiter = exports.defaultInterpolateMessageFn = exports.DIRECTIVE_SDL = exports.Store = exports.RedisStore = exports.InMemoryStore = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
const types_1 = require("util/types");
|
|
5
6
|
const graphql_1 = require("graphql");
|
|
6
7
|
const picomatch_1 = tslib_1.__importDefault(require("picomatch"));
|
|
7
8
|
const utils_1 = require("@graphql-tools/utils");
|
|
@@ -9,8 +10,6 @@ const promise_helpers_1 = require("@whatwg-node/promise-helpers");
|
|
|
9
10
|
const get_graphql_rate_limiter_js_1 = require("./get-graphql-rate-limiter.js");
|
|
10
11
|
const in_memory_store_js_1 = require("./in-memory-store.js");
|
|
11
12
|
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; } });
|
|
14
13
|
const redis_store_js_1 = require("./redis-store.js");
|
|
15
14
|
Object.defineProperty(exports, "RedisStore", { enumerable: true, get: function () { return redis_store_js_1.RedisStore; } });
|
|
16
15
|
const store_js_1 = require("./store.js");
|
|
@@ -28,6 +27,9 @@ exports.DIRECTIVE_SDL = `
|
|
|
28
27
|
`;
|
|
29
28
|
const defaultInterpolateMessageFn = (message, identifier) => interpolateByArgs(message, { id: identifier });
|
|
30
29
|
exports.defaultInterpolateMessageFn = defaultInterpolateMessageFn;
|
|
30
|
+
const getTypeInfo = (0, utils_1.memoize1)(function getTypeInfo(schema) {
|
|
31
|
+
return new graphql_1.TypeInfo(schema);
|
|
32
|
+
});
|
|
31
33
|
const useRateLimiter = (options) => {
|
|
32
34
|
const rateLimiterFn = (0, get_graphql_rate_limiter_js_1.getGraphQLRateLimiter)({
|
|
33
35
|
...options,
|
|
@@ -40,81 +42,144 @@ const useRateLimiter = (options) => {
|
|
|
40
42
|
type: (0, picomatch_1.default)(config.type),
|
|
41
43
|
field: (0, picomatch_1.default)(config.field),
|
|
42
44
|
},
|
|
43
|
-
}));
|
|
45
|
+
})) || [];
|
|
44
46
|
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
|
+
});
|
|
45
67
|
return {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
];
|
|
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;
|
|
74
81
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
}
|
|
82
90
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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);
|
|
108
|
+
}
|
|
109
|
+
const rateLimitResult = (0, promise_helpers_1.handleMaybePromise)(() => rateLimiterFn(field.name, executionArgs, resolverRateLimitConfig), rateLimitError => {
|
|
110
|
+
if (!rateLimitError) {
|
|
111
|
+
return true;
|
|
86
112
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
if (options.onRateLimitError) {
|
|
114
|
+
options.onRateLimitError({
|
|
115
|
+
error: rateLimitError,
|
|
116
|
+
...executionArgs,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
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
|
+
}
|
|
101
150
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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,
|
|
106
160
|
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
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
|
+
}
|
|
115
172
|
}
|
|
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
|
+
});
|
|
116
181
|
}
|
|
117
|
-
}
|
|
182
|
+
});
|
|
118
183
|
},
|
|
119
184
|
onContextBuilding({ extendContext }) {
|
|
120
185
|
extendContext({
|
|
@@ -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 = (
|
|
57
|
+
const rateLimiter = (fieldName,
|
|
58
58
|
// Resolver args
|
|
59
|
-
{ args, context,
|
|
59
|
+
{ args, context, },
|
|
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(
|
|
67
|
+
const fieldIdentity = getFieldIdentity(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,
|
|
106
106
|
max: maxCalls,
|
|
107
107
|
window: windowMs,
|
|
108
108
|
});
|
package/esm/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isPromise } from 'util/types';
|
|
2
|
+
import { getNamedType, isAbstractType, isListType, isObjectType, TypeInfo, visit, visitWithTypeInfo, } from 'graphql';
|
|
2
3
|
import picomatch from 'picomatch';
|
|
3
|
-
import { createGraphQLError, getDirectiveExtensions } from '@graphql-tools/utils';
|
|
4
|
+
import { createGraphQLError, getArgumentValues, getDefinedRootType, getDirectiveExtensions, getOperationASTFromDocument, memoize1, memoize4, } from '@graphql-tools/utils';
|
|
4
5
|
import { handleMaybePromise } from '@whatwg-node/promise-helpers';
|
|
5
6
|
import { getGraphQLRateLimiter } from './get-graphql-rate-limiter.js';
|
|
6
7
|
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,
|
|
10
|
+
export { InMemoryStore, RedisStore, Store, };
|
|
11
11
|
export const DIRECTIVE_SDL = /* GraphQL */ `
|
|
12
12
|
directive @rateLimit(
|
|
13
13
|
max: Int
|
|
@@ -20,6 +20,9 @@ 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
|
+
});
|
|
23
26
|
export const useRateLimiter = (options) => {
|
|
24
27
|
const rateLimiterFn = getGraphQLRateLimiter({
|
|
25
28
|
...options,
|
|
@@ -32,81 +35,144 @@ export const useRateLimiter = (options) => {
|
|
|
32
35
|
type: picomatch(config.type),
|
|
33
36
|
field: picomatch(config.field),
|
|
34
37
|
},
|
|
35
|
-
}));
|
|
38
|
+
})) || [];
|
|
36
39
|
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
|
+
});
|
|
37
60
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
];
|
|
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;
|
|
66
74
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
83
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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);
|
|
101
|
+
}
|
|
102
|
+
const rateLimitResult = handleMaybePromise(() => rateLimiterFn(field.name, executionArgs, resolverRateLimitConfig), rateLimitError => {
|
|
103
|
+
if (!rateLimitError) {
|
|
104
|
+
return true;
|
|
78
105
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
if (options.onRateLimitError) {
|
|
107
|
+
options.onRateLimitError({
|
|
108
|
+
error: rateLimitError,
|
|
109
|
+
...executionArgs,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
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
|
+
}
|
|
93
143
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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,
|
|
98
153
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
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
|
+
}
|
|
107
165
|
}
|
|
166
|
+
return node;
|
|
167
|
+
},
|
|
168
|
+
}));
|
|
169
|
+
return handleMaybePromise(() => (rateLimitCalls.size ? Promise.all(rateLimitCalls) : undefined), () => {
|
|
170
|
+
if (errors.length) {
|
|
171
|
+
setResultAndStopExecution({
|
|
172
|
+
errors,
|
|
173
|
+
});
|
|
108
174
|
}
|
|
109
|
-
}
|
|
175
|
+
});
|
|
110
176
|
},
|
|
111
177
|
onContextBuilding({ extendContext }) {
|
|
112
178
|
extendContext({
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envelop/rate-limiter",
|
|
3
|
-
"version": "10.0.0-alpha-
|
|
3
|
+
"version": "10.0.0-alpha-20251212093955-081ac8d8cd90f8b5d037dd416f9eca842de513d1",
|
|
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-
|
|
7
|
+
"@envelop/core": "^5.5.0-alpha-20251212093955-081ac8d8cd90f8b5d037dd416f9eca842de513d1"
|
|
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-
|
|
17
|
+
"@envelop/on-resolve": "^8.0.0-alpha-20251212093955-081ac8d8cd90f8b5d037dd416f9eca842de513d1"
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { GraphQLResolveInfo } from 'graphql';
|
|
2
1
|
import { MaybePromise } from '@whatwg-node/promise-helpers';
|
|
3
2
|
import type { GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs } from './types.cjs';
|
|
4
3
|
/**
|
|
@@ -20,10 +19,8 @@ declare const getFieldIdentity: (fieldName: string, identityArgs: readonly strin
|
|
|
20
19
|
* can wrap this or it can be used directly in resolvers.
|
|
21
20
|
* @param userConfig - global (usually app-wide) rate limiting config
|
|
22
21
|
*/
|
|
23
|
-
declare const getGraphQLRateLimiter: (userConfig: GraphQLRateLimitConfig) => (({ args, context,
|
|
24
|
-
parent: any;
|
|
22
|
+
declare const getGraphQLRateLimiter: (userConfig: GraphQLRateLimitConfig) => ((fieldName: string, { args, context, }: {
|
|
25
23
|
args: Record<string, any>;
|
|
26
24
|
context: any;
|
|
27
|
-
info: GraphQLResolveInfo;
|
|
28
25
|
}, { arrayLengthField, identityArgs, max, window, message, uncountRejected, }: GraphQLRateLimitDirectiveArgs) => MaybePromise<string | undefined>);
|
|
29
26
|
export { getGraphQLRateLimiter, getFieldIdentity };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { GraphQLResolveInfo } from 'graphql';
|
|
2
1
|
import { MaybePromise } from '@whatwg-node/promise-helpers';
|
|
3
2
|
import type { GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs } from './types.js';
|
|
4
3
|
/**
|
|
@@ -20,10 +19,8 @@ declare const getFieldIdentity: (fieldName: string, identityArgs: readonly strin
|
|
|
20
19
|
* can wrap this or it can be used directly in resolvers.
|
|
21
20
|
* @param userConfig - global (usually app-wide) rate limiting config
|
|
22
21
|
*/
|
|
23
|
-
declare const getGraphQLRateLimiter: (userConfig: GraphQLRateLimitConfig) => (({ args, context,
|
|
24
|
-
parent: any;
|
|
22
|
+
declare const getGraphQLRateLimiter: (userConfig: GraphQLRateLimitConfig) => ((fieldName: string, { args, context, }: {
|
|
25
23
|
args: Record<string, any>;
|
|
26
24
|
context: any;
|
|
27
|
-
info: GraphQLResolveInfo;
|
|
28
25
|
}, { arrayLengthField, identityArgs, max, window, message, uncountRejected, }: GraphQLRateLimitDirectiveArgs) => MaybePromise<string | undefined>);
|
|
29
26
|
export { getGraphQLRateLimiter, getFieldIdentity };
|
package/typings/index.d.cts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GraphQLField, GraphQLNamedOutputType } 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';
|
|
6
5
|
import { RedisStore } from './redis-store.cjs';
|
|
7
6
|
import { Store } from './store.cjs';
|
|
8
7
|
import { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, Options } from './types.cjs';
|
|
9
|
-
export { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, InMemoryStore, Options,
|
|
8
|
+
export { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, InMemoryStore, Options, RedisStore, Store, };
|
|
10
9
|
export type IdentifyFn<ContextType = unknown> = (context: ContextType) => string;
|
|
11
|
-
|
|
10
|
+
interface RateLimitExecutionParams<ContextType = unknown> {
|
|
12
11
|
root: unknown;
|
|
13
12
|
args: Record<string, unknown>;
|
|
14
13
|
context: ContextType;
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
type: GraphQLNamedOutputType;
|
|
15
|
+
field: GraphQLField<any, any>;
|
|
16
|
+
}
|
|
17
|
+
export type MessageInterpolator<ContextType = unknown> = (message: string, identifier: string, params: RateLimitExecutionParams<ContextType>) => string;
|
|
17
18
|
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";
|
|
18
19
|
export type RateLimitDirectiveArgs = {
|
|
19
20
|
max?: number;
|
|
@@ -31,9 +32,7 @@ export type RateLimiterPluginOptions = {
|
|
|
31
32
|
onRateLimitError?: (event: {
|
|
32
33
|
error: string;
|
|
33
34
|
identifier: string;
|
|
34
|
-
|
|
35
|
-
info: GraphQLResolveInfo;
|
|
36
|
-
}) => void;
|
|
35
|
+
} & RateLimitExecutionParams) => void;
|
|
37
36
|
interpolateMessage?: MessageInterpolator;
|
|
38
37
|
configByField?: ConfigByField[];
|
|
39
38
|
} & Omit<GraphQLRateLimitConfig, 'identifyContext'>;
|
package/typings/index.d.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GraphQLField, GraphQLNamedOutputType } 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';
|
|
6
5
|
import { RedisStore } from './redis-store.js';
|
|
7
6
|
import { Store } from './store.js';
|
|
8
7
|
import { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, Options } from './types.js';
|
|
9
|
-
export { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, InMemoryStore, Options,
|
|
8
|
+
export { FormatErrorInput, GraphQLRateLimitConfig, GraphQLRateLimitDirectiveArgs, Identity, InMemoryStore, Options, RedisStore, Store, };
|
|
10
9
|
export type IdentifyFn<ContextType = unknown> = (context: ContextType) => string;
|
|
11
|
-
|
|
10
|
+
interface RateLimitExecutionParams<ContextType = unknown> {
|
|
12
11
|
root: unknown;
|
|
13
12
|
args: Record<string, unknown>;
|
|
14
13
|
context: ContextType;
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
type: GraphQLNamedOutputType;
|
|
15
|
+
field: GraphQLField<any, any>;
|
|
16
|
+
}
|
|
17
|
+
export type MessageInterpolator<ContextType = unknown> = (message: string, identifier: string, params: RateLimitExecutionParams<ContextType>) => string;
|
|
17
18
|
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";
|
|
18
19
|
export type RateLimitDirectiveArgs = {
|
|
19
20
|
max?: number;
|
|
@@ -31,9 +32,7 @@ export type RateLimiterPluginOptions = {
|
|
|
31
32
|
onRateLimitError?: (event: {
|
|
32
33
|
error: string;
|
|
33
34
|
identifier: string;
|
|
34
|
-
|
|
35
|
-
info: GraphQLResolveInfo;
|
|
36
|
-
}) => void;
|
|
35
|
+
} & RateLimitExecutionParams) => void;
|
|
37
36
|
interpolateMessage?: MessageInterpolator;
|
|
38
37
|
configByField?: ConfigByField[];
|
|
39
38
|
} & Omit<GraphQLRateLimitConfig, 'identifyContext'>;
|
package/typings/types.d.cts
CHANGED
|
@@ -89,11 +89,5 @@ 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;
|
|
98
92
|
readonly enableBatchRequestCache?: boolean;
|
|
99
93
|
}
|
package/typings/types.d.ts
CHANGED
|
@@ -89,11 +89,5 @@ 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;
|
|
98
92
|
readonly enableBatchRequestCache?: boolean;
|
|
99
93
|
}
|
package/cjs/rate-limit-error.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
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;
|
package/esm/rate-limit-error.js
DELETED