Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calculate the query complexity based on how nested a field is (depth limitation) #96

Open
kasir-barati opened this issue Dec 19, 2024 · 6 comments

Comments

@kasir-barati
Copy link

kasir-barati commented Dec 19, 2024

So I was trying to see how other calculate their query complexity and ended up in the hygraph website, as you can see in their doc they are saying that how they calculate the query complexity is like this:

  • Scalar fields: Each scalar field in a query contributes one point to the query complexity.
  • Relations / Unions: Relations multiply their complexity times the level of nesting in the query.

Ref.

So I tried to find out how I can do it with this lib but it seems it is not possible with the current API. I also wanna point out that I feel like it is not completely impossible but I kinda have no clue as to how should I go about this: "multiply their complexity times the level of nesting in the query."

Any comment?

Side note: In NestJS we can see the same recommendation about scalar fields, i.e. to add one point to the overall complexity per scalar field (ref).

@kasir-barati
Copy link
Author

BTW I forked this repo since I thought I might find some kind of clue there and open a PR, but I also love to hear what you know about this.

@kasir-barati
Copy link
Author

So my first question now I know that I have access to something called node which in NestJS is part of the type defined for the complexity estimator as you can see it here

But in runtime we have access to it:

node

This object is the FieldNode. Now I think I just need to look for a way to find the current fields nesting level by using node.

@kasir-barati
Copy link
Author

I had some progress, now I know I can calculate field nestedness level using node.loc.source and field.name. Although field.name is not gonna cut it since what if it is nested within nested with the same name:

{ 
  getPosts {
    id 
    author { 
      posts { 
        id 
      } 
    } 
  }
}

I cannot differentiate the first id from the second id but I can use field.astNode.loc.start I guess to do some string interpolation. Although I feel like I am reinventing the wheel, are not I 💦?

@kasir-barati
Copy link
Author

Had some progress: https://stackoverflow.com/q/79297272/8784518

@kasir-barati
Copy link
Author

So I managed to do it. Here is how I did it: I have a fieldDepth helper function to calculate the depth of each field. Then I mutiply it to a hard coded value for that field and add it to the childComplexity. I am not sure about the math formula since in my test app in my local system when I execute this query it takes 5 seconds to return a response.

Of course I am not using dataloader and I have the issue of N+1. But if you're interested in it check out my code here.

Off-topic:

  1. I've been meaning to ask about this YouTube video where Morris Matsa separates "field cost" from "type cost".
  2. He also talks about how he gives complexity of 0 to simple scalar types whereas here hygraph says that scalar fields contributes one point to the overall complexity of a query.
  3. Do you think we should add this to this lib's doc @ivome? TBH I have mixed fillings about it since this is rather implementation details and has nothing to do directly with this lib, but on the other hand I feel a lot of devs who are new to GraphQL will benefit from it since it shows how to calculate the complexity of a query.

Any comment on these?

@kasir-barati
Copy link
Author

kasir-barati commented Dec 24, 2024

BTW I was digging on this topic of depth limitation and I read here that this lib does check the complexity of a query by assigning a number to each field. So when I looked at my own code I realized that most likely I do not need even that lib since I am already calculating the depth of a field and incorporate it to the field's complexity score.

So maybe we can add it to this lib so that people know how they can limit the complexity of a query by its depth + a static score. My current implementation look like this:

@Field(() => UserDto, {
  complexity({ childComplexity, node }: ComplexityEstimatorArgs) {
    const depth = fieldDepth(node);
    const complexity = 1 * depth + childComplexity;

    return complexity;
  },
})
author: UserDto;

And here is the code for field-depth.ts:

import { FieldNode } from 'graphql';
import { fieldDepthQueryNormalizer } from './field-depth-query-normalizer.util';

/**
 * @description Calculates the depth of a field
 */
export function fieldDepth(node: Readonly<FieldNode>) {
  if (!node.loc) {
    throw 'EmptyNodeLocation';
  }
  if (!node.name.loc) {
    throw 'EmptyNodeNameLocation';
  }

  const normalizedSourceBody = fieldDepthQueryNormalizer(
    node.loc.source.body,
  );

  return (
    normalizedSourceBody.slice(0, node.name.loc.start).split('{')
      .length - 2
  );
}

And finally the field-depth-query-normalizer.util.ts:

import {
  DefinitionNode,
  DocumentNode,
  Kind,
  parse,
  print,
} from 'graphql';

/**
 * @description Strips the query from fragments
 */
export function fieldDepthQueryNormalizer(
  query: Readonly<string>,
): string {
  const ast = parse(query);
  const definitionNodes: DefinitionNode[] = [];
  const definitions = (ast as DocumentNode).definitions;

  for (const definition of definitions) {
    if (definition.kind !== Kind.OPERATION_DEFINITION) {
      continue;
    }
    definitionNodes.push(definition);
  }

  return definitionNodes
    .map((definitionNode) => print(definitionNode))
    .join(' ');
}

@kasir-barati kasir-barati changed the title Calculate the query complexity based on how nested a field is Calculate the query complexity based on how nested a field is (depth limitation) Dec 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant