GithubHelp home page GithubHelp logo

Comments (39)

kaqqao avatar kaqqao commented on June 15, 2024

If you want to customize how GraphQL types are produced, you can provide your own custom TypeMapper. I'll check your other comments to see what exactly you need to do. But nearly 100% of everything SPQR produces is already customizable.

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

Hi @kaqqao

That's great! But I'm not sure on how to accomplish this. What I want to achieve is stated in #490 , but I'll summarize it here. I have a Pojo that is annotated and available in the GraphQL Schema. However I need to attach another field to the GraphQLObjectType, which is based on something a user in our application can configure, meaning that it's not a Java class/property but a dynamic field.

@GraphQLType(name= "Dog")
class Dog {
    private String name = "Bello";

    @GraphQLQuery
    public String getName();
}

Results in:

type Dog {
    name: String
}

Now the dynamic property needs to be attached to the Dog type, so that the end result looks like this:

type Dog {
    name: String
    numberOfPaws: Int
}

How would I achieve this using a TypeMapper?

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

You don't even need a TypeMapper, it sounds. This is enough:

//In any registered class

@GraphQLQuery
public int numberOfPaws(@GraphQLContext Dog dog) {
    return ...; //any dynamic logic
}

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

For a lot of cases this is indeed sufficient, but not for this one, as even the paws field itself is dynamic. It could be paws, tail or teeth depending on what the user configures.

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

Aah, I see. So you have to decide the name/type of the field (s) at runtime, when the schema is being generated?

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

Yes, that's correct!

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

Ok, yeah, definitely doable with a custom TypeMapper. I'll give you an example when I get to the keyboard, on the phone right now.

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

awesome! In addition I just pushed the following commit to a fork I made of GraphQL SPQR to demonstrate what I originally requested: master...Balf:graphql-spqr:mutate-graphqlouttype-via-schematransformer

This change allows me to do this in a custom schema transformer:

@Override
public GraphQLOutputType transformOutputType(GraphQLOutputType type, BuildContext buildContext) {
    if (type instanceof GraphQLObjectType objectType) {
        if (objectType.getName().equals("Dog")) {
            return objectType.transform(builder -> builder.field(
                    newFieldDefinition()
                            .name("paws")
                            .type(GraphQLInt)
                            .build()
            ));
        }
    }
   return type;
}

This actually does do what I want it to, but if a TypeMapper is the way to go, I'll go that route

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

The issue with transforming it from the outside is that you don't have much context available. Imagine e.g. @DateFormat("yy-mm") List<Date> (contrived example, I know). Inside of TypeMapper for Date you can access the "outer" List type and find the annotation that influences the mapping, which you wouldn't be able to do from the outside where you only uave access to the Date object (Dog in your example). You could also change the union or interface members externally, without being able to register them with the rest of SPQR, so it could (as I think you experienced) explode at runtime. TypeMapper gives you access to the required registries, so you can always work around these situations.

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

That makes sense. I'm going to give it a go using a TypeMapper and I'll let you know if it succeeds

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

Here's a quick example:

public class DogMapper extends ObjectTypeMapper {
    @Override
    protected List<GraphQLFieldDefinition> getFields(String typeName, AnnotatedType javaType, TypeMappingEnvironment env) {
        List<GraphQLFieldDefinition> fields = super.getFields(typeName, javaType, env);
        fields.add(GraphQLFieldDefinition.newFieldDefinition() //Append a custom field
                .name("paws")
                .type(GraphQLInt)
                .build());
        // Append a custom resolver
        DataFetcher<?> dataFetcher = e -> ((Dog) e.getSource()).getPaws();
        env.buildContext.codeRegistry.dataFetcher(coordinates(typeName, "paws"), dataFetcher);
        return fields;
    }

    @Override
    public boolean supports(AnnotatedElement element, AnnotatedType type) {
        return ClassUtils.isSuperClass(Dog.class, type);
    }
}

...
generator
    .withOperationsFromSingleton(new DataService())
    .withTypeMappers(new DogMapper())

I'm still puzzled why your visitor example didn't work. In your specific case, I see no issues. Will debug a bit and post my findings on that issue.

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

Thanks! That should do the trick. With regards to the Visitor issue: It seems like the typeRegistry in the GlobalEnvironment is not updated after the Schema is changed with a Visitor. In my example I noticed that the old state, without the added field, was still reflected in the typeRegistry

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

Works like a charm! Thanks for the support! I'll close this issue.

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

One thing I see as a needed improvement is to make OperationMapper#createResolver overload that would be public and useful for situations like this... Because without it, you're left to your own devices for deserializing inputs, invoking interceptors etc in each custom DataFetcher 🤔

#492

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

Btw, I'm curious, are there Java methods underlying your custom resolvers (Datafetchers) or is the logic fully dynamic?
If there's a method underneath, I think I have an even better approach for the above: providing a custom ResolverBuilder. With that approach not even a TypeMapper is needed, and all the usual SPQR behaviors (automatic deserialization, interceptors, converters etc) remain.

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

Ok, then I'm pretty sure I can come up with a better approach from the one above. One that saves you from creating custom DataFetchers and has the usual SPQR magic applied. It's awfully late now though, so I'll post that tomorrow.

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

Nice! The actual Dog type is in practise a bit more complex by the way. As in: the paws object in real life is not an int, but an object in itself, like so (where paws is replaced by breed):

type Dog {
    name: String
    breed: Breed
}

type Breed {
    breedName: String
    characteristics: Characteristic
}

type Characteristic {
    description: String
}

So both the Breed & Characteristic type are dynamically generated and attached to the Dog type. But for all types the data is fetched via DataFetchers

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

I'm having a hard time understanding your situation... If the additional operations you're dynamically adding are indeed backed by Java methods, why can't you get away with just dynamically filtering them? You could do this with a custom InclusionStrategy or, better yet, a custom ResolverBuilder.

public class CustomResolverBuilder extends AnnotatedResolverBuilder {

    @Override
    protected boolean isQuery(Method method, ResolverBuilderParams params) {
        //Expose additional methods on chosen classes
        return method.getName().equals("lookupBreed");
    }

    @Override
    public boolean supports(AnnotatedType type) {
        return ClassUtils.isSuperClass(Dog.class, type);
    }
}

By providing a custom ResolverBuilder, you can control not only if but also how the method gets exposed (you can change anything from the name to arguments, operation type etc). Technically, you can even register additional resolvers here, using only Java types, without dealing with any GraphQL APIs, by building your own Resolver instance and overriding buildQueryResolvers, but that's better done by conditionally registering additional classes with @GraphQLContext methods inside, together with dynamic filtering perhaps.

You need a TypeMapper only if you want to get to the low-level GraphQL APIs, as seen above. You can still use SPQR's mapping machinery from there, e.g. by calling env.operationMapper.toGraphQLField(...) which builds a field and registers a DataFetcher for it. But this is difficult if an appropriate ResolverBuilder hasn't already prepared everything for you. You can also call env.operationMapper.toGraphQLType(...) to fully replace the resulting GraphQL type. This can be useful if the GraphQL type doesn't resemble any Java type, but is it's own thing. This is e.g. how SPQR internally transforms Java Maps to GraphQL Lists.

I can probably help you choose the right approach if you give me more context.

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

I'll try and clear it up a bit, but it's relatively complex to describe, I think. I'll do my best.

In our application we have a content type, which is Dog in the example above. Dog is an actual Java class and annotated using SPQR.

Users of the application can create templates which can be attached to the Dog in the application. A Dog can have 0 or more templates, but only one of each. There is a Java class that represents these templates, which looks a bit like the code below. A template has 1 or more properties which should be retrievable via GraphQL as well. A property can be of several types and has a get(...)Value method for each possible type.

public class Template {
    private String name;

    //this is a Java safe identifier, while the name can be a human readable string
    private String identifier;

    private List<Property>

    //the getters and setters for the items above
}

public class Property {
    //let's assume it can be either an int or a String
    private PropertyType type;

    private String identifier;

    private int intValue= 0;

    private String stringValue = "";

    public int getIntValue();

    public String getStringValue();

    public String getIdentifier();
}

When a user creates a template in the application I want this template and the underlying properties to be available in the GraphQL Schema, under a field named metaData, with type MetaData. The MetaData object is generated using the templates that are present on the Dog object (currently using a TypeMapper, as per your previous suggestion). The MetaData object is nothing more than a GraphQLField wrapping the templates. It's just there for semantic reasons. Each template is in turn a field within the MetaData object. Each template in turn is it's own type with the configured properties as it's field.

Let's assume a user created a template named Address with identifier address and the properties streetname, zipcode and city. That should be represented in the Schema like this:

type Dog {
    name: String
    metaData: MetaData
}

type MetaData {
    address: Address //using the identifier as the field name
}

type Address {
    streetname: String
    zipzode: String
    city: String
}

If a user would create another template called Recipe with identifier recipe and a list of Strings called ingredients, the Schema should reflect this like so:

type Dog {
    name: String
    metaData: MetaData
}

type MetaData {
    address: Address //using the identifier as the field name
    recipe: Recipe
}

type Address {
    streetname: String
    zipzode: String
    city: String
}

type Recipe {
    ingredients: [String]   
}

Based on your TypeMapper suggestion I created the TypeMapper shown below (this is a simplified version of the actual mapper), which actually does do what I want it to. But this solution does require 3 DataFetchers, so if that isn't necessary that would be great. I've added it here because it might make clearer what I'm trying to achieve.

Thanks for your help once again.

public class DogTypeMapper extends ObjectTypeMapper {

    @Override
    protected List<GraphQLFieldDefinition> getFields(String typeName, AnnotatedType javaType, TypeMappingEnvironment env) {
        List<GraphQLFieldDefinition> fields = super.getFields(typeName, javaType, env);

        List<Templates> templates = getTemplates();

        GraphQLCodeRegistry.Builder codeRegistryBuilder = env.buildContext.codeRegistry;

        fields.add(GraphQLFieldDefinition.newFieldDefinition() //Append a custom field
                        .name("metaData")
                        .type(newObject()
                                .name("MetaData")
                                .fields(getTemplateFields(templates, codeRegistryBuilder))
                                .build()
                        )
                        .build());

        codeRegistryBuilder.dataFetcher(FieldCoordinates.coordinates("Dog", "metaData"), new MetaDataFetcher());

        return fields;
    }

    @Override
    public boolean supports(AnnotatedElement element, AnnotatedType type) {
        return ClassUtils.isSuperClass(Dog.class, type);
    }


    private List<GraphQLFieldDefinition> getTemplateFields(List<Template> templates, GraphQLCodeRegistry.Builder codeRegistryBuilder) {

        List<GraphQLFieldDefinition> fields = new ArrayList<>();

        for (Template template : templates) {

            String templateObjectName = StringUtils.capitalize(template.getIdentifier());

        
            GraphQLFieldDefinition fieldDefinition = newFieldDefinition()
                    .name(template.getIdentifier())
                    .type(newObject()
                            .name(templateObjectName)
                            .fields(getTemplatePropertyFields(template, templateObjectName, codeRegistryBuilder))
                            .build())
                    .build();

            codeRegistryBuilder.dataFetcher(FieldCoordinates.coordinates("MetaData", template.getIdentifier()), new TemplateDataFetcher());
            
            fields.add(fieldDefinition);
        }

        return fields;
    }

