@envelop/resource-limitations 8.0.1-alpha-20260116142832-a2277336f0a5a4d64d168c98cf97b7513f70b9f7 → 8.1.0-alpha-20260120163327-6e77ec22fc62a09ba152ed764585743e1557eaf8
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/LICENSE +21 -0
- package/cjs/index.js +186 -0
- package/cjs/package.json +1 -0
- package/esm/index.js +181 -0
- package/package.json +32 -52
- package/typings/index.d.cts +45 -0
- package/typings/index.d.ts +45 -0
- package/dist/index.cjs +0 -134
- package/dist/index.d.cts +0 -48
- package/dist/index.d.mts +0 -48
- package/dist/index.mjs +0 -130
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Dotan Simha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/cjs/index.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useResourceLimitations = exports.ResourceLimitationValidationRule = exports.defaultPaginationArgumentMinimum = exports.defaultPaginationArgumentMaximum = exports.defaultNodeCostLimit = void 0;
|
|
4
|
+
const graphql_1 = require("graphql");
|
|
5
|
+
const core_1 = require("@envelop/core");
|
|
6
|
+
const extended_validation_1 = require("@envelop/extended-validation");
|
|
7
|
+
const utils_1 = require("@graphql-tools/utils");
|
|
8
|
+
const getWrappedType = (graphqlType) => {
|
|
9
|
+
if (graphqlType instanceof graphql_1.GraphQLList || graphqlType instanceof graphql_1.GraphQLNonNull) {
|
|
10
|
+
return getWrappedType(graphqlType.ofType);
|
|
11
|
+
}
|
|
12
|
+
return graphqlType;
|
|
13
|
+
};
|
|
14
|
+
const isValidArgType = (type, paginationArgumentTypes) => type === graphql_1.GraphQLInt ||
|
|
15
|
+
((0, graphql_1.isScalarType)(type) && !!paginationArgumentTypes && paginationArgumentTypes.includes(type.name));
|
|
16
|
+
const hasFieldDefConnectionArgs = (field, argumentTypes) => {
|
|
17
|
+
let hasFirst = false;
|
|
18
|
+
let hasLast = false;
|
|
19
|
+
for (const arg of field.args) {
|
|
20
|
+
if (arg.name === 'first' && isValidArgType(arg.type, argumentTypes)) {
|
|
21
|
+
hasFirst = true;
|
|
22
|
+
}
|
|
23
|
+
else if (arg.name === 'last' && isValidArgType(arg.type, argumentTypes)) {
|
|
24
|
+
hasLast = true;
|
|
25
|
+
}
|
|
26
|
+
else if (hasLast && hasFirst) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { hasFirst, hasLast };
|
|
31
|
+
};
|
|
32
|
+
const buildMissingPaginationFieldErrorMessage = (params) => `Missing pagination argument for field '${params.fieldName}'. ` +
|
|
33
|
+
`Please provide ` +
|
|
34
|
+
(params.hasFirst && params.hasLast
|
|
35
|
+
? "either the 'first' or 'last'"
|
|
36
|
+
: params.hasFirst
|
|
37
|
+
? "the 'first'"
|
|
38
|
+
: "the 'last'") +
|
|
39
|
+
' field argument.';
|
|
40
|
+
const buildInvalidPaginationRangeErrorMessage = (params) => `Invalid pagination argument for field '${params.fieldName}'. ` +
|
|
41
|
+
`The value for the '${params.argumentName}' argument must be an integer within ${params.paginationArgumentMinimum}-${params.paginationArgumentMaximum}.`;
|
|
42
|
+
exports.defaultNodeCostLimit = 500000;
|
|
43
|
+
exports.defaultPaginationArgumentMaximum = 100;
|
|
44
|
+
exports.defaultPaginationArgumentMinimum = 1;
|
|
45
|
+
/**
|
|
46
|
+
* Validate whether a user is allowed to execute a certain GraphQL operation.
|
|
47
|
+
*/
|
|
48
|
+
const ResourceLimitationValidationRule = (params) => (context, executionArgs) => {
|
|
49
|
+
const { paginationArgumentMaximum, paginationArgumentMinimum } = params;
|
|
50
|
+
const nodeCostStack = [];
|
|
51
|
+
let totalNodeCost = 0;
|
|
52
|
+
const connectionFieldMap = new WeakSet();
|
|
53
|
+
return {
|
|
54
|
+
Field: {
|
|
55
|
+
enter(fieldNode) {
|
|
56
|
+
const fieldDef = context.getFieldDef();
|
|
57
|
+
// if it is not found the query is invalid and graphql validation will complain
|
|
58
|
+
if (fieldDef != null) {
|
|
59
|
+
const argumentValues = (0, utils_1.getArgumentValues)(fieldDef, fieldNode, executionArgs.variableValues || undefined);
|
|
60
|
+
const type = getWrappedType(fieldDef.type);
|
|
61
|
+
if (type instanceof graphql_1.GraphQLObjectType && type.name.endsWith('Connection')) {
|
|
62
|
+
let nodeCost = 1;
|
|
63
|
+
connectionFieldMap.add(fieldNode);
|
|
64
|
+
const { hasFirst, hasLast } = hasFieldDefConnectionArgs(fieldDef, params.paginationArgumentTypes);
|
|
65
|
+
if (hasFirst === false && hasLast === false) {
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
67
|
+
console.warn('Encountered paginated field without pagination arguments.');
|
|
68
|
+
}
|
|
69
|
+
else if (hasFirst === true || hasLast === true) {
|
|
70
|
+
if (('first' in argumentValues === false && 'last' in argumentValues === false) ||
|
|
71
|
+
(argumentValues.first === null && argumentValues.last === null)) {
|
|
72
|
+
context.reportError(new graphql_1.GraphQLError(buildMissingPaginationFieldErrorMessage({
|
|
73
|
+
fieldName: fieldDef.name,
|
|
74
|
+
hasFirst,
|
|
75
|
+
hasLast,
|
|
76
|
+
}), fieldNode));
|
|
77
|
+
}
|
|
78
|
+
else if ('first' in argumentValues && !argumentValues.last) {
|
|
79
|
+
if (argumentValues.first < paginationArgumentMinimum ||
|
|
80
|
+
argumentValues.first > paginationArgumentMaximum) {
|
|
81
|
+
context.reportError(new graphql_1.GraphQLError(buildInvalidPaginationRangeErrorMessage({
|
|
82
|
+
paginationArgumentMaximum,
|
|
83
|
+
paginationArgumentMinimum,
|
|
84
|
+
argumentName: 'first',
|
|
85
|
+
fieldName: fieldDef.name,
|
|
86
|
+
}), fieldNode));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// eslint-disable-next-line dot-notation
|
|
90
|
+
nodeCost = argumentValues['first'];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (!argumentValues.first && 'last' in argumentValues) {
|
|
94
|
+
if (argumentValues.last < paginationArgumentMinimum ||
|
|
95
|
+
argumentValues.last > paginationArgumentMaximum) {
|
|
96
|
+
context.reportError(new graphql_1.GraphQLError(buildInvalidPaginationRangeErrorMessage({
|
|
97
|
+
paginationArgumentMaximum,
|
|
98
|
+
paginationArgumentMinimum,
|
|
99
|
+
argumentName: 'last',
|
|
100
|
+
fieldName: fieldDef.name,
|
|
101
|
+
}), fieldNode));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// eslint-disable-next-line dot-notation
|
|
105
|
+
nodeCost = argumentValues['last'];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
context.reportError(new graphql_1.GraphQLError(buildMissingPaginationFieldErrorMessage({
|
|
110
|
+
fieldName: fieldDef.name,
|
|
111
|
+
hasFirst,
|
|
112
|
+
hasLast,
|
|
113
|
+
}), fieldNode));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
nodeCostStack.push(nodeCost);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
leave(node) {
|
|
121
|
+
if (connectionFieldMap.delete(node)) {
|
|
122
|
+
totalNodeCost = totalNodeCost + nodeCostStack.reduce((a, b) => a * b, 1);
|
|
123
|
+
nodeCostStack.pop();
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
Document: {
|
|
128
|
+
leave(documentNode) {
|
|
129
|
+
if (totalNodeCost === 0) {
|
|
130
|
+
totalNodeCost = 1;
|
|
131
|
+
}
|
|
132
|
+
if (totalNodeCost > params.nodeCostLimit) {
|
|
133
|
+
context.reportError(new graphql_1.GraphQLError(`Cannot request more than ${params.nodeCostLimit} nodes in a single document. Please split your operation into multiple sub operations or reduce the amount of requested nodes.`, documentNode));
|
|
134
|
+
}
|
|
135
|
+
params.reportNodeCost?.(totalNodeCost, executionArgs);
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
exports.ResourceLimitationValidationRule = ResourceLimitationValidationRule;
|
|
141
|
+
const useResourceLimitations = (params) => {
|
|
142
|
+
const paginationArgumentMaximum = params?.paginationArgumentMaximum ?? exports.defaultPaginationArgumentMaximum;
|
|
143
|
+
const paginationArgumentMinimum = params?.paginationArgumentMinimum ?? exports.defaultPaginationArgumentMinimum;
|
|
144
|
+
const nodeCostLimit = params?.nodeCostLimit ?? exports.defaultNodeCostLimit;
|
|
145
|
+
const extensions = params?.extensions ?? false;
|
|
146
|
+
const nodeCostMap = new WeakMap();
|
|
147
|
+
const handleResult = ({ result, args }) => {
|
|
148
|
+
const nodeCost = nodeCostMap.get(args);
|
|
149
|
+
if (nodeCost != null) {
|
|
150
|
+
result.extensions = {
|
|
151
|
+
...result.extensions,
|
|
152
|
+
resourceLimitations: {
|
|
153
|
+
nodeCost,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
onPluginInit({ addPlugin }) {
|
|
160
|
+
addPlugin((0, extended_validation_1.useExtendedValidation)({
|
|
161
|
+
rules: [
|
|
162
|
+
(0, exports.ResourceLimitationValidationRule)({
|
|
163
|
+
nodeCostLimit,
|
|
164
|
+
paginationArgumentMaximum,
|
|
165
|
+
paginationArgumentMinimum,
|
|
166
|
+
paginationArgumentTypes: params?.paginationArgumentScalars,
|
|
167
|
+
reportNodeCost: extensions
|
|
168
|
+
? (nodeCost, ref) => {
|
|
169
|
+
nodeCostMap.set(ref, nodeCost);
|
|
170
|
+
}
|
|
171
|
+
: undefined,
|
|
172
|
+
}),
|
|
173
|
+
],
|
|
174
|
+
onValidationFailed: params => handleResult(params),
|
|
175
|
+
}));
|
|
176
|
+
},
|
|
177
|
+
onExecute({ args }) {
|
|
178
|
+
return {
|
|
179
|
+
onExecuteDone(payload) {
|
|
180
|
+
return (0, core_1.handleStreamOrSingleExecutionResult)(payload, ({ result }) => handleResult({ result, args }));
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
exports.useResourceLimitations = useResourceLimitations;
|
package/cjs/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { GraphQLError, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, isScalarType, } from 'graphql';
|
|
2
|
+
import { handleStreamOrSingleExecutionResult } from '@envelop/core';
|
|
3
|
+
import { useExtendedValidation } from '@envelop/extended-validation';
|
|
4
|
+
import { getArgumentValues } from '@graphql-tools/utils';
|
|
5
|
+
const getWrappedType = (graphqlType) => {
|
|
6
|
+
if (graphqlType instanceof GraphQLList || graphqlType instanceof GraphQLNonNull) {
|
|
7
|
+
return getWrappedType(graphqlType.ofType);
|
|
8
|
+
}
|
|
9
|
+
return graphqlType;
|
|
10
|
+
};
|
|
11
|
+
const isValidArgType = (type, paginationArgumentTypes) => type === GraphQLInt ||
|
|
12
|
+
(isScalarType(type) && !!paginationArgumentTypes && paginationArgumentTypes.includes(type.name));
|
|
13
|
+
const hasFieldDefConnectionArgs = (field, argumentTypes) => {
|
|
14
|
+
let hasFirst = false;
|
|
15
|
+
let hasLast = false;
|
|
16
|
+
for (const arg of field.args) {
|
|
17
|
+
if (arg.name === 'first' && isValidArgType(arg.type, argumentTypes)) {
|
|
18
|
+
hasFirst = true;
|
|
19
|
+
}
|
|
20
|
+
else if (arg.name === 'last' && isValidArgType(arg.type, argumentTypes)) {
|
|
21
|
+
hasLast = true;
|
|
22
|
+
}
|
|
23
|
+
else if (hasLast && hasFirst) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { hasFirst, hasLast };
|
|
28
|
+
};
|
|
29
|
+
const buildMissingPaginationFieldErrorMessage = (params) => `Missing pagination argument for field '${params.fieldName}'. ` +
|
|
30
|
+
`Please provide ` +
|
|
31
|
+
(params.hasFirst && params.hasLast
|
|
32
|
+
? "either the 'first' or 'last'"
|
|
33
|
+
: params.hasFirst
|
|
34
|
+
? "the 'first'"
|
|
35
|
+
: "the 'last'") +
|
|
36
|
+
' field argument.';
|
|
37
|
+
const buildInvalidPaginationRangeErrorMessage = (params) => `Invalid pagination argument for field '${params.fieldName}'. ` +
|
|
38
|
+
`The value for the '${params.argumentName}' argument must be an integer within ${params.paginationArgumentMinimum}-${params.paginationArgumentMaximum}.`;
|
|
39
|
+
export const defaultNodeCostLimit = 500000;
|
|
40
|
+
export const defaultPaginationArgumentMaximum = 100;
|
|
41
|
+
export const defaultPaginationArgumentMinimum = 1;
|
|
42
|
+
/**
|
|
43
|
+
* Validate whether a user is allowed to execute a certain GraphQL operation.
|
|
44
|
+
*/
|
|
45
|
+
export const ResourceLimitationValidationRule = (params) => (context, executionArgs) => {
|
|
46
|
+
const { paginationArgumentMaximum, paginationArgumentMinimum } = params;
|
|
47
|
+
const nodeCostStack = [];
|
|
48
|
+
let totalNodeCost = 0;
|
|
49
|
+
const connectionFieldMap = new WeakSet();
|
|
50
|
+
return {
|
|
51
|
+
Field: {
|
|
52
|
+
enter(fieldNode) {
|
|
53
|
+
const fieldDef = context.getFieldDef();
|
|
54
|
+
// if it is not found the query is invalid and graphql validation will complain
|
|
55
|
+
if (fieldDef != null) {
|
|
56
|
+
const argumentValues = getArgumentValues(fieldDef, fieldNode, executionArgs.variableValues || undefined);
|
|
57
|
+
const type = getWrappedType(fieldDef.type);
|
|
58
|
+
if (type instanceof GraphQLObjectType && type.name.endsWith('Connection')) {
|
|
59
|
+
let nodeCost = 1;
|
|
60
|
+
connectionFieldMap.add(fieldNode);
|
|
61
|
+
const { hasFirst, hasLast } = hasFieldDefConnectionArgs(fieldDef, params.paginationArgumentTypes);
|
|
62
|
+
if (hasFirst === false && hasLast === false) {
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.warn('Encountered paginated field without pagination arguments.');
|
|
65
|
+
}
|
|
66
|
+
else if (hasFirst === true || hasLast === true) {
|
|
67
|
+
if (('first' in argumentValues === false && 'last' in argumentValues === false) ||
|
|
68
|
+
(argumentValues.first === null && argumentValues.last === null)) {
|
|
69
|
+
context.reportError(new GraphQLError(buildMissingPaginationFieldErrorMessage({
|
|
70
|
+
fieldName: fieldDef.name,
|
|
71
|
+
hasFirst,
|
|
72
|
+
hasLast,
|
|
73
|
+
}), fieldNode));
|
|
74
|
+
}
|
|
75
|
+
else if ('first' in argumentValues && !argumentValues.last) {
|
|
76
|
+
if (argumentValues.first < paginationArgumentMinimum ||
|
|
77
|
+
argumentValues.first > paginationArgumentMaximum) {
|
|
78
|
+
context.reportError(new GraphQLError(buildInvalidPaginationRangeErrorMessage({
|
|
79
|
+
paginationArgumentMaximum,
|
|
80
|
+
paginationArgumentMinimum,
|
|
81
|
+
argumentName: 'first',
|
|
82
|
+
fieldName: fieldDef.name,
|
|
83
|
+
}), fieldNode));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// eslint-disable-next-line dot-notation
|
|
87
|
+
nodeCost = argumentValues['first'];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (!argumentValues.first && 'last' in argumentValues) {
|
|
91
|
+
if (argumentValues.last < paginationArgumentMinimum ||
|
|
92
|
+
argumentValues.last > paginationArgumentMaximum) {
|
|
93
|
+
context.reportError(new GraphQLError(buildInvalidPaginationRangeErrorMessage({
|
|
94
|
+
paginationArgumentMaximum,
|
|
95
|
+
paginationArgumentMinimum,
|
|
96
|
+
argumentName: 'last',
|
|
97
|
+
fieldName: fieldDef.name,
|
|
98
|
+
}), fieldNode));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// eslint-disable-next-line dot-notation
|
|
102
|
+
nodeCost = argumentValues['last'];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
context.reportError(new GraphQLError(buildMissingPaginationFieldErrorMessage({
|
|
107
|
+
fieldName: fieldDef.name,
|
|
108
|
+
hasFirst,
|
|
109
|
+
hasLast,
|
|
110
|
+
}), fieldNode));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
nodeCostStack.push(nodeCost);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
leave(node) {
|
|
118
|
+
if (connectionFieldMap.delete(node)) {
|
|
119
|
+
totalNodeCost = totalNodeCost + nodeCostStack.reduce((a, b) => a * b, 1);
|
|
120
|
+
nodeCostStack.pop();
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
Document: {
|
|
125
|
+
leave(documentNode) {
|
|
126
|
+
if (totalNodeCost === 0) {
|
|
127
|
+
totalNodeCost = 1;
|
|
128
|
+
}
|
|
129
|
+
if (totalNodeCost > params.nodeCostLimit) {
|
|
130
|
+
context.reportError(new GraphQLError(`Cannot request more than ${params.nodeCostLimit} nodes in a single document. Please split your operation into multiple sub operations or reduce the amount of requested nodes.`, documentNode));
|
|
131
|
+
}
|
|
132
|
+
params.reportNodeCost?.(totalNodeCost, executionArgs);
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
export const useResourceLimitations = (params) => {
|
|
138
|
+
const paginationArgumentMaximum = params?.paginationArgumentMaximum ?? defaultPaginationArgumentMaximum;
|
|
139
|
+
const paginationArgumentMinimum = params?.paginationArgumentMinimum ?? defaultPaginationArgumentMinimum;
|
|
140
|
+
const nodeCostLimit = params?.nodeCostLimit ?? defaultNodeCostLimit;
|
|
141
|
+
const extensions = params?.extensions ?? false;
|
|
142
|
+
const nodeCostMap = new WeakMap();
|
|
143
|
+
const handleResult = ({ result, args }) => {
|
|
144
|
+
const nodeCost = nodeCostMap.get(args);
|
|
145
|
+
if (nodeCost != null) {
|
|
146
|
+
result.extensions = {
|
|
147
|
+
...result.extensions,
|
|
148
|
+
resourceLimitations: {
|
|
149
|
+
nodeCost,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
onPluginInit({ addPlugin }) {
|
|
156
|
+
addPlugin(useExtendedValidation({
|
|
157
|
+
rules: [
|
|
158
|
+
ResourceLimitationValidationRule({
|
|
159
|
+
nodeCostLimit,
|
|
160
|
+
paginationArgumentMaximum,
|
|
161
|
+
paginationArgumentMinimum,
|
|
162
|
+
paginationArgumentTypes: params?.paginationArgumentScalars,
|
|
163
|
+
reportNodeCost: extensions
|
|
164
|
+
? (nodeCost, ref) => {
|
|
165
|
+
nodeCostMap.set(ref, nodeCost);
|
|
166
|
+
}
|
|
167
|
+
: undefined,
|
|
168
|
+
}),
|
|
169
|
+
],
|
|
170
|
+
onValidationFailed: params => handleResult(params),
|
|
171
|
+
}));
|
|
172
|
+
},
|
|
173
|
+
onExecute({ args }) {
|
|
174
|
+
return {
|
|
175
|
+
onExecuteDone(payload) {
|
|
176
|
+
return handleStreamOrSingleExecutionResult(payload, ({ result }) => handleResult({ result, args }));
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envelop/resource-limitations",
|
|
3
|
-
"version": "8.0
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "8.1.0-alpha-20260120163327-6e77ec22fc62a09ba152ed764585743e1557eaf8",
|
|
5
4
|
"description": "A rate-limit implementation based on resource limitations and static calculation of the score (similar to GitHub GraphQL API)",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
8
|
+
"@envelop/core": "^5.5.0-alpha-20260120163327-6e77ec22fc62a09ba152ed764585743e1557eaf8"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@graphql-tools/utils": "^11.0.0",
|
|
12
|
+
"tslib": "^2.5.0",
|
|
13
|
+
"@envelop/extended-validation": "^7.1.0-alpha-20260120163327-6e77ec22fc62a09ba152ed764585743e1557eaf8"
|
|
14
|
+
},
|
|
6
15
|
"repository": {
|
|
7
16
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/graphql-hive/
|
|
17
|
+
"url": "https://github.com/graphql-hive/envelop.git",
|
|
9
18
|
"directory": "packages/plugins/resource-limitations"
|
|
10
19
|
},
|
|
11
20
|
"author": "Laurin Quast <laurinquast@googlemail.com>",
|
|
@@ -13,71 +22,42 @@
|
|
|
13
22
|
"engines": {
|
|
14
23
|
"node": ">=18.0.0"
|
|
15
24
|
},
|
|
16
|
-
"main": "
|
|
17
|
-
"
|
|
18
|
-
"
|
|
25
|
+
"main": "cjs/index.js",
|
|
26
|
+
"module": "esm/index.js",
|
|
27
|
+
"typings": "typings/index.d.ts",
|
|
28
|
+
"typescript": {
|
|
29
|
+
"definition": "typings/index.d.ts"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
19
32
|
"exports": {
|
|
20
33
|
".": {
|
|
21
34
|
"require": {
|
|
22
|
-
"types": "./
|
|
23
|
-
"default": "./
|
|
35
|
+
"types": "./typings/index.d.cts",
|
|
36
|
+
"default": "./cjs/index.js"
|
|
24
37
|
},
|
|
25
38
|
"import": {
|
|
26
|
-
"types": "./
|
|
27
|
-
"default": "./
|
|
39
|
+
"types": "./typings/index.d.ts",
|
|
40
|
+
"default": "./esm/index.js"
|
|
28
41
|
},
|
|
29
42
|
"default": {
|
|
30
|
-
"types": "./
|
|
31
|
-
"default": "./
|
|
43
|
+
"types": "./typings/index.d.ts",
|
|
44
|
+
"default": "./esm/index.js"
|
|
32
45
|
}
|
|
33
46
|
},
|
|
34
47
|
"./*": {
|
|
35
48
|
"require": {
|
|
36
|
-
"types": "./
|
|
37
|
-
"default": "./
|
|
49
|
+
"types": "./typings/*.d.cts",
|
|
50
|
+
"default": "./cjs/*.js"
|
|
38
51
|
},
|
|
39
52
|
"import": {
|
|
40
|
-
"types": "./
|
|
41
|
-
"default": "./
|
|
53
|
+
"types": "./typings/*.d.ts",
|
|
54
|
+
"default": "./esm/*.js"
|
|
42
55
|
},
|
|
43
56
|
"default": {
|
|
44
|
-
"types": "./
|
|
45
|
-
"default": "./
|
|
57
|
+
"types": "./typings/*.d.ts",
|
|
58
|
+
"default": "./esm/*.js"
|
|
46
59
|
}
|
|
47
60
|
},
|
|
48
61
|
"./package.json": "./package.json"
|
|
49
|
-
},
|
|
50
|
-
"files": [
|
|
51
|
-
"dist"
|
|
52
|
-
],
|
|
53
|
-
"scripts": {
|
|
54
|
-
"build": "tsdown",
|
|
55
|
-
"check": "tsc --pretty --noEmit"
|
|
56
|
-
},
|
|
57
|
-
"peerDependencies": {
|
|
58
|
-
"@envelop/core": "workspace:^",
|
|
59
|
-
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
|
60
|
-
},
|
|
61
|
-
"dependencies": {
|
|
62
|
-
"@envelop/extended-validation": "workspace:^",
|
|
63
|
-
"@graphql-tools/utils": "^10.0.0",
|
|
64
|
-
"tslib": "^2.5.0"
|
|
65
|
-
},
|
|
66
|
-
"devDependencies": {
|
|
67
|
-
"@envelop/core": "workspace:^",
|
|
68
|
-
"@graphql-tools/schema": "10.0.30",
|
|
69
|
-
"graphql": "16.12.0",
|
|
70
|
-
"tsdown": "^0.20.0-beta.1",
|
|
71
|
-
"typescript": "5.9.3"
|
|
72
|
-
},
|
|
73
|
-
"publishConfig": {
|
|
74
|
-
"access": "public"
|
|
75
|
-
},
|
|
76
|
-
"sideEffects": false,
|
|
77
|
-
"buildOptions": {
|
|
78
|
-
"input": "./src/index.ts"
|
|
79
|
-
},
|
|
80
|
-
"typescript": {
|
|
81
|
-
"definition": "dist/index.d.mts"
|
|
82
62
|
}
|
|
83
|
-
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ExecutionArgs } from 'graphql';
|
|
2
|
+
import { Plugin } from '@envelop/core';
|
|
3
|
+
import { ExtendedValidationRule } from '@envelop/extended-validation';
|
|
4
|
+
export declare const defaultNodeCostLimit = 500000;
|
|
5
|
+
export declare const defaultPaginationArgumentMaximum = 100;
|
|
6
|
+
export declare const defaultPaginationArgumentMinimum = 1;
|
|
7
|
+
export type ResourceLimitationValidationRuleParams = {
|
|
8
|
+
nodeCostLimit: number;
|
|
9
|
+
paginationArgumentMaximum: number;
|
|
10
|
+
paginationArgumentMinimum: number;
|
|
11
|
+
paginationArgumentTypes?: string[];
|
|
12
|
+
reportNodeCost?: (cost: number, executionArgs: ExecutionArgs) => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Validate whether a user is allowed to execute a certain GraphQL operation.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ResourceLimitationValidationRule: (params: ResourceLimitationValidationRuleParams) => ExtendedValidationRule;
|
|
18
|
+
type UseResourceLimitationsParams = {
|
|
19
|
+
/**
|
|
20
|
+
* The node cost limit for rejecting a operation.
|
|
21
|
+
* @default 500000
|
|
22
|
+
*/
|
|
23
|
+
nodeCostLimit?: number;
|
|
24
|
+
/**
|
|
25
|
+
* The custom scalar types accepted for connection arguments.
|
|
26
|
+
*/
|
|
27
|
+
paginationArgumentScalars?: string[];
|
|
28
|
+
/**
|
|
29
|
+
* The maximum value accepted for connection arguments.
|
|
30
|
+
* @default 100
|
|
31
|
+
*/
|
|
32
|
+
paginationArgumentMaximum?: number;
|
|
33
|
+
/**
|
|
34
|
+
* The minimum value accepted for connection arguments.
|
|
35
|
+
* @default 1
|
|
36
|
+
*/
|
|
37
|
+
paginationArgumentMinimum?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the resourceLimitations.nodeCost field should be included within the execution result extensions map.
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
extensions?: boolean;
|
|
43
|
+
};
|
|
44
|
+
export declare const useResourceLimitations: (params?: UseResourceLimitationsParams) => Plugin;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ExecutionArgs } from 'graphql';
|
|
2
|
+
import { Plugin } from '@envelop/core';
|
|
3
|
+
import { ExtendedValidationRule } from '@envelop/extended-validation';
|
|
4
|
+
export declare const defaultNodeCostLimit = 500000;
|
|
5
|
+
export declare const defaultPaginationArgumentMaximum = 100;
|
|
6
|
+
export declare const defaultPaginationArgumentMinimum = 1;
|
|
7
|
+
export type ResourceLimitationValidationRuleParams = {
|
|
8
|
+
nodeCostLimit: number;
|
|
9
|
+
paginationArgumentMaximum: number;
|
|
10
|
+
paginationArgumentMinimum: number;
|
|
11
|
+
paginationArgumentTypes?: string[];
|
|
12
|
+
reportNodeCost?: (cost: number, executionArgs: ExecutionArgs) => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Validate whether a user is allowed to execute a certain GraphQL operation.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ResourceLimitationValidationRule: (params: ResourceLimitationValidationRuleParams) => ExtendedValidationRule;
|
|
18
|
+
type UseResourceLimitationsParams = {
|
|
19
|
+
/**
|
|
20
|
+
* The node cost limit for rejecting a operation.
|
|
21
|
+
* @default 500000
|
|
22
|
+
*/
|
|
23
|
+
nodeCostLimit?: number;
|
|
24
|
+
/**
|
|
25
|
+
* The custom scalar types accepted for connection arguments.
|
|
26
|
+
*/
|
|
27
|
+
paginationArgumentScalars?: string[];
|
|
28
|
+
/**
|
|
29
|
+
* The maximum value accepted for connection arguments.
|
|
30
|
+
* @default 100
|
|
31
|
+
*/
|
|
32
|
+
paginationArgumentMaximum?: number;
|
|
33
|
+
/**
|
|
34
|
+
* The minimum value accepted for connection arguments.
|
|
35
|
+
* @default 1
|
|
36
|
+
*/
|
|
37
|
+
paginationArgumentMinimum?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the resourceLimitations.nodeCost field should be included within the execution result extensions map.
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
extensions?: boolean;
|
|
43
|
+
};
|
|
44
|
+
export declare const useResourceLimitations: (params?: UseResourceLimitationsParams) => Plugin;
|
|
45
|
+
export {};
|
package/dist/index.cjs
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
let graphql = require("graphql");
|
|
2
|
-
let _envelop_core = require("@envelop/core");
|
|
3
|
-
let _envelop_extended_validation = require("@envelop/extended-validation");
|
|
4
|
-
let _graphql_tools_utils = require("@graphql-tools/utils");
|
|
5
|
-
|
|
6
|
-
//#region src/index.ts
|
|
7
|
-
const getWrappedType = (graphqlType) => {
|
|
8
|
-
if (graphqlType instanceof graphql.GraphQLList || graphqlType instanceof graphql.GraphQLNonNull) return getWrappedType(graphqlType.ofType);
|
|
9
|
-
return graphqlType;
|
|
10
|
-
};
|
|
11
|
-
const isValidArgType = (type, paginationArgumentTypes) => type === graphql.GraphQLInt || (0, graphql.isScalarType)(type) && !!paginationArgumentTypes && paginationArgumentTypes.includes(type.name);
|
|
12
|
-
const hasFieldDefConnectionArgs = (field, argumentTypes) => {
|
|
13
|
-
let hasFirst = false;
|
|
14
|
-
let hasLast = false;
|
|
15
|
-
for (const arg of field.args) if (arg.name === "first" && isValidArgType(arg.type, argumentTypes)) hasFirst = true;
|
|
16
|
-
else if (arg.name === "last" && isValidArgType(arg.type, argumentTypes)) hasLast = true;
|
|
17
|
-
else if (hasLast && hasFirst) break;
|
|
18
|
-
return {
|
|
19
|
-
hasFirst,
|
|
20
|
-
hasLast
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
const buildMissingPaginationFieldErrorMessage = (params) => `Missing pagination argument for field '${params.fieldName}'. Please provide ` + (params.hasFirst && params.hasLast ? "either the 'first' or 'last'" : params.hasFirst ? "the 'first'" : "the 'last'") + " field argument.";
|
|
24
|
-
const buildInvalidPaginationRangeErrorMessage = (params) => `Invalid pagination argument for field '${params.fieldName}'. The value for the '${params.argumentName}' argument must be an integer within ${params.paginationArgumentMinimum}-${params.paginationArgumentMaximum}.`;
|
|
25
|
-
const defaultNodeCostLimit = 5e5;
|
|
26
|
-
const defaultPaginationArgumentMaximum = 100;
|
|
27
|
-
const defaultPaginationArgumentMinimum = 1;
|
|
28
|
-
/**
|
|
29
|
-
* Validate whether a user is allowed to execute a certain GraphQL operation.
|
|
30
|
-
*/
|
|
31
|
-
const ResourceLimitationValidationRule = (params) => (context, executionArgs) => {
|
|
32
|
-
const { paginationArgumentMaximum, paginationArgumentMinimum } = params;
|
|
33
|
-
const nodeCostStack = [];
|
|
34
|
-
let totalNodeCost = 0;
|
|
35
|
-
const connectionFieldMap = /* @__PURE__ */ new WeakSet();
|
|
36
|
-
return {
|
|
37
|
-
Field: {
|
|
38
|
-
enter(fieldNode) {
|
|
39
|
-
const fieldDef = context.getFieldDef();
|
|
40
|
-
if (fieldDef != null) {
|
|
41
|
-
const argumentValues = (0, _graphql_tools_utils.getArgumentValues)(fieldDef, fieldNode, executionArgs.variableValues || void 0);
|
|
42
|
-
const type = getWrappedType(fieldDef.type);
|
|
43
|
-
if (type instanceof graphql.GraphQLObjectType && type.name.endsWith("Connection")) {
|
|
44
|
-
let nodeCost = 1;
|
|
45
|
-
connectionFieldMap.add(fieldNode);
|
|
46
|
-
const { hasFirst, hasLast } = hasFieldDefConnectionArgs(fieldDef, params.paginationArgumentTypes);
|
|
47
|
-
if (hasFirst === false && hasLast === false) console.warn("Encountered paginated field without pagination arguments.");
|
|
48
|
-
else if (hasFirst === true || hasLast === true) if ("first" in argumentValues === false && "last" in argumentValues === false || argumentValues["first"] === null && argumentValues["last"] === null) context.reportError(new graphql.GraphQLError(buildMissingPaginationFieldErrorMessage({
|
|
49
|
-
fieldName: fieldDef.name,
|
|
50
|
-
hasFirst,
|
|
51
|
-
hasLast
|
|
52
|
-
}), fieldNode));
|
|
53
|
-
else if ("first" in argumentValues && !argumentValues["last"]) if (argumentValues["first"] < paginationArgumentMinimum || argumentValues["first"] > paginationArgumentMaximum) context.reportError(new graphql.GraphQLError(buildInvalidPaginationRangeErrorMessage({
|
|
54
|
-
paginationArgumentMaximum,
|
|
55
|
-
paginationArgumentMinimum,
|
|
56
|
-
argumentName: "first",
|
|
57
|
-
fieldName: fieldDef.name
|
|
58
|
-
}), fieldNode));
|
|
59
|
-
else nodeCost = argumentValues["first"];
|
|
60
|
-
else if (!argumentValues["first"] && "last" in argumentValues) if (argumentValues["last"] < paginationArgumentMinimum || argumentValues["last"] > paginationArgumentMaximum) context.reportError(new graphql.GraphQLError(buildInvalidPaginationRangeErrorMessage({
|
|
61
|
-
paginationArgumentMaximum,
|
|
62
|
-
paginationArgumentMinimum,
|
|
63
|
-
argumentName: "last",
|
|
64
|
-
fieldName: fieldDef.name
|
|
65
|
-
}), fieldNode));
|
|
66
|
-
else nodeCost = argumentValues["last"];
|
|
67
|
-
else context.reportError(new graphql.GraphQLError(buildMissingPaginationFieldErrorMessage({
|
|
68
|
-
fieldName: fieldDef.name,
|
|
69
|
-
hasFirst,
|
|
70
|
-
hasLast
|
|
71
|
-
}), fieldNode));
|
|
72
|
-
nodeCostStack.push(nodeCost);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
leave(node) {
|
|
77
|
-
if (connectionFieldMap.delete(node)) {
|
|
78
|
-
totalNodeCost = totalNodeCost + nodeCostStack.reduce((a, b) => a * b, 1);
|
|
79
|
-
nodeCostStack.pop();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
Document: { leave(documentNode) {
|
|
84
|
-
if (totalNodeCost === 0) totalNodeCost = 1;
|
|
85
|
-
if (totalNodeCost > params.nodeCostLimit) context.reportError(new graphql.GraphQLError(`Cannot request more than ${params.nodeCostLimit} nodes in a single document. Please split your operation into multiple sub operations or reduce the amount of requested nodes.`, documentNode));
|
|
86
|
-
params.reportNodeCost?.(totalNodeCost, executionArgs);
|
|
87
|
-
} }
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
const useResourceLimitations = (params) => {
|
|
91
|
-
const paginationArgumentMaximum = params?.paginationArgumentMaximum ?? defaultPaginationArgumentMaximum;
|
|
92
|
-
const paginationArgumentMinimum = params?.paginationArgumentMinimum ?? defaultPaginationArgumentMinimum;
|
|
93
|
-
const nodeCostLimit = params?.nodeCostLimit ?? defaultNodeCostLimit;
|
|
94
|
-
const extensions = params?.extensions ?? false;
|
|
95
|
-
const nodeCostMap = /* @__PURE__ */ new WeakMap();
|
|
96
|
-
const handleResult = ({ result, args }) => {
|
|
97
|
-
const nodeCost = nodeCostMap.get(args);
|
|
98
|
-
if (nodeCost != null) result.extensions = {
|
|
99
|
-
...result.extensions,
|
|
100
|
-
resourceLimitations: { nodeCost }
|
|
101
|
-
};
|
|
102
|
-
};
|
|
103
|
-
return {
|
|
104
|
-
onPluginInit({ addPlugin }) {
|
|
105
|
-
addPlugin((0, _envelop_extended_validation.useExtendedValidation)({
|
|
106
|
-
rules: [ResourceLimitationValidationRule({
|
|
107
|
-
nodeCostLimit,
|
|
108
|
-
paginationArgumentMaximum,
|
|
109
|
-
paginationArgumentMinimum,
|
|
110
|
-
paginationArgumentTypes: params?.paginationArgumentScalars,
|
|
111
|
-
reportNodeCost: extensions ? (nodeCost, ref) => {
|
|
112
|
-
nodeCostMap.set(ref, nodeCost);
|
|
113
|
-
} : void 0
|
|
114
|
-
})],
|
|
115
|
-
onValidationFailed: (params$1) => handleResult(params$1)
|
|
116
|
-
}));
|
|
117
|
-
},
|
|
118
|
-
onExecute({ args }) {
|
|
119
|
-
return { onExecuteDone(payload) {
|
|
120
|
-
return (0, _envelop_core.handleStreamOrSingleExecutionResult)(payload, ({ result }) => handleResult({
|
|
121
|
-
result,
|
|
122
|
-
args
|
|
123
|
-
}));
|
|
124
|
-
} };
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
//#endregion
|
|
130
|
-
exports.ResourceLimitationValidationRule = ResourceLimitationValidationRule;
|
|
131
|
-
exports.defaultNodeCostLimit = defaultNodeCostLimit;
|
|
132
|
-
exports.defaultPaginationArgumentMaximum = defaultPaginationArgumentMaximum;
|
|
133
|
-
exports.defaultPaginationArgumentMinimum = defaultPaginationArgumentMinimum;
|
|
134
|
-
exports.useResourceLimitations = useResourceLimitations;
|
package/dist/index.d.cts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { ExecutionArgs } from "graphql";
|
|
2
|
-
import { Plugin } from "@envelop/core";
|
|
3
|
-
import { ExtendedValidationRule } from "@envelop/extended-validation";
|
|
4
|
-
|
|
5
|
-
//#region src/index.d.ts
|
|
6
|
-
declare const defaultNodeCostLimit = 500000;
|
|
7
|
-
declare const defaultPaginationArgumentMaximum = 100;
|
|
8
|
-
declare const defaultPaginationArgumentMinimum = 1;
|
|
9
|
-
type ResourceLimitationValidationRuleParams = {
|
|
10
|
-
nodeCostLimit: number;
|
|
11
|
-
paginationArgumentMaximum: number;
|
|
12
|
-
paginationArgumentMinimum: number;
|
|
13
|
-
paginationArgumentTypes?: string[];
|
|
14
|
-
reportNodeCost?: (cost: number, executionArgs: ExecutionArgs) => void;
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Validate whether a user is allowed to execute a certain GraphQL operation.
|
|
18
|
-
*/
|
|
19
|
-
declare const ResourceLimitationValidationRule: (params: ResourceLimitationValidationRuleParams) => ExtendedValidationRule;
|
|
20
|
-
type UseResourceLimitationsParams = {
|
|
21
|
-
/**
|
|
22
|
-
* The node cost limit for rejecting a operation.
|
|
23
|
-
* @default 500000
|
|
24
|
-
*/
|
|
25
|
-
nodeCostLimit?: number;
|
|
26
|
-
/**
|
|
27
|
-
* The custom scalar types accepted for connection arguments.
|
|
28
|
-
*/
|
|
29
|
-
paginationArgumentScalars?: string[];
|
|
30
|
-
/**
|
|
31
|
-
* The maximum value accepted for connection arguments.
|
|
32
|
-
* @default 100
|
|
33
|
-
*/
|
|
34
|
-
paginationArgumentMaximum?: number;
|
|
35
|
-
/**
|
|
36
|
-
* The minimum value accepted for connection arguments.
|
|
37
|
-
* @default 1
|
|
38
|
-
*/
|
|
39
|
-
paginationArgumentMinimum?: number;
|
|
40
|
-
/**
|
|
41
|
-
* Whether the resourceLimitations.nodeCost field should be included within the execution result extensions map.
|
|
42
|
-
* @default false
|
|
43
|
-
*/
|
|
44
|
-
extensions?: boolean;
|
|
45
|
-
};
|
|
46
|
-
declare const useResourceLimitations: (params?: UseResourceLimitationsParams) => Plugin;
|
|
47
|
-
//#endregion
|
|
48
|
-
export { ResourceLimitationValidationRule, ResourceLimitationValidationRuleParams, defaultNodeCostLimit, defaultPaginationArgumentMaximum, defaultPaginationArgumentMinimum, useResourceLimitations };
|
package/dist/index.d.mts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { ExecutionArgs } from "graphql";
|
|
2
|
-
import { Plugin } from "@envelop/core";
|
|
3
|
-
import { ExtendedValidationRule } from "@envelop/extended-validation";
|
|
4
|
-
|
|
5
|
-
//#region src/index.d.ts
|
|
6
|
-
declare const defaultNodeCostLimit = 500000;
|
|
7
|
-
declare const defaultPaginationArgumentMaximum = 100;
|
|
8
|
-
declare const defaultPaginationArgumentMinimum = 1;
|
|
9
|
-
type ResourceLimitationValidationRuleParams = {
|
|
10
|
-
nodeCostLimit: number;
|
|
11
|
-
paginationArgumentMaximum: number;
|
|
12
|
-
paginationArgumentMinimum: number;
|
|
13
|
-
paginationArgumentTypes?: string[];
|
|
14
|
-
reportNodeCost?: (cost: number, executionArgs: ExecutionArgs) => void;
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Validate whether a user is allowed to execute a certain GraphQL operation.
|
|
18
|
-
*/
|
|
19
|
-
declare const ResourceLimitationValidationRule: (params: ResourceLimitationValidationRuleParams) => ExtendedValidationRule;
|
|
20
|
-
type UseResourceLimitationsParams = {
|
|
21
|
-
/**
|
|
22
|
-
* The node cost limit for rejecting a operation.
|
|
23
|
-
* @default 500000
|
|
24
|
-
*/
|
|
25
|
-
nodeCostLimit?: number;
|
|
26
|
-
/**
|
|
27
|
-
* The custom scalar types accepted for connection arguments.
|
|
28
|
-
*/
|
|
29
|
-
paginationArgumentScalars?: string[];
|
|
30
|
-
/**
|
|
31
|
-
* The maximum value accepted for connection arguments.
|
|
32
|
-
* @default 100
|
|
33
|
-
*/
|
|
34
|
-
paginationArgumentMaximum?: number;
|
|
35
|
-
/**
|
|
36
|
-
* The minimum value accepted for connection arguments.
|
|
37
|
-
* @default 1
|
|
38
|
-
*/
|
|
39
|
-
paginationArgumentMinimum?: number;
|
|
40
|
-
/**
|
|
41
|
-
* Whether the resourceLimitations.nodeCost field should be included within the execution result extensions map.
|
|
42
|
-
* @default false
|
|
43
|
-
*/
|
|
44
|
-
extensions?: boolean;
|
|
45
|
-
};
|
|
46
|
-
declare const useResourceLimitations: (params?: UseResourceLimitationsParams) => Plugin;
|
|
47
|
-
//#endregion
|
|
48
|
-
export { ResourceLimitationValidationRule, ResourceLimitationValidationRuleParams, defaultNodeCostLimit, defaultPaginationArgumentMaximum, defaultPaginationArgumentMinimum, useResourceLimitations };
|
package/dist/index.mjs
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { GraphQLError, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, isScalarType } from "graphql";
|
|
2
|
-
import { handleStreamOrSingleExecutionResult } from "@envelop/core";
|
|
3
|
-
import { useExtendedValidation } from "@envelop/extended-validation";
|
|
4
|
-
import { getArgumentValues } from "@graphql-tools/utils";
|
|
5
|
-
|
|
6
|
-
//#region src/index.ts
|
|
7
|
-
const getWrappedType = (graphqlType) => {
|
|
8
|
-
if (graphqlType instanceof GraphQLList || graphqlType instanceof GraphQLNonNull) return getWrappedType(graphqlType.ofType);
|
|
9
|
-
return graphqlType;
|
|
10
|
-
};
|
|
11
|
-
const isValidArgType = (type, paginationArgumentTypes) => type === GraphQLInt || isScalarType(type) && !!paginationArgumentTypes && paginationArgumentTypes.includes(type.name);
|
|
12
|
-
const hasFieldDefConnectionArgs = (field, argumentTypes) => {
|
|
13
|
-
let hasFirst = false;
|
|
14
|
-
let hasLast = false;
|
|
15
|
-
for (const arg of field.args) if (arg.name === "first" && isValidArgType(arg.type, argumentTypes)) hasFirst = true;
|
|
16
|
-
else if (arg.name === "last" && isValidArgType(arg.type, argumentTypes)) hasLast = true;
|
|
17
|
-
else if (hasLast && hasFirst) break;
|
|
18
|
-
return {
|
|
19
|
-
hasFirst,
|
|
20
|
-
hasLast
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
const buildMissingPaginationFieldErrorMessage = (params) => `Missing pagination argument for field '${params.fieldName}'. Please provide ` + (params.hasFirst && params.hasLast ? "either the 'first' or 'last'" : params.hasFirst ? "the 'first'" : "the 'last'") + " field argument.";
|
|
24
|
-
const buildInvalidPaginationRangeErrorMessage = (params) => `Invalid pagination argument for field '${params.fieldName}'. The value for the '${params.argumentName}' argument must be an integer within ${params.paginationArgumentMinimum}-${params.paginationArgumentMaximum}.`;
|
|
25
|
-
const defaultNodeCostLimit = 5e5;
|
|
26
|
-
const defaultPaginationArgumentMaximum = 100;
|
|
27
|
-
const defaultPaginationArgumentMinimum = 1;
|
|
28
|
-
/**
|
|
29
|
-
* Validate whether a user is allowed to execute a certain GraphQL operation.
|
|
30
|
-
*/
|
|
31
|
-
const ResourceLimitationValidationRule = (params) => (context, executionArgs) => {
|
|
32
|
-
const { paginationArgumentMaximum, paginationArgumentMinimum } = params;
|
|
33
|
-
const nodeCostStack = [];
|
|
34
|
-
let totalNodeCost = 0;
|
|
35
|
-
const connectionFieldMap = /* @__PURE__ */ new WeakSet();
|
|
36
|
-
return {
|
|
37
|
-
Field: {
|
|
38
|
-
enter(fieldNode) {
|
|
39
|
-
const fieldDef = context.getFieldDef();
|
|
40
|
-
if (fieldDef != null) {
|
|
41
|
-
const argumentValues = getArgumentValues(fieldDef, fieldNode, executionArgs.variableValues || void 0);
|
|
42
|
-
const type = getWrappedType(fieldDef.type);
|
|
43
|
-
if (type instanceof GraphQLObjectType && type.name.endsWith("Connection")) {
|
|
44
|
-
let nodeCost = 1;
|
|
45
|
-
connectionFieldMap.add(fieldNode);
|
|
46
|
-
const { hasFirst, hasLast } = hasFieldDefConnectionArgs(fieldDef, params.paginationArgumentTypes);
|
|
47
|
-
if (hasFirst === false && hasLast === false) console.warn("Encountered paginated field without pagination arguments.");
|
|
48
|
-
else if (hasFirst === true || hasLast === true) if ("first" in argumentValues === false && "last" in argumentValues === false || argumentValues["first"] === null && argumentValues["last"] === null) context.reportError(new GraphQLError(buildMissingPaginationFieldErrorMessage({
|
|
49
|
-
fieldName: fieldDef.name,
|
|
50
|
-
hasFirst,
|
|
51
|
-
hasLast
|
|
52
|
-
}), fieldNode));
|
|
53
|
-
else if ("first" in argumentValues && !argumentValues["last"]) if (argumentValues["first"] < paginationArgumentMinimum || argumentValues["first"] > paginationArgumentMaximum) context.reportError(new GraphQLError(buildInvalidPaginationRangeErrorMessage({
|
|
54
|
-
paginationArgumentMaximum,
|
|
55
|
-
paginationArgumentMinimum,
|
|
56
|
-
argumentName: "first",
|
|
57
|
-
fieldName: fieldDef.name
|
|
58
|
-
}), fieldNode));
|
|
59
|
-
else nodeCost = argumentValues["first"];
|
|
60
|
-
else if (!argumentValues["first"] && "last" in argumentValues) if (argumentValues["last"] < paginationArgumentMinimum || argumentValues["last"] > paginationArgumentMaximum) context.reportError(new GraphQLError(buildInvalidPaginationRangeErrorMessage({
|
|
61
|
-
paginationArgumentMaximum,
|
|
62
|
-
paginationArgumentMinimum,
|
|
63
|
-
argumentName: "last",
|
|
64
|
-
fieldName: fieldDef.name
|
|
65
|
-
}), fieldNode));
|
|
66
|
-
else nodeCost = argumentValues["last"];
|
|
67
|
-
else context.reportError(new GraphQLError(buildMissingPaginationFieldErrorMessage({
|
|
68
|
-
fieldName: fieldDef.name,
|
|
69
|
-
hasFirst,
|
|
70
|
-
hasLast
|
|
71
|
-
}), fieldNode));
|
|
72
|
-
nodeCostStack.push(nodeCost);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
leave(node) {
|
|
77
|
-
if (connectionFieldMap.delete(node)) {
|
|
78
|
-
totalNodeCost = totalNodeCost + nodeCostStack.reduce((a, b) => a * b, 1);
|
|
79
|
-
nodeCostStack.pop();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
Document: { leave(documentNode) {
|
|
84
|
-
if (totalNodeCost === 0) totalNodeCost = 1;
|
|
85
|
-
if (totalNodeCost > params.nodeCostLimit) context.reportError(new GraphQLError(`Cannot request more than ${params.nodeCostLimit} nodes in a single document. Please split your operation into multiple sub operations or reduce the amount of requested nodes.`, documentNode));
|
|
86
|
-
params.reportNodeCost?.(totalNodeCost, executionArgs);
|
|
87
|
-
} }
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
const useResourceLimitations = (params) => {
|
|
91
|
-
const paginationArgumentMaximum = params?.paginationArgumentMaximum ?? defaultPaginationArgumentMaximum;
|
|
92
|
-
const paginationArgumentMinimum = params?.paginationArgumentMinimum ?? defaultPaginationArgumentMinimum;
|
|
93
|
-
const nodeCostLimit = params?.nodeCostLimit ?? defaultNodeCostLimit;
|
|
94
|
-
const extensions = params?.extensions ?? false;
|
|
95
|
-
const nodeCostMap = /* @__PURE__ */ new WeakMap();
|
|
96
|
-
const handleResult = ({ result, args }) => {
|
|
97
|
-
const nodeCost = nodeCostMap.get(args);
|
|
98
|
-
if (nodeCost != null) result.extensions = {
|
|
99
|
-
...result.extensions,
|
|
100
|
-
resourceLimitations: { nodeCost }
|
|
101
|
-
};
|
|
102
|
-
};
|
|
103
|
-
return {
|
|
104
|
-
onPluginInit({ addPlugin }) {
|
|
105
|
-
addPlugin(useExtendedValidation({
|
|
106
|
-
rules: [ResourceLimitationValidationRule({
|
|
107
|
-
nodeCostLimit,
|
|
108
|
-
paginationArgumentMaximum,
|
|
109
|
-
paginationArgumentMinimum,
|
|
110
|
-
paginationArgumentTypes: params?.paginationArgumentScalars,
|
|
111
|
-
reportNodeCost: extensions ? (nodeCost, ref) => {
|
|
112
|
-
nodeCostMap.set(ref, nodeCost);
|
|
113
|
-
} : void 0
|
|
114
|
-
})],
|
|
115
|
-
onValidationFailed: (params$1) => handleResult(params$1)
|
|
116
|
-
}));
|
|
117
|
-
},
|
|
118
|
-
onExecute({ args }) {
|
|
119
|
-
return { onExecuteDone(payload) {
|
|
120
|
-
return handleStreamOrSingleExecutionResult(payload, ({ result }) => handleResult({
|
|
121
|
-
result,
|
|
122
|
-
args
|
|
123
|
-
}));
|
|
124
|
-
} };
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
//#endregion
|
|
130
|
-
export { ResourceLimitationValidationRule, defaultNodeCostLimit, defaultPaginationArgumentMaximum, defaultPaginationArgumentMinimum, useResourceLimitations };
|