-
Notifications
You must be signed in to change notification settings - Fork 183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature Request: Add support for GraphQLOutputType to the SchemaTransformer #491
Comments
If you want to customize how GraphQL types are produced, you can provide your own custom |
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:
Now the dynamic property needs to be attached to the Dog type, so that the end result looks like this:
How would I achieve this using a TypeMapper? |
You don't even need a //In any registered class
@GraphQLQuery
public int numberOfPaws(@GraphQLContext Dog dog) {
return ...; //any dynamic logic
} |
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. |
Aah, I see. So you have to decide the name/type of the field (s) at runtime, when the schema is being generated? |
Yes, that's correct! |
Ok, yeah, definitely doable with a custom |
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 |
The issue with transforming it from the outside is that you don't have much context available. Imagine e.g. |
That makes sense. I'm going to give it a go using a TypeMapper and I'll let you know if it succeeds |
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. |
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 |
Works like a charm! Thanks for the support! I'll close this issue. |
One thing I see as a needed improvement is to make |
Btw, I'm curious, are there Java methods underlying your custom resolvers ( |
You lost me on the OperationMapper bit 😬, but yes, there are Datafetchers with Java methods underneath. The objects are dynamic, but the way they are created and handled are the same.
|
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 |
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):
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 |
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 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 You need a I can probably help you choose the right approach if you give me more context. |
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:
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:
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;
}
}
|
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. |
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 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. |
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 😅 |
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. |
n.b. I'd like to contribute to this project, perhaps I could assist by writing some documentation on SPQR? |
After deciding this approach was terrible I deleted my code but, since you're curious, I recovered it from IDE history 😅 // 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 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 |
Yes, absolutely! Would be highly appreciated! We can coordinate over email or chat or something if you want. |
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. |
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? |
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 |
Yes!
From within a So while mapping 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. 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)) |
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! 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? |
Huh, I'm not sure I understood this one :/ public Dog getGermanShepard() {...}
public Dog getChihuahua() {...} And you somehow dynamically decide that in one the Or is In other words, what do you encounter in the Java type system when you decide you need to map to |
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 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 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 |
It’s a fun one again 😬. I’ll try and create an example project tonight to describe what I need. That’s clearer than trying to explain it.
|
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}}}"
} |
I got surprisingly rusty at using low-level SPQR 😳 Took me a while to remember what options exist. |
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! |
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 |
As you can see in issue number #490 I've tried to modify my schema using a visitor, leading to issues with the DelegatingTypeResolver.
So great was my joy to find out SPQR implements it's own SchemaTransformer mechanic. However the one thing that isn't supported it mutating GraphQLOutputTypes, something I need for my project.
Could you add support for that?
Kind regards,
Balf
The text was updated successfully, but these errors were encountered: