leangen / graphql-spqr

Build a GraphQL service in seconds
Apache License 2.0
1.09k stars 179 forks source link

Explicitly register Java classes to the GraphQL schema. #475

Closed Balf closed 8 months ago

Balf commented 9 months ago

Hi @kaqqao,

Thanks for your amazing work on this library. I've been fiddling around with for some days now and I've been looking into following.

I'm working on adding a GraphQL API on an existing codebase where a part of the application is deployed as libs and another part of the application is deployed via OSGi.

The main entry point for GraphQL is in the libraries and most interfaces are in those libraries as well. Getting GraphQL to run with these interfaces and their annotations has been very easy to set up. However, it's possible that some implementations of the interfaces are outside of the libraries and in an OSGi bundle.

Since OSGi bundles have their own classloader SPQR does not discover these classes when generating the schema. I've got a working solution where I manually create a GraphQLType within the OSGi bundle and register that via the .withAdditionalTypes() method. But that solution does require me to program the entire GraphQLType.

Short example in pseudo code:

Java libraries

@GraphQLInterface (name = "IAnimal", implementationAutoDiscovery = true)
interface IAnimal {
   @GraphQLQuery
   String getName;
}

//This is discovered
public class Bat implements IAnimal {
    public String getName() {
        return "Bat";
    }
}

OSGi bundle

//This isn't auto discovered
public class Llama implements IAnimal {
    public String getName() {
        return "Llama";
    }
    @GraphQLQuery
    public int getNumberOfLegs() {
        return 4;
    }
}

What would be the easiest/most efficiënt to register Llama to the GraphQL Schema?

For completeness the code that generates the schema

        AnimalService animalService = new AnimalService();

        GraphQLSchemaGenerator schemaGenerator = new GraphQLSchemaGenerator()
                .withBasePackages("io.leangen")
                .withOperationsFromSingleton(animalService)
                .withNestedResolverBuilders(new AnnotatedResolverBuilder());

        //My current solution that can register additional types
        for (AdditionalTypeProvider additionalTypeProvider : additionalTypeProviders) {
            List<GraphQLType> additionalTypes = additionalTypeProvider.getGraphQLTypes();
            GraphQLCodeRegistry codeRegistry = additionalTypeProvider.getCodeRegistry();
            Map<String, AnnotatedType> additionalTypeMappings = additionalTypeProvider.getAdditionalTypeMappings();
            schemaGenerator.withAdditionalTypes(additionalTypes, additionalTypeMappings, codeRegistry);
        }

        GraphQLSchema schema = schemaGenerator.generate();
Balf commented 8 months ago

@kaqqao I noticed your comments in #476 yesterday, both those aimed at nihila and myself, and took a look at the DefaultImplementationDiscoveryStrategy class. This actually already supports a method to add additional implemention classes, which resolves my issue quite nicely! My code now looks like this:

AnimalService animalService = new AnimalService();

GraphQLSchemaGenerator schemaGenerator = new GraphQLSchemaGenerator()
        .withBasePackages("io.leangen")
        .withOperationsFromSingleton(animalService)
        .withNestedResolverBuilders(new AnnotatedResolverBuilder());

var defaultImplementationDiscoveryStrategy = new DefaultImplementationDiscoveryStrategy();

//The new solution that just takes a class that implements IAnimal
for (var additionalImplementationProvider : additionalImplementationProvider) {
    // This line returns Llama.class from my original code
    Class<?> additionalImplementation = additionalImplementationProvider.getClass(); 
    defaultImplementationDiscoveryStrategy.withAdditionalImplementations(additionalImplementation);
}

schemaGenerator.withImplementationDiscoveryStrategy(defaultImplementationDiscoveryStrategy);

GraphQLSchema schema = schemaGenerator.generate();

This code takes the annotated Llama.class and adds it to the Schema as required, without having to define the entire type myself as per my previous solution. I'm closing this issue, as this solution does exactly what I wanted.

If you have time, could you take a look at #480 that I raised recently? That one is somewhat more complex and I don't know where to start in that case.

kaqqao commented 7 months ago

Yup, this looks good! 👍

Just 2 remarks:

I'll have a look at #480.

Balf commented 7 months ago

Thanks for your feedback, the package name is indeed my own package in the actual code. The second point I'll update, cheers!

Balf commented 7 months ago

@kaqqao I'd like to contribute to the documentation, so I can add documentation for issues like mine. What would be the best way to do that?