leangen / graphql-spqr

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

Question: Extending an InputObject type and handle it in SPQR #480

Closed Balf closed 3 months ago

Balf commented 8 months ago

Hi @kaqqao,

I have another question:

The GraphQL API I'm working on requires me to register types dynamically (see issue 475) in addition to hardcoded types. All these types extend the same interface, in the example schema below that would be IAnimal.

In the schema is a query that can return a list of the types that implement that interface, which I have implemented like this:


@GraphQLQuery(name = "animals")
public List<IAnimal> getAnimals(@GraphQLArgument(name = "Filter") Filter filter) {
     // the business logic that retrieves and returns the animals
}

//Filter class
public class Filter {
    private String name;

    private int numberOfLegs;

   // getters and setters for both
}

this generates the following entries in the schema (I've left out the irrelevant parts):


type Query {
    animals(Filter: FilterInput): [IAnimal]
}

input FilterInput {
    name: String
    numberOfLegs: Int
}

So far so good, but now it becomes complicated. It might be that one of the dynamically created types contains a field definition that is not available on the IAnimal interface. Lets take the Llama example from issue 475 again.

public class Llama implements IAnimal {
    public String getName() {
        return "Llama";
    }
    @GraphQLQuery
    public int getNumberOfLegs() {
        return 4;
    }
    @GraphQLQuery
    public boolean doesSpit() {
       return true;
    }
}

A new field is registered for the Llama type in the schedule and I can retrieve that via the ... on Llama method. Once again, so far so good. But what I want to achieve is that the Filter input type is updated to include the doesSpit field.

Question 1

I know that you can extend a type in GraphQL using the extend keyword, so that the schema would look like this:

input FilterInput {
    name: String
    numberOfLegs: Int
}

extend input FilterInput {
   doesSpit: Boolean
}

How can I achieve this in GraphQL SPQR and is this the best solution?

Question 2

The animals query implementation from the example above has a Filter Java class as it's argument. That class obviously does not know the new doesSpit field, as it is a dynamically generated field in the Filter Input definition. So I'm assuming that this implementation of the query will not work, because the mapping of the Filter input type to the Filter java class will fail.

Is there a way around this? I'm actually not particularly picky about the type of object that the getAnimals method actually receives. A Map with the key and the corresponding value would be perfectly fine. What is important is that the Filter type is explicitly available in the schema so that any consumers of the API are aware of which fields are available to filter on.

Is this possible with GraphQL SPQR, and if so how, or do I need to use GraphQL Java directly to achieve this?

Balf commented 7 months ago

@kaqqao I'm currently thinking I might need to create the actual query and the relevant types, including the datafetchers and type resolvers etc, directly in GraphQL Java, rather than SPQR to get this to work.

Is there a way to register a GraphQLFieldDefinition to the root Query type using SPQR?

Balf commented 7 months ago

Update

I came up with a sort of workable solution, which I think is good enough to keep me going forward for now. The filter class now looks like this:


@GraphQLQuery(name = "animals")
public List<IAnimal> getAnimals(@GraphQLArgument(name = "Filter") Filter filter) {
     // the business logic that retrieves and returns the animals
}

//Filter class
public class Filter {
    private String name;

    private int numberOfLegs;

    //new
    private Map<String, Object> customFields;

    @JsonAnyGetter
    public Map<String, Object> getCustomFields() {
        return customFields;
    }

    @JsonAnySetter
    public void setCustomFields(String key, Object value) {
        this.customFields.put(key, value);
    }

   // other getters and setters
}

In addition I "overwrite" the Filter input type in the schema, by providing an additional type with the same name, like so:

private GraphQLInputType getFilterType() {
        return GraphQLInputObjectType.newInputObject()
                .name("FilterInput")
                .field(
                        newInputObjectField()
                                .name("name")
                                .type(GraphQLString)
                                .build())
                .field(
                        newInputObjectField()
                                .name("numberOfLegs")
                                .type(GraphQLInt)
                                .build()
                )
                .field(
                        newInputObjectField()
                                .name("doesSpit")
                                .type(GraphQLBoolean)
                                .build()
                )
                .build();
    }

I then register this type to the SchemaGenerator using the withAdditionalTypes method. This results in a Filter object containing the doesSpit in the customFields map.

But if you know a better solution I would still like to hear it ofcourse.

Balf commented 3 months ago

Ah, I see this is still open: I actually figured out another way to do it: Changing a schema