0

I have a SpringBoot application that uses GraphQL to return data to a request.

What I have

One of my queries returns a list of responses based on a list of ids supplied. So my .graphqls file is a follows:

type Query {
    texts(ids: [String]): [Response]
}
type Response {
   id: String
   text: String
}

and the following are request & response:

Request

texts(ids:["id 1","id 2"]){
   id
   text
}

Response

{
 "data": [
   {
     "id": "id 1",
     "text": "Text 1"
   },
   {
     "id": "id 2",
     "text": "Text 2"
   }
  ]
}

At the moment, if id(s) is/are not in aws, then exception is thrown and the response is an error block saying that certain id(s) was/were not found. Unfortunately, the response for other ids that were found is not displayed - instead the data block returns a null. If I check wether data is present in the code via ssay if/else statment, then partial response can be returned but I will not know that it is a partial response.

What I want to happen

My application fetches the data from aws and occasionally some of it may not be present, meaning that for one of the supplied ids, there will be no data. Not a problem, I can do checks and simply never process this id. But I would like to inform a user if the response I returned is partial (and some info is missing due to absence of data).

See example of the output I want at the end.

What I tried

While learning about GraphQL, I have encountered an instrumentation - a great tool for logging. Since it goes through all stages of execution, I thought that I can try and change the response midway - the Instrumentation class has a lot of methods, so I tried to find the one that works. I tried to make beginExecution(InstrumentationExecutionParameters parameters) and instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) to work but neither worked for me.

I think the below may work, but as comments suggests there are parts that I failed to figure out

    @Override
    public GraphQLSchema instrumentSchema(GraphQLSchema schema, InstrumentationExecutionParameters parameters) {
        String id = ""; // how to extract an id from the passed query (without needing to disect parameters.getQuery();
        log.info("The id is " + id);
        if(s3Service.doesExist(id)) {
            return super.instrumentSchema(schema, parameters);
        }
        schema.transform(); // How would I add extra field
        return schema;
    }

I also found this post that seem to offer more simpler solution. Unfortunately, the link provided by host does not exist and link provided by the person who answered a question is very brief. I wonder if anyone know how to use this annotation and maybe have an example I can look at?

Finally, I know there is DataFetcherResult which can construct partial response. The problem here is that some of my other apps use reactive programming, so while it will be great for Spring mvc apps, it will not be so great for spring flux apps (because as I understand it, DataFetcherResult waits for all the outputs and as such is a blocker). Happy to be corrected on this one.

Desired output

I would like my response to look like so, when some data that was requested is not found.

Either

{
 "data": [
   {
     "id": "id 1",
     "text": "Text 1"
   },
   {
     "id": "id 2",
     "text": "Text 2"
   },
   {
     "id": "Non existant id",
     "msg": "This id was not found"
   }
  ]
}

or

{
 "error": [
   "errors": [
     {
       "message": "There was a problem getting data for this id(s): Bad id 1"
     }
   ]
 ],
 "data": [
   {
     "id": "id 1",
     "text": "Text 1"
   },
   {
     "id": "id 2",
     "text": "Text 2"
   }
  ]
}
Joe
  • 337
  • 6
  • 21

1 Answers1

0

So I figured out one way of achieving this, using instrumentation and extension block (as oppose to error block which is what I wanted to use initially). The big thanks goes to fellow Joe, who answered this question. Combine it with DataFetchingEnviroment (great video here) variable and I got the working solution.

My instrumentation class is as follows

public class CustomInstrum extends SimpleInstrumentation {

    @Override
    public CompletableFuture<ExecutionResult> instrumentExecutionResult(
            ExecutionResult executionResult,
            InstrumentationExecutionParameters parameters) {
        if(parameters.getGraphQLContext().hasKey("Faulty ids")) {
            Map<Object, Object> currentExt = executionResult.getExtensions();
            Map<Object, Object> newExtensionMap = new LinkedHashMap<>();
            newExtensionMap.putAll(currentExt == null ? Collections.emptyMap() : currentExt);
            newExtensionMap.put("Warning:", "No data was found for the following ids: " + parameters.getGraphQLContext().get("Faulty ids").toString());

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

and my DataFetchingEnviroment is in my resolver:

public CompletableFuture<List<Article>> articles(List<String> ids, DataFetchingEnvironment env) {
        List<CompletableFuture<Article>> res = new ArrayList<>();
        // Below's list would contain the bad ids
        List<String> faultyIds = new ArrayList<>();
        for(String id : ids) {
            log.info("Getting article for id {}",id);
            if(s3Service.doesExist(id)) {
               res.add(filterService.gettingArticle(id));
            } else {
                faultyIds.add(id);// if data doesn't exist then id will not be processed
            }
        }
        // if we have any bad ids, then we add the list to the context for instrumentations to pick it up, right before returning a response
        if(!faultyIds.isEmpty()) {
            env.getGraphQlContext().put("Faulty ids", faultyIds);
        }
        return CompletableFuture.allOf(res.toArray(new CompletableFuture[0])).thenApply(item -> res.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList()));
    }

You can obviously separate error related ids to different contexts but for my simple case, one will suffice. I however still interested in how can the same results be achieved via error block, so i will leave this question hanging for a bit before accepting this as a final answer.

My response looks as follows now:

{
  "extensions": {
    "Warning:": "No data was found for the following ids: [234]"
  },
  "data": { ... }

My only concern with this approach is security and "doing the right thing" - is this correct thing to do, adding something to the context and then using instrumentation to influence the response? Are there any potential security issues? If someone know anything about it and could share, it will help me greatly!

Update After further testing it appears if exception is thrown it will still not work, so it only works if you know beforehand that something goes wrong and add appropriate exception handling. Cannot be used with try/catch block. So I am a half step back again.

Joe
  • 337
  • 6
  • 21