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)

Sign up to request clarification or add additional context in comments.

1 Comment

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/…>
1

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().<ExtensionsBuilder>get(ExtensionsBuilder.class);
if (extensionsBuilder != null) {
    extensionsBuilder.addValue("yourKey", yourValue);
}

Comments

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

Comments

-1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.