queryComplexityRuleBuilder function
Returns a ValidationRule that reports errors when
the maxComplexity
or maxDepth
is reached.
The complexity for each fieldNode is: complexity = fieldComplexity + (childrenComplexity + fieldTypeComplexity) * complexityMultiplier
Where fieldComplexity is the ElementComplexity in
GraphQLObjectField.attachments or defaultFieldComplexity
if there aren't any.
childrenComplexity is: If the field is a scalar or enum (leaf types): 0 if it is an object or interface: sum(objectFieldsComplexities) if it is an union: max(possibleTypesComplexities)
fieldTypeComplexity will be taken as the ElementComplexity from GraphQLNamedType.attachments or 0 if there aren't any.
If the fieldType is a GraphQLListType, complexityMultiplier
will be the provided listComplexityMultiplier
, otherwise 1.
Implementation
ValidationRule queryComplexityRuleBuilder({
required int maxComplexity,
required int maxDepth,
int listComplexityMultiplier = 10,
int defaultFieldComplexity = 1,
}) {
return (ValidationCtx context) {
return TypedVisitor()
..add<OperationDefinitionNode>((node) {
int operationComplexity = 0;
int currentDepth = 0;
int maxOperationDepth = 0;
late final int Function(PossibleSelectionsObject) _compObj;
int _comp(
GraphQLObjectField field,
PossibleSelections? selections,
) {
currentDepth += 1;
maxOperationDepth = max(maxOperationDepth, currentDepth);
final int childrenComplexity;
if (selections == null) {
// leaf type
childrenComplexity = 0;
} else if (selections.isUnion) {
final unionComplexities = selections.unionMap.values.map(_compObj);
childrenComplexity = unionComplexities.fold(0, max);
} else {
childrenComplexity = _compObj(selections.forObject);
}
currentDepth -= 1;
final fieldComplexity = _getElementComplexity(field)?.complexity ??
defaultFieldComplexity;
final type = field.type;
final fieldTypeComplexity = selections == null || selections.isUnion
? _getTypeComplexity(type)?.complexity ?? 0
// object complexity already accounted for in childrenComplexity
: 0;
final _listComplexityMultiplier = type is GraphQLListType ||
type is GraphQLNonNullType && type.ofType is GraphQLListType
? listComplexityMultiplier
: 1;
return fieldComplexity +
(childrenComplexity + fieldTypeComplexity) *
_listComplexityMultiplier;
}
_compObj = (PossibleSelectionsObject forObject) {
final objTypeComplexity =
_getTypeComplexity(forObject.objectType)?.complexity ?? 0;
final fieldsComplexity = forObject.mapAliased.values
.map((value) => _comp(value.field, value.lookAhead()))
.sum;
return objTypeComplexity + fieldsComplexity;
};
final rootType = context.schema.getRootType(node.type);
if (rootType != null) {
final variableValues = <String, Object?>{};
final fields = collectFields(
context.schema,
context.fragmentsMap,
rootType,
node.selectionSet,
variableValues,
);
for (final e in fields.entries) {
final field = rootType.fieldByName(e.value.first.name.value);
if (field == null) continue;
final selections = possibleSelectionsCallback(
context.schema,
field,
e.value,
context.document,
variableValues,
)();
final rootFieldComplexity = _comp(field, selections);
operationComplexity += rootFieldComplexity;
}
}
final operationName = node.name == null ? '' : ' "${node.name!.value}"';
if (operationComplexity > maxComplexity) {
context.reportError(
GraphQLError(
'Maximum operation complexity of $maxComplexity reached.'
' Operation$operationName complexity: $operationComplexity.',
extensions: errorExtensions(
specUrl:
'https://github.com/juancastillo0/leto#query-complexity',
errorCode: 'queryComplexity',
),
),
);
}
if (maxOperationDepth > maxDepth) {
context.reportError(
GraphQLError(
'Maximum operation depth of $maxDepth reached.'
' Operation$operationName depth: $maxOperationDepth.',
extensions: errorExtensions(
specUrl:
'https://github.com/juancastillo0/leto#query-complexity',
errorCode: 'queryDepthComplexity',
),
),
);
}
});
};
}