jcrygier / graphql-jpa

JPA Implementation of GraphQL (builds on graphql-java)
MIT License
165 stars 46 forks source link

Allow Schema manipulation to support mutations #19

Open manuel-mauky opened 7 years ago

manuel-mauky commented 7 years ago

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.

mkuzmentsov commented 7 years ago

Can we start working on this? It would be fantastic to have support of mutations.

manuel-mauky commented 7 years ago

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:

  1. It doesn't use proper dependency injection. Instead the GraphQLExecutor creates it's own instance of GraphQLSchemaBuilder. This makes it hard to extend the functionality from the outside.
  2. 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.

szantogab commented 7 years ago

Any update on this?

Jegp commented 7 years ago

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)