    private List<GraphQLFieldDefinition> getTemplatePropertyFields(Template template, String templateObjectName, GraphQLCodeRegistry.Builder codeRegistryBuilder) {

        List<GraphQLFieldDefinition> fields = new ArrayList<>();

        for (Property property : template.getProperties()) {

            String type = property.getType();
            GraphQLOutputType dataType;

            switch (type.getValue()) {
                case "int" -> dataType = GraphQLInt;
                default -> dataType = GraphQLString;
            }

            GraphQLFieldDefinition fieldDefinition = newFieldDefinition()
                    .name(property.getIdentifier())
                    .type(dataType)
                    .build();

            codeRegistryBuilder.dataFetcher(FieldCoordinates.coordinates(templateObjectName, property.getIdentifier()),
                    new PropertyDataFetcher());

            fields.add(fieldDefinition);
            
        }
        return fields;
    }
}

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

Looking at your ResolverBuilder solution leads me to think I interpreted your question concerning Java methods backing my DataFetchers incorrectly.

Am I correct in assuming that your ResolverBuilder example would expose a lookupBreed method, which is already present on the Dog class, just not exposed via annotations? If so, I don't think that would work for my situation, as the Dog class does not have explicit methods to retrieve specific templates.

I interpreted your question as: "Do you use Java logic to retrieve the data". Not quite sure why I interpreted it that way :P.

The DataFetchers in my example from my previous comment actually execute some domain specific logic to retrieve the correct template instances, so I think my answer should have been: The logic is fully dynamic.

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

Yup, I get your situation better now! And with that taken into account, I think your solution is pretty much spot-on. The only downside I can think of is that the custom DataFetchers won't automatically call interceptors etc, but I don't think that's an issue in your situation. It would still be nice of SPQR to provide a convenient way to warp such custom DataFetchers into a SPQR-aware layer, so I'll have a look into it.

My idea described above was basically to add some glue classes that could be then used as substitutes for SPQR to map instead of the actual classes you have. For completeness sake, I'll write down an example, but I don't think that approach is better than what you have.

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

Ok, after actually trying the other idea, and writing 3 times more code than in the previous example, I'm very confident it's not worth the bother and you're on the right track with your solution 😅

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

The ResolverBuilder concept seems to be quite interesting, so I look forward to that example. it might come in handy in the future ^^. Thanks for your efforts in support of this issue. It made the inner workings of SPQR quite a bit clearer to me.

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

n.b. I'd like to contribute to this project, perhaps I could assist by writing some documentation on SPQR?

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

After deciding this approach was terrible I deleted my code but, since you're curious, I recovered it from IDE history 😅
So here it is, in all its... whatever this is. Please don't use it for anything except... fun, I suppose 😬

// I made your Template class generic, where T represents the domain type the template applies to,
// e.g. `Template<Dog>`. I'll horribly abuse this fact later.
public class Template<T> {
    private String name;
    private String identifier;
    private List<PropertyInfo> properties;

    ...
}

// Describes a property. Does not contain instance values.
public class PropertyInfo {
    ...
}

//A class to represent an instantiated template, contains the property values for a concrete domain object instance
public record TemplateInstance(List<Property> properties) {

    public Property getProperty(String name) {
        return properties.stream()
                .filter(p -> p.identifier().equals(name))
                .findFirst()
                .orElseThrow();
    }
}

// An instance of a property. Contains values for a concrete instance of a domain object
public record Property(String identifier, int intValue, String stringValue) {}

public record PropertyType(String value) {}

//Maps domain types to the applicable templates.
//Also maps domain instances to template instances.
public static class MetaDataRegistry {
    //Keyed by a potentially generic Type. I never make use of this, but you could register different templates for
    //e.g. Box<Chocolate> and Box<Kitten> types
    private final Map<? extends Type, List<Template<?>>> templates;

    public <T> MetaDataRegistry(Map<Class<?>, List<Template<?>>> templates) {
        this.templates = templates;
    }

