How to respond with extensions using graphql-java?

How to respond with extensions using graphql-java?


7

My responses from GraphQL have to follow a particular format of

{
    data:{}
    errors:[{}]
    extensions:{}
}

However, I am uncertain how to respond with extensions from my methods.
I am using graphql-spring-boot which pulls in graphql-java, graphql-java-tools, and graphql-java-servlet.

I understand that my results from a query/mutation method will be wrapped in the data object, and if any exceptions were thrown they’ll be wrapped in errors.

If I have a GraphQL Schema defined as

type Query {
    someQuery(input: String!) : String!
}

and a corresponding Java method

public String someQuery(String input) {
    return "Hello, world!";
}

The GraphQL response will be

{
    data: { "Hello, world!"}
}

I would like to know how I am able to add extensions to my GraphQL response so that the output is as:

{
    data: {"Hello, world!"}
    extensions: { <something>}
}

4 Answers
4


6

The best way I’ve found to return extensions is to implement a subclass of SimpleInstrumentation that overrides instrumentExecutionResult (code stolen partially from graphql-java’s TracingInstrumentation):

@Override
public CompletableFuture<ExecutionResult> instrumentExecutionResult(
        ExecutionResult executionResult,
        InstrumentationExecutionParameters parameters) {
    Map<Object, Object> currentExt = executionResult.getExtensions();
    Map<Object, Object> newExtensionMap = new LinkedHashMap<>();
    newExtensionMap.putAll(currentExt == null ? Collections.emptyMap() : currentExt);
    newExtensionMap.put("MyExtensionKey", myExtensionValue);

    return CompletableFuture.completedFuture(
        new ExecutionResultImpl(
            executionResult.getData(), 
            executionResult.getErrors(), 
            newExtensionMap));
}

When setting up the GraphQL instance you then pass an instance of the instrumentation class in:

GraphQL graphQL = GraphQL
        .newGraphQL(schema)
        .instrumentation(new MyInstrumentation())
        .build()

(Not sure entirely how this is handled by graphql-spring-boot but would imagine there is some way to @Autowire or otherwise configure the GraphQL instance? InstrumentationProvider from graphql-java-servlet might be what you’d use to do this)

1

  • There is a convenient builder class that makes it a bit easier to build a new ExecutionResultImpl from an existing one. Less boilerplate. See <github.com/graphql-java/graphql-java/blob/…>

    – David Groomes

    Apr 21, 2022 at 22:36


0

Okay so after spending some 20 hours on this thing . Finally figured it out .
Required a lot of trial and error . Also we most definitely dont have proper/enough documenting for this .

Although my solution answers this question , It answers an extra question as well, which is -> How do I make extensions dynamic for every query ?

Meaning , all 3 (data, errors and extensions) would be dynamic in the response .

My use case – How do I add logs for each query in extensions ?

Answer – (Note – The code below contains lombok annotations)

Step 1 – Creating your Instrumentation Class and the Instrumentation State Class .

//This is the state class . 
//State is the first thing created whenever a graphql query is run . 
//We will embed our Logs object here .
@Builder
class LogInstrumentationState implements InstrumentationState {
    @Getter
    @Setter
    public LogsDto logsDto;
}


//This is the instrumentation class that will be used to create the graphql object .
//The overridden methods are different stages in the graphql query execution 
@Builder
public class LogsInstrumentation extends SimpleInstrumentation {

  //First stage in graphql query execution .
  //Object for our custom state class object is created here . 
  @Override
  public InstrumentationState createState() {
    return LogInstrumentationState.builder().build();
  }

   //Second Stage in graphql query execution
   //Reference of initialized Logs object in the main code flow is passed here . 
   //This reference is stored in our custom state class's object .
  @Override
  public ExecutionInput instrumentExecutionInput(ExecutionInput executionInput,
                                                 InstrumentationExecutionParameters parameters) {
    LogsDto logsDto = (LogsDto) executionInput.getExtensions().get("logs");

    LogInstrumentationState logInstrumentationState = parameters.getInstrumentationState();
    logInstrumentationState.setLogsDto(logsDto);
    return super.instrumentExecutionInput(executionInput, parameters);
  }

  //This is the last stage in the graphql query execution .
  //Logs are taken from the custom container and added into extensions . 
  @Override
  public CompletableFuture<ExecutionResult> instrumentExecutionResult(
      ExecutionResult executionResult, InstrumentationExecutionParameters parameters) {

    Map<Object, Object> newExtensionMap = getExtensionsMap(executionResult,parameters);

    return CompletableFuture.completedFuture(
        new ExecutionResultImpl(
            executionResult.getData(),
            executionResult.getErrors(),
            newExtensionMap));
  }

  //Helper function
  public Map<Object, Object> getExtensionsMap(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) {
    Map<Object, Object> currentExt = executionResult.getExtensions();
    Map<Object, Object> newExtensionMap = new LinkedHashMap<>();
    newExtensionMap.putAll(currentExt == null ? Collections.emptyMap() : currentExt);
    LogsDto logsDto =
        ((LogInstrumentationState)parameters.getInstrumentationState()).getLogsDto();
    newExtensionMap.put(ControllerConstants.LOGS, logsDto);
    return newExtensionMap;
  }

}

Step 2 – Creating graphql object –

GraphQL graphQl = GraphQL.newGraphQL(graphqlSchema).instrumentation(LogsInstrumentation.builder().build())
        .build();

Step 3 – Creating executing input . This is where you pass the dynamic Log object into the LogsInstrumentation class .

var executionInput = ExecutionInput.newExecutionInput()
          .query(...)
          .variables(...)
          .operationName(...)
          .extensions(Map.of("logs",logsDto))
          .dataLoaderRegistry(...)
          .graphQLContext(graphqlContext).build();


      ExecutionResult executionResult = graphQl.execute(executionInput);

Step 4 – This is how you get your extensions after your query has completed .

Map<Object, Object> extensions = executionResult.getExtensions();
LogsDto logsDto = (LogsDto) extensions.get("logs");

My source


0

Starting with graphql-java 21.0, you can set extensions directly on the DataFetcherResult: https://github.com/graphql-java/graphql-java/blob/v21.0/src/main/java/graphql/execution/DataFetcherResult.java#L205

It was added here. If you’re stuck on an older version of graphql-java, you can get it from the DataFetchingEnvironment like so:

ExtensionsBuilder extensionsBuilder = dfe.getGraphQlContext().get<ExtensionsBuilder>(ExtensionsBuilder.class);
if (extensionsBuilder != null) {
    extensionsBuilder.addValue("yourKey", yourValue);
}


-1

you can implement GraphQLError where extra error properties can be added.

0



Leave a Reply

Your email address will not be published. Required fields are marked *