/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */ /** * Created by Ivo Meißner on 28.07.17. */ import { getNamedType, GraphQLError, GraphQLInterfaceType, GraphQLObjectType, isAbstractType, isCompositeType, Kind, TypeInfo, ValidationContext, visit, visitWithTypeInfo } from 'graphql'; import { getArgumentValues, getDirectiveValues, getVariableValues } from 'graphql/execution/values.js'; function queryComplexityMessage(max, actual) { return `The query exceeds the maximum complexity of ${max}. ` + `Actual complexity is ${actual}`; } export function getComplexity(options) { const typeInfo = new TypeInfo(options.schema); const errors = []; const context = new ValidationContext(options.schema, options.query, typeInfo, (error)=>errors.push(error)); const visitor = new QueryComplexity(context, { // Maximum complexity does not matter since we're only interested in the calculated complexity. context: options.context, estimators: options.estimators, maximumComplexity: Infinity, operationName: options.operationName, variables: options.variables }); visit(options.query, visitWithTypeInfo(typeInfo, visitor)); // Throw first error if any if (errors.length) { throw errors.pop(); } return visitor.complexity; } export class QueryComplexity { complexity; context; estimators; includeDirectiveDef; OperationDefinition; options; requestContext; skipDirectiveDef; variableValues; constructor(context, options){ if (!(typeof options.maximumComplexity === 'number' && options.maximumComplexity > 0)) { throw new Error('Maximum query complexity must be a positive number'); } this.context = context; this.complexity = 0; this.options = options; this.includeDirectiveDef = this.context.getSchema().getDirective('include'); this.skipDirectiveDef = this.context.getSchema().getDirective('skip'); this.estimators = options.estimators; this.variableValues = {}; this.requestContext = options.context; this.OperationDefinition = { enter: this.onOperationDefinitionEnter, leave: this.onOperationDefinitionLeave }; } createError() { if (typeof this.options.createError === 'function') { return this.options.createError(this.options.maximumComplexity, this.complexity); } return new GraphQLError(queryComplexityMessage(this.options.maximumComplexity, this.complexity)); } nodeComplexity(node, typeDef) { if (node.selectionSet) { let fields = {}; if (typeDef instanceof GraphQLObjectType || typeDef instanceof GraphQLInterfaceType) { fields = typeDef.getFields(); } // Determine all possible types of the current node let possibleTypeNames; if (isAbstractType(typeDef)) { possibleTypeNames = this.context.getSchema().getPossibleTypes(typeDef).map((t)=>t.name); } else { possibleTypeNames = [ typeDef.name ]; } // Collect complexities for all possible types individually const selectionSetComplexities = node.selectionSet.selections.reduce((complexities, childNode)=>{ // let nodeComplexity = 0; let innerComplexities = complexities; let includeNode = true; let skipNode = false; for (const directive of childNode.directives ?? []){ const directiveName = directive.name.value; switch(directiveName){ case 'include': { const values = getDirectiveValues(this.includeDirectiveDef, childNode, this.variableValues || {}); if (typeof values.if === 'boolean') { includeNode = values.if; } break; } case 'skip': { const values = getDirectiveValues(this.skipDirectiveDef, childNode, this.variableValues || {}); if (typeof values.if === 'boolean') { skipNode = values.if; } break; } } } if (!includeNode || skipNode) { return complexities; } switch(childNode.kind){ case Kind.FIELD: { const field = fields[childNode.name.value]; // Invalid field, should be caught by other validation rules if (!field) { break; } const fieldType = getNamedType(field.type); // Get arguments let args; try { args = getArgumentValues(field, childNode, this.variableValues || {}); } catch (e) { this.context.reportError(e); return complexities; } // Check if we have child complexity let childComplexity = 0; if (isCompositeType(fieldType)) { childComplexity = this.nodeComplexity(childNode, fieldType); } // Run estimators one after another and return first valid complexity // score const estimatorArgs = { type: typeDef, args, childComplexity, context: this.requestContext, field, node: childNode }; const validScore = this.estimators.find((estimator)=>{ const tmpComplexity = estimator(estimatorArgs); if (typeof tmpComplexity === 'number' && !isNaN(tmpComplexity)) { innerComplexities = addComplexities(tmpComplexity, complexities, possibleTypeNames); return true; } return false; }); if (!validScore) { this.context.reportError(new GraphQLError(`No complexity could be calculated for field ${typeDef.name}.${field.name}. ` + 'At least one complexity estimator has to return a complexity score.')); return complexities; } break; } case Kind.FRAGMENT_SPREAD: { const fragment = this.context.getFragment(childNode.name.value); // Unknown fragment, should be caught by other validation rules if (!fragment) { break; } const fragmentType = this.context.getSchema().getType(fragment.typeCondition.name.value); // Invalid fragment type, ignore. Should be caught by other validation rules if (!isCompositeType(fragmentType)) { break; } const nodeComplexity = this.nodeComplexity(fragment, fragmentType); if (isAbstractType(fragmentType)) { // Add fragment complexity for all possible types innerComplexities = addComplexities(nodeComplexity, complexities, this.context.getSchema().getPossibleTypes(fragmentType).map((t)=>t.name)); } else { // Add complexity for object type innerComplexities = addComplexities(nodeComplexity, complexities, [ fragmentType.name ]); } break; } case Kind.INLINE_FRAGMENT: { let inlineFragmentType = typeDef; if (childNode.typeCondition && childNode.typeCondition.name) { inlineFragmentType = this.context.getSchema().getType(childNode.typeCondition.name.value); if (!isCompositeType(inlineFragmentType)) { break; } } const nodeComplexity = this.nodeComplexity(childNode, inlineFragmentType); if (isAbstractType(inlineFragmentType)) { // Add fragment complexity for all possible types innerComplexities = addComplexities(nodeComplexity, complexities, this.context.getSchema().getPossibleTypes(inlineFragmentType).map((t)=>t.name)); } else { // Add complexity for object type innerComplexities = addComplexities(nodeComplexity, complexities, [ inlineFragmentType.name ]); } break; } default: { innerComplexities = addComplexities(this.nodeComplexity(childNode, typeDef), complexities, possibleTypeNames); break; } } return innerComplexities; }, {}); // Only return max complexity of all possible types if (!selectionSetComplexities) { return NaN; } return Math.max(...Object.values(selectionSetComplexities), 0); } return 0; } onOperationDefinitionEnter(operation) { if (typeof this.options.operationName === 'string' && this.options.operationName !== operation.name.value) { return; } // Get variable values from variables that are passed from options, merged // with default values defined in the operation const { coerced, errors } = getVariableValues(this.context.getSchema(), // We have to create a new array here because input argument is not readonly in graphql ~14.6.0 operation.variableDefinitions ? [ ...operation.variableDefinitions ] : [], this.options.variables ?? {}); if (errors && errors.length) { // We have input validation errors, report errors and abort errors.forEach((error)=>this.context.reportError(error)); return; } this.variableValues = coerced; switch(operation.operation){ case 'mutation': this.complexity += this.nodeComplexity(operation, this.context.getSchema().getMutationType()); break; case 'query': this.complexity += this.nodeComplexity(operation, this.context.getSchema().getQueryType()); break; case 'subscription': this.complexity += this.nodeComplexity(operation, this.context.getSchema().getSubscriptionType()); break; default: throw new Error(`Query complexity could not be calculated for operation of type ${operation.operation}`); } } onOperationDefinitionLeave(operation) { if (typeof this.options.operationName === 'string' && this.options.operationName !== operation.name.value) { return; } if (this.options.onComplete) { this.options.onComplete(this.complexity); } if (this.complexity > this.options.maximumComplexity) { return this.context.reportError(this.createError()); } } } /** * Adds a complexity to the complexity map for all possible types * @param complexity * @param complexityMap * @param possibleTypes */ function addComplexities(complexity, complexityMap, possibleTypes) { for (const type of possibleTypes){ if (Object.prototype.hasOwnProperty.call(complexityMap, type)) { complexityMap[type] += complexity; } else { complexityMap[type] = complexity; } } return complexityMap; } //# sourceMappingURL=QueryComplexity.js.map