    public List<Template<?>> getTemplates(Type type) {
        return templates.getOrDefault(type, Collections.emptyList());
    }

    public Template<?> getTemplate(Type type, String templateName) {
        return getTemplates(type).stream()
                .filter(t -> t.identifier.equals(templateName))
                .findFirst()
                .orElseThrow();
    }

    public TemplateInstance getTemplateInstance(Object instance, String template) {
        //I'm pretending here that all objects (e.g. Dogs) have the same property values
        //Read the actual properties as appropriate
        return new TemplateInstance(List.of(
                new Property("streetname", 0, "Starlane"),
                new Property("zipcode", 0, "XX123"),
                new Property("city", 0, "Moonbeam City")
        ));
    }
}

// Fetches the correct template instance for the given domain object
// Executable is supposed to represent a method or field used to fetch the value of a field.
// This is why I asked if you have an "underlying method" for your extensions.
// But when I learned that you do not, and have to _fully dynamically_ load the values, I still wanted to
// see if I can abuse this mechanism anyway. Purely for the... errrrr... "intellectual pursuit" 👀
public class TemplateInstanceFetcher extends Executable {

    private final MetaDataRegistry registry;
    private final String templateName;

    private static final AnnotatedType RETURN_TYPE = GenericTypeReflector.annotate(TemplateInstance.class);

    public TemplateInstanceFetcher(MetaDataRegistry registry, String templateName) {
        this.registry = registry;
        this.templateName = templateName;
    }

    @Override
    public Object execute(Object target, Object[] args) throws InvocationTargetException, IllegalAccessException {
        return registry.getTemplateInstance(target, templateName);
    }

    @Override
    public AnnotatedType getReturnType() {
        return RETURN_TYPE;
    }

    @Override
    public int getParameterCount() {
        return 0;
    }

    @Override
    public AnnotatedType[] getAnnotatedParameterTypes() {
        return new AnnotatedType[0];
    }

    @Override
    public Parameter[] getParameters() { //This is old API, likely to soon change
        return new Parameter[0];
    }

    @Override
    public int hashCode() {
        return 1; //Terrible hack. But would explode if not overridden. As it expects a method or field to exist.
    }

    @Override
    public boolean equals(Object that) {
        return false; //Same as above
    }
}

//This ResolverBuilder appends the fields contributed by the templates, e.g. address
public class TemplateTypeResolverBuilder extends PublicResolverBuilder {

    private final MetaDataRegistry metaDataRegistry;

    public TemplateTypeResolverBuilder(MetaDataRegistry metaDataRegistry) {
        this.metaDataRegistry = metaDataRegistry;
    }

    @Override
    public Collection<Resolver> buildQueryResolvers(ResolverBuilderParams params) {
        Class<?> rawType = ClassUtils.getRawType(params.getBeanType().getType());
        return metaDataRegistry.getTemplates(rawType).stream()
                .map(t -> {
                    try {
                        //Synthetic GraphQLType annotation. Abused to pass the name of the template to the downstream ResolverBuilder,
                        //and, conveniently, to control the output GraphQL type name (which could have been done differently)
                        //Terrible, awful hackery. Don't do it for real if other choices exist.
                        GraphQLType graphQLType = TypeFactory.annotation(GraphQLType.class, Map.of("name", t.getIdentifier()));
                        //Synthetic Template<DomainType> e.g. Template<Dog> type instance.
                        //Used by the downstream ResolverBuilder to know what domain type it's working with.
                        AnnotatedType fieldType = TypeFactory.parameterizedAnnotatedClass(Template.class, new Annotation[]{graphQLType}, params.getBeanType());
                        return new Resolver(t.getIdentifier(), t.getName(), null, false,
                                new TemplateInstanceFetcher(metaDataRegistry, t.getIdentifier()), new TypedElement(fieldType), emptyList(), null);
                    } catch (AnnotationFormatException e) {
                        throw new RuntimeException(e);
                    }
                })
                .collect(Collectors.toList());
    }
}

