javaspringspring-bootgraphqlspring-graphql

Accessing grandparent (source of source) in Spring for GraphQL @SchemaMapping


I can't find out how I can load a referenced resource, identified by a deeply nestet property in combination with a property higher up in a nested data structure.

Given these classes (getters and setters etc. omitted, but you get the idea):

class Resource {
    int tenantId;
    List<SubResource> subResources;
}

class SubResource {
    String name;
    int userId;
}

class Tenant {
    int id;
    String name;
}

class User {
    int id;
    String name;
}

And this GraphQL schema:

type Resource {
    tenantName: String
    subResources: [SubResource]
}

type SubResource {
   userName
}

I can resolve the Resource.tenantName property in my ResourceController with a method like this:

@SchemaMapping
public CompletableFuture<String> tenantName(Resource resource, DataLoader<Integer, Tenant> loader) {
    return loader.load(resource.getTenantId()).thenApply(User::getName);
}

But SubResource.userName... how do I do that, when I need both tenantId and userId to look up userName? This is as far as I got so far :-/

@SchemaMapping
public CompletableFuture<String> userName(SubResource subResource, DataLoader<TenantAndUserId, User> loader) {
    // Look up tenant id - but how?
    int tenantId = ???;
    // TenantAndUserId just holds tenantId and userId properties
    var key = new TenantAndUserId(tenantId, subResource.getUserId()); 
    return loader.load(key).thenApply(User::getName);
}


Solution

  • I finally found out how to use localContext to pass tenantId down to the userName method, so just in case others have the same issue, here is what I added:

    @SchemaMapping
    DataFetcherResult<List<SubResource>> subResources(Resource resource) {
          return DataFetcherResult.<List<SubResource>>newResult()
                .data(resource.getSubResources())
                .localContext(GraphQLContext.newContext()
                      .of("tenantId", resource.getTenantId())
                      .build())
                .build();
    }
    

    and then the userName method can have tenantId injected like this:

    @SchemaMapping
    public CompletableFuture<String> userName(SubResource subResource, @LocalContextValue("tenantId") UUID tenantId, DataLoader<TenantAndUserId, User> loader) {
        // TenantAndUserId just holds tenantId and userId properties
        var key = new TenantAndUserId(tenantId, subResource.getUserId()); 
        return loader.load(key).thenApply(User::getName);
    }