Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compare GraphQL query tree in java

Tags:

java

graphql

My spring-boot application is generating GraphQL queries, however I want to compare that query in my test.

So basically I have two strings where the first one is containing the actual value and the latter one the expected value. I want to parse that in a class or tree node so I can compare them if both of them are equal. So even if the order of the fields are different, I need to know if it's the same query.

So for example we have these two queries: Actual:

query Query {
 car {
  brand
  color
  year
 }
 person {
  name
  age
 }
}

Expected

query Query {
 person {
  age
  name
 }
 car {
  brand
  color
  year
 }
}

I expect that these queries both are semantically the same.

I tried

Parser parser = new Parser();
Document expectedDocument = parser.parseDocument(expectedValue);
Document actualDocument = parser.parseDocument(actualValue);

if (expectedDocument.isEqualTo(actualDocument)) {
    return MatchResult.exactMatch();
}

But found out that it does nothing since the isEqualTo is doing this:

public boolean isEqualTo(Node o) {
    if (this == o) {
        return true;
    } else {
        return o != null && this.getClass() == o.getClass();
    }
}

I know with JSON I can use Jackson for this purpose and compare treenodes, or parsing it into a Java object and have my own equals() implementation, but I don't know how to do that for GraphQL Java.

How can I parse my GraphQL query string into an object so that I can compare it?

like image 516
com2ghz Avatar asked Feb 24 '26 18:02

com2ghz


1 Answers

I have recently solved this problem myself. You can reduce the query to a hash and compare the values. You can account for varied query order by utilizing a tree structure. You can take advantage of the QueryTraverser and QueryReducer to accomplish this.

First you can create the QueryTraverser, the exact method for creating this will depend on your execution point. Assuming you are doing it in the AsyncExecutor with access to the ExecutionContext the below code snippet will suffice. But you can do this in instrumentation or the data fetcher itself if you so choose;

val queryTraverser = QueryTraverser.newQueryTraverser()
            .schema(context.graphQLSchema)
            .document(context.document
            .operationName(context.operationDefinition.name)
            .variables(context.executionInput?.variables ?: emptyMap())
            .build()

Next you will need to provide an implementation of the reducer, and some accumulation object that can add each field to a tree structure. Here is a simplified version of an accumulation object

class MyAccumulation {

    /**
     * A sorted map of the field node of the query and its arguments
     */
    private val fieldPaths = TreeMap<String, String>()

    /**
     * Add a given field and arguments to the sorted map.
     */
    fun addFieldPath(path: String, arguments: String) {
        fields[path] = arguments
    }

    /**
     * Function to generate the query hash
     */
    fun toHash(): String {

        val joinedFields = fieldPaths.entries
            .joinToString("") { "${it.key}[${it.value}]" }

        return HashingLibrary.hashingfunction(joinedFields)
}

A sample reducer implementation would look like the below;

class MyReducer : QueryReducer<MyAccumulation> {

    override fun reduceField(
        fieldEnvironment: QueryVisitorFieldEnvironment,
        acc: MyAccumulation
    ): MyAccumulation {
        if (fieldEnvironment.isTypeNameIntrospectionField) {
            return acc
        }
        
        // Get your field path, this should account for
        // the same node with different parents, and you should recursively
        // traverse the parent environment to construct this
        val fieldPath = getFieldPath(fieldEnvironment)

        // Provide a reproduceable stringified arguments string
        val arguments = getArguments(fieldEnvironment.arguments)

        acc.addFieldPath(fieldPath, arguments)

        return acc
    }
}

Finally put it all together;

val queryHash = queryTraverser
    .reducePreOrder(MyReducer(), MyAccumulation())
    .toHash()

You can now generate a repdocueable hash for a query that does not care about the query structure, only the actual fields that were requested.

Note: These code snippets are in kotlin but are transposable to Java.

like image 198
Jags Avatar answered Feb 26 '26 07:02

Jags



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!