//This resolver builder appends the fields defined by the properties of a template e.g. zipcode
public class SyntheticPropertiesResolverBuilder extends PublicResolverBuilder {

    private final MetaDataRegistry registry;

    public SyntheticPropertiesResolverBuilder(MetaDataRegistry registry) {
        this.registry = registry;
    }

    @Override
    public Collection<Resolver> buildQueryResolvers(ResolverBuilderParams params) {
        //Extracts the domain type e.g. Dog, from the Template<Dog> type we synthesized earlier
        Type type = ((ParameterizedType) params.getBeanType().getType()).getActualTypeArguments()[0];
        //Extracts the template name from the @GraphQLType annotation we synthesized earlier
        String templateName = params.getBeanType().getAnnotation(GraphQLType.class).name();
        return registry.getTemplate(type, templateName).getProperties().stream()
                .map(this::toResolver)
                .collect(Collectors.toList());
    }

    private Resolver toResolver(PropertyInfo p) {
        AnnotatedType javaType;
        switch (p.getType().value()) {
            case "int": javaType = GenericTypeReflector.annotate(int.class); break;
            default: javaType = GenericTypeReflector.annotate(String.class); break;
        }
        return new Resolver(p.getIdentifier(), "", null, false,
                new PropertyValueFetcher(p.getIdentifier(), p.getType(), javaType), new TypedElement(javaType), emptyList(), null);
    }

    @Override
    public boolean supports(AnnotatedType type) {
        return ClassUtils.isSuperClass(Template.class, type);
    }
}

//Fetches the value of the property from a TemplateInstance.
//Similar story to the Executable from above regarding the hackery.
private class PropertyValueFetcher extends Executable {

    private final String propertyName;
    private final PropertyType type;
    private final AnnotatedType javaType;

    private PropertyValueFetcher(String propertyName, PropertyType type, AnnotatedType javaType) {
        this.propertyName = propertyName;
        this.type = type;
        this.javaType = javaType;
    }

    @Override
    public Object execute(Object target, Object[] args) throws InvocationTargetException, IllegalAccessException {
        Property property = ((TemplateInstance) target).getProperty(propertyName);
        Object value;
        switch (type.value()) {
            case "int": value = property.intValue(); break;
            default: value = property.stringValue(); break;
        }
        return value;
    }

    @Override
    public AnnotatedType getReturnType() {
        return javaType;
    }
   
    ... // Same boilerplate as above
}

So after all that nonsense, we can generate a schema:

GraphQLSchemaGenerator generator = new GraphQLSchemaGenerator();
//Just some dummy templates and property values
MetaDataRegistry registry = new MetaDataRegistry(Map.of(Dog.class, List.of(new Template("Address", "address", List.of(
        new PropertyInfo(new PropertyType("String"), "streetname"),
        new PropertyInfo(new PropertyType("String"), "zipcode"),
        new PropertyInfo(new PropertyType("String"), "city")
)))));
generator
        .withOperationsFromSingleton(new DataService())
        .withNestedResolverBuilders((config, builders) -> builders
                .append(new TemplateTypeResolverBuilder(registry))
                .append(new SyntheticPropertiesResolverBuilder(registry)));
GraphQLSchema spqrSchema = generator.generate();
GraphQL spqr = GraphQL.newGraphQL(spqrSchema).build();

With this, you'd get a GraphQLType such as:

type Dog {
    name: String
    address: Address
}

type Address {
    streetName: String
    zipCode: String
}

No clue why I ended up adding an address to the dog, but I suppose it makes sense 🙈

As you'll notice, all that mess and I still conveniently omitted adding the metaData field (among other glitches). So yeah, it's a horrible approach, don't ever go near it, but I hope you found the involved type-level acrobatics interesting 😅

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

n.b. I'd like to contribute to this project, perhaps I could assist by writing some documentation on SPQR?

Yes, absolutely! Would be highly appreciated! We can coordinate over email or chat or something if you want.

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

I made some updates to the code block above, if you're reading that from your email client, might be worth seeing the updated version on the web.

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

haha, wow, it seems that the TypeMapper solution is a bit less code :P. But an interesting read nonetheless.

Concerning documentation: Can I reach you on the e-mailaddress listed in your Github profile?

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

Another small question in close relation with this. One of the properties a template can have is an actual custom Java object which I can annotate. It's currently not present in the schema. What is the best way to get this in the schema?

To illustrate this, using the example above with the Dog's Address. Let's say the zipcode part of the Address is an actual Java class:

@GraphQLType(name = "ZipCode",  description = "the zipcode belonging to an address")
public class ZipCode {
   private int numbers = 1234;

   private String letters = "AB"

  @GraphQLQuery 
  public int getNumbers();

  @GraphQLQuery
  public String getLetters();

}

I want the corresponding type to be available in the schema so I can reference it using a GraphQLTypeReference

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

Concerning documentation: Can I reach you on the e-mailaddress listed in your Github profile?

Yes!

What is the best way to get this in the schema?

From within a TypeMapper you can delegate to SPQR at any point via env.toGraphQLType(javaType) (you can also reach into other env.operationMapper.toGraphQL... methods).

So while mapping Dog, you can always do something like:

fields.add(GraphQLFieldDefinition.newFieldDefinition() //Append a custom field
                        .name("address")
                        .type(env.toGraphQLType(GenericTypeReflector.annotate(Address.class)))
                        .build());

SPQR will then correctly use references to avoid mapping the same type twice and to break endless recursions.
Does that suffice?

If, on the other hand, you want to register interface implementations that would never otherwise be discovered, you can manually add them to the default strategy:

generator.withImplementationDiscoveryStrategy(new DefaultImplementationDiscoveryStrategy()
        .withAdditionalImplementations(SecretItemSubClass.class, ObscureItemSubClass.class))

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

Once again! Like a charm! I've used the first example in this case. The ImplementationStrategy tactic I use as well to tackle a different usecase, to combat limitations when working in an OSGi like environment. Very powerful stuff!
Now I've got another one :P (Just let me know if I'm being greedy :P ).

Let's stay with the Dog example. The issue is similar to the one we discussed earlier, but not entirely so. So there is the Dog class:

public class Dog {
    private String name;
    private String sex;

   public String getName() {
        return name;
   }
   
    public void setName(String name) {
        this.name = name;
    }
   public String getSex() {
        return name;
   }
   
    public void setSex(String sex) {
        this.sex = sex;
    }
}

Every dog can have a template attached with a list of properties, same as above. There might be several instances of Dog, each with a different template attached to it. Now what I would like is that each instance of Dog is available as it's own type in the GraphQL schema. So let's say there are two instances of the Dog class, one with the template Chihuahua and one with the template GermanShephard.

The Chihuahua template has 1 boolean property with the name/identifier isTiny and the GermanShephard template has a boolean property isGuardDog. I would like these reflected in the schema as:

type Dog {
    name: String
    sex: String
}

type Chihuahua {
    name: String
    sex: String
    isTiny: Boolean  
}

type GermanShephard {
    name: String
    sex: String
    isTiny: Boolean  
}

I would like both GermanShephard and Chihuahaha mapped to the Dog class, since both types are both just Dogs with a template attached and then provide custom DataFetchers for the additional fields as in my TypeMapper from earlier.

A TypeMapper would allow me to map a single class to a single GraphQLType. What (I think) I'm looking for is a way to map multiple types to a single Java class. I realize I will also need to build a custom TypeResolver to resolve any returned Dog's to the correct GraphQLType. What would be the best way to achieve this?

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

Huh, I'm not sure I understood this one :/
What does the underlying Java code look like?
Are these Dog subtypes returned from different methods directly, e.g.

public Dog getGermanShepard() {...}

public Dog getChihuahua() {...}

And you somehow dynamically decide that in one the Dog is mapped one way and differently in another? If so, how do you decide? It must be some static information you're relying on, in which case you could implement multiple TypeMappers to begin with (with different supports implementations using that same static info to decide).

Or is Dog an interface and the subtypes are discovered automatically? Your example doesn't suggest any relationship between any of these types. And since the subtypes don't actually exist in the Java type system, it seems to me GermanShepard and Chihuahua are just floating in the GraphQL schema, without anything being able to consume/produce them.

In other words, what do you encounter in the Java type system when you decide you need to map to Chihuahua and GermanShepard?

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

If all these types are indeed unrelated, they can only be useful in a union. In that case, make a type mapper that when a Dog is encountered produces a GraphQLUnion, and register all these dynamically created types as possible value types for that union.

See what I mean? A GraphQL type is either used directly, in which case you must have static information to decide how to map it from your Java type (and TypeMapper is where this decision is made). Or you are returning a GraphQL interface or union for a Java type, in which case your TypeMapper can add all the implementations/union members that would otherwise not be discovered, and they'll be in the schema. The only remaining case would be to have unreachable types in the schema, which is possible but not useful.

You could hold a mutable list that you pass around to various TypeMappers into which they can add extra types, and you then pass that list to generator.withAdditionalTypes() but, given the above, I can't come up with any situation where that is useful.

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

I've created a sample project, based on the spring boot sample, that does want I want: a Dog with a template attached is represented in the GraphQL Schema as being it's own type. The interface/class structure in the project might not seem optimal, but it closely reflects the actual situation and is something I can not change unfortunately.

There is one major maintenance problem with it, which you can see in the GraphQLSampleController within the attached project: In the GermanShepard type definition I'm redeclaring fields that are already annotated on the Dog class. If any modifications ever happen on the Dog class they will need applied to the definition of the GermanShepard type too, which leaves room for errors and adds extra maintenance complexity.

Then there is another smaller problem: In the generated schema you will find a DogWithTemplate type, based on the Java interface with the same name. This type is not useful as any dogs with templates will be reflected in the schema as their own specific type, so I'd like to ignore it. However when I ignore it via the @GraphQLIgnore annotation it breaks the TypeResolver.

When creating this example I realised I'm not explicitly mapping the types to certain Java classes here, so my original question was wrong (again :P). The maintenance problem is really the most important challenge I'm trying to solve, as the real world equivalent of the Dog interface does not have 2 fields but more like 20 :P.

Sample project: spring-boot-sample.zip

Working example query:

{
    "query" : "query { dogs { name ... on ChiHuaHua { fitsInAHandBag } ... on GermanShepard { foodPreference} } dog(name: \"Bello\" ) { name ... on GermanShepard { foodPreference}}}"
}

from graphql-spqr.

kaqqao avatar kaqqao commented on June 15, 2024

I got surprisingly rusty at using low-level SPQR 😳 Took me a while to remember what options exist.
Anyway, here's what I came up with. In short, a custom TypeMapper that adds all discovered implementations as well.
To avoid duplicating field definitions, I simply copied over the fields and fetchers from the interface. It's a bit of a hack, but does the job 👀
But, still, I'm only semi-certain I understood your situation properly 😅 So take my code as inspiration, rather than gospel.

spring-boot-sample.zip

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

I think this would actually fit what I need to do perfectly, and it's definitely more elegant than what I had so far, thanks!

from graphql-spqr.

Balf avatar Balf commented on June 15, 2024

In relation to this: could we make registerImplementations method in the InterfaceMapper.java protected rather than private? I've got a map an interface that is implementen by both Java and virtual types, making it protected would enable me to use it in my own TypeMapper.

Update, nevermind, your example already fixed that, sorry :D

from graphql-spqr.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.