graphql-jpa
graphql-jpa copied to clipboard
Allow Schema manipulation to support mutations
At the moment the whole Schema is generated under the hood by the library without any way of editing it as developer. This way it's not possible to have mutations.
The reason for this is in GraphQLSchemaBuilder
.getGraphQLSchema()`:
public GraphQLSchema getGraphQLSchema() {
GraphQLSchema.Builder schemaBuilder = GraphQLSchema.newSchema();
schemaBuilder.query(getQueryType());
return schemaBuilder.build();
}
This method is invoked by GraphQLExecutor.createGraphQL()
:
@PostConstruct
protected void createGraphQL() {
if (entityManager != null)
this.graphQL = new GraphQL(new GraphQLSchemaBuilder(entityManager).getGraphQLSchema());
}
For testing I've changed this locally so that the GraphQLSchemaBuilder
now also gets a "schemaEnhancer" function which can be used to "enhance" the schema. The function has the signature BiConsumer<GraphQLSchema.Builder, EntityManager>
:
public GraphQLSchemaBuilder(EntityManager entityManager, BiConsumer<GraphQLSchema.Builder, EntityManager> schemaEnhancer) {
this.entityManager = entityManager;
this.schemaEnhancer = schemaEnhancer;
}
public GraphQLSchema getGraphQLSchema() {
GraphQLSchema.Builder schemaBuilder = GraphQLSchema.newSchema();
schemaBuilder.query(getQueryType());
if(schemaEnhancer != null) {
schemaEnhancer.accept(schemaBuilder, entityManager);
}
return schemaBuilder.build();
}
This way I can provide a function to manipulate the schema after the query type is generated but before the actual schema is build. This way I can add mutations like this:
public class Mutation {
public void addMutation(GraphQLSchema.Builder schemaBuilder, EntityManager entityManager) {
GraphQLObjectType mutation = newObject()
.name("Mutation")
.field(createMyMutation(entityManager))
.build();
schemaBuilder.mutation(mutation);
}
private GraphQLFieldDefinition createMyMutation(EntityManager entityManager) {
return newFieldDefinition()
.name("myMutation")
.type(new GraphQLTypeReference("Person"))
.argument(newArgument()
.name("name")
.type(new GraphQLNonNull(GraphQLString))
.build())
.dataFetcher(env -> {
String name = env.getArgument("name");
Person person = new Person(name);
return entityManager.merge(person);
}).build();
}
}
// Spring Boot
@Configuration
public class DiSetup {
@Bean
public GraphQLExecutor graphQLExecutor(EntityManager entityManager, Mutation mutation) {
return new GraphQLExecutor(entityManager, mutation::addMutation);
}
}
I need to pass through the enhancer function to the GraphQLExecutor
and from there to the GraphQLSchemaBuilder
. In my test project I'm using Spring Boot and therefore have to configure the dependency injection accordingly.
It works for me and I would be happy to provide a PullRequest. However, I'd like to discuss the approach beforehand because I'm not sure if this approach would work for environments other then Spring Boot and I'm not sure if the approach of passing an enhancer function is the best possible way. Maybe there is a better way that better uses dependency injection mechanisms?
EDIT: Another aspect is that there may be use cases where the developer likes to add or manipulate the query part of the Schema. At the moment this isn't possible and it wouldn't be possible with my proposes solution either because the creation of the query part of the Schema is hard coded. Maybe we could find a more flexible way of defining these kinds of things.
Can we start working on this? It would be fantastic to have support of mutations.
I've spend some time thinking about this topic and I think the approach with the "SchemaEnhancer" function that I've proposed in the opening comment has some drawbacks and in my opinion we should think about another solution instead. In my opinion we have 2 major problems with the existing implementation:
- It doesn't use proper dependency injection. Instead the
GraphQLExecutor
creates it's own instance ofGraphQLSchemaBuilder
. This makes it hard to extend the functionality from the outside. - The
GraphQLSchemaBuilder
class has a wrong name and a wrong API in comparison to what it actually does. The only real responsibility of the class is to build a GraphQL query field definition for all JPA entities. So maybe a better name would be for example "JpaGraphQLQueryProvider".
What this class generates is only a (small) part of the actual GraphQL schema.
Let's think about this idea a little further: There could be other "QueryProviders" that generate query field definitions that are then put together to form the actual query type in the root of the schema. Similar to that there could be multiple "MutationProviders" implemented by the user of the library that form the mutation type of the schema.
With this approach we would need a new GraphQLSchemaBuilder
class that takes all available QueryProviders and MutationProviders and creates the actual schema. A similar approach is used in the graphql-java-servlet project.
With this type of API the user could define his mutations and also additional query types. There could even be third party libraries (like some Spring Boot magic) that creates the mutation definition based on some annotations.
The big question for me is: How do you wire these classes up at runtime? This could be implemented with some CDI magic that looks for all available implementations of QueryProvider
and MutationProvider
interfaces at runtime. But this would need a dependency to CDI which would be problematic in Spring projects.
At the moment I don't have a good idea of how this could me implemented.
Any update on this?
I created a pull request which resolved this. It's a temporary fix, but it should pave the way for better, more long-term solutions. To create a mutation you can write:
GraphQLExecutor executor = ...
GraphQLObject mutation = ...
GraphQLSchema.Builder builder = executor.getBuilder().mutation(mutation)
executor.updateSchema(builder)