-
Notifications
You must be signed in to change notification settings - Fork 177
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
Attribute naming strategy? #32
Comments
Yup, this is really easily done. The interesting interfaces are Here's a quick example customizing both the operation and argument name generation, on all levels: //Delegating generator configured to first try finding an explicit @GraphQLQuery or
//@GraphQLMutation annotation and use the name from there, if not found - use
//the custom name generator, which in your case could generate snake case names
OperationNameGenerator nameGenerator = new DelegatingOperationNameGenerator(new AnnotatedOperationNameGenerator(),
new OperationNameGenerator() {
//when a method is exposed as a query
@Override
public String generateQueryName(Method queryMethod, AnnotatedType declaringType, Object instance) {
return getSnakeCaseName(queryMethod);
}
//when a field is exposed as a query
@Override
public String generateQueryName(Field queryField, AnnotatedType declaringType, Object instance) {
return getSnakeCaseName(queryField);
}
//when a method is exposed as a mutation
@Override
public String generateMutationName(Method mutationMethod, AnnotatedType declaringType, Object instance) {
return getSnakeCaseName(mutationMethod);
}
});
AnnotatedArgumentBuilder argumentBuilder = new AnnotatedArgumentBuilder(new DefaultTypeTransformer(false, false)) {
@Override
protected String getArgumentName(Parameter parameter, AnnotatedType parameterType) {
return getSnakeCaseName(parameter);
}
};
//Customize the built-in resolver builders to use the implementations from above
FilteredResolverBuilder customAnnotated = new AnnotatedResolverBuilder()
.withOperationNameGenerator(nameGenerator)
.withResolverArgumentBuilder(argumentBuilder);
FilteredResolverBuilder customBean = new BeanResolverBuilder("your.base.package")
.withOperationNameGenerator(nameGenerator)
.withResolverArgumentBuilder(argumentBuilder);
GraphQLSchema schema = new GraphQLSchemaGenerator()
.withOperationsFromSingleton(new Service())
.withResolverBuilders(customAnnotated) //use the custom builder for top-level operations
.withNestedResolverBuilders(customAnnotated, customBean) //use the custom builders for nested operations
.generate(); Notice how you can have different configurations for the top-level queries and mutations and the nested queries/fields. You can also use the custom strategy per bean/class, e.g. //only use the custom strategy for this bean
.withOperationsFromSingleton(new Service(), customAnnotated) Instead of Does this help? |
Getting this exception now:
|
It seems like DelegatingOperationNameGenerator isn't catching the exception from AnnotatedOperationNameGenerator. edit: Our models are not annotated with any sort of GraphQL annotations. This worked fine for camelCasedAttributes. The classes we are passing to |
Yup, it's common to not have any annotations on the model. //create snakeCaseNameGenerator and snakeCaseArgumentBuilder as above
ResolverBuilder customBean = new BeanResolverBuilder("your.base.package")
.withOperationNameGenerator(snakeCaseNameGenerator)
.withResolverArgumentBuilder(snakeCaseArgumentBuilder); //drop this if you don't need to customize argument names
GraphQLSchema schema = new GraphQLSchemaGenerator()
.withOperationsFromSingleton(new Service())
.withNestedResolverBuilders(customBean)
.generate(); That should take care of it. |
Ok, inbound snake_cased parameters are working. As are queries that return an integer or other primitives. However queries that return an object or an array of objects are still throwing validation errors, even for attributes like "id" that are the same in camelCase and snake_case. This service method: @GraphQLQuery(name = "shopify_tags")
public List<ShopifyTag> getByApplicationIdAndEntityType(
@GraphQLArgument(name = "application_id") String applicationId,
@GraphQLArgument(name = "entity_type") String entityType) {
return repo.getShopifyTagList(applicationId, EntityType.valueOf(entityType));
} With this query: Returns: {"errors":[{"validation_error_type":"FieldUndefined","message":"Validation error of type FieldUndefined: Field id is undefined","locations":[{"line":1,"column":75}],"error_type":"ValidationError"},{"validation_error_type":"FieldUndefined","message":"Validation error of type FieldUndefined: Field application_id is undefined","locations":[{"line":1,"column":79}],"error_type":"ValidationError"},{"validation_error_type":"FieldUndefined","message":"Validation error of type FieldUndefined: Field entity_type is undefined","locations":[{"line":1,"column":95}],"error_type":"ValidationError"},{"validation_error_type":"FieldUndefined","message":"Validation error of type FieldUndefined: Field name is undefined","locations":[{"line":1,"column":108}],"error_type":"ValidationError"}],"extensions":null} |
Here's how the schema is being created: AnnotatedArgumentBuilder argumentBuilder = new AnnotatedArgumentBuilder() {
@Override
protected String getArgumentName(Parameter parameter, AnnotatedType parameterType) {
return LOWER_CAMEL.to(LOWER_UNDERSCORE, parameter.getName());
}
};
FilteredResolverBuilder customBean = new BeanResolverBuilder("com.company")
.withOperationNameGenerator(new SnakeCaseOperationNameGenerator())
.withResolverArgumentBuilder(argumentBuilder);
GraphQLSchemaGenerator generator = new GraphQLSchemaGenerator();
services.forEach(generator::withOperationsFromSingleton);
generator.withNestedResolverBuilders(customBean);
generator.withValueMapperFactory(abstractTypes -> new JacksonValueMapper(MAPPER));
generator.withScalarMappingStrategy(new MapScalarStrategy());
schema = generator.generate(); I've set breakpoints in |
So simply updating the |
I'm getting really close. FilteredResolverBuilder customBean = new BeanResolverBuilder("com.company")
.withOperationNameGenerator(new SnakeCaseOperationNameGenerator());
GraphQLSchemaGenerator generator = new GraphQLSchemaGenerator();
services.forEach(generator::withOperationsFromSingleton);
generator.withNestedResolverBuilders(customBean);
generator.withValueMapperFactory(abstractTypes -> new JacksonValueMapper(MAPPER));
generator.withScalarMappingStrategy(new MapScalarStrategy());
schema = generator.generate(); and the important part of @Override
public String generateQueryName(Method queryMethod, AnnotatedType declaringType, Object instance) {
GraphQLQuery annotation = queryMethod.getAnnotation(GraphQLQuery.class);
if (annotation != null) {
return annotation.name();
}
return LOWER_CAMEL.to(LOWER_UNDERSCORE, getFieldNameFromGetter(queryMethod));
} |
Sure, setting annotation explicitly will always work, but I was under an impression this was something you didn't want to do. That's why I was suggesting configuring it from the outside. What's still not working for you? Can you give me an example class that gets mapped incorrectly? |
Everything is working now. I had some minor tests to cleanup. The models are not annotated, but the service methods are. Actually it seems like this: @Override
public String generateQueryName(Method queryMethod, AnnotatedType declaringType, Object instance) {
return LOWER_CAMEL.to(LOWER_UNDERSCORE, getFieldNameFromGetter(queryMethod));
} is sufficient now that |
Glad you sorted it out 👍 |
I found this thread very useful. Thanks for sharing @efenderbosch and @kaqqao . My aim was very similar to @efenderbosch 's: I've got models without explicit graphql annotations and service methods that are annotated. I'll post my full final solution here in case this is useful for anyone: import io.leangen.graphql.metadata.strategy.query.OperationNameGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.Introspector;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static com.google.common.base.CaseFormat.*;
/**
* A snake case name generator based on the ideas discussed in https://github.com/leangen/graphql-spqr/issues/32
*
*/
public class SnakeCaseOperationNameGenerator implements OperationNameGenerator {
private static final Logger logger = LoggerFactory.getLogger(SnakeCaseOperationNameGenerator.class);
@Override
public String generateQueryName(Method queryMethod, AnnotatedType declaringType, Object instance) {
return LOWER_CAMEL.to(LOWER_UNDERSCORE, getFieldNameFromGetter(queryMethod));
}
//when a field is exposed as a query
@Override
public String generateQueryName(Field queryField, AnnotatedType declaringType, Object instance) {
return LOWER_CAMEL.to(LOWER_UNDERSCORE, queryField.getName());
}
//when a method is exposed as a mutation
@Override
public String generateMutationName(Method mutationMethod, AnnotatedType declaringType, Object instance) {
return LOWER_CAMEL.to(LOWER_UNDERSCORE, getFieldNameFromGetter(mutationMethod));
}
@Override
public String generateSubscriptionName(Method subscriptionMethod, AnnotatedType declaringType, Object instance) {
return LOWER_CAMEL.to(LOWER_UNDERSCORE, getFieldNameFromGetter(subscriptionMethod));
}
/**
* Retrieve the field name from the getter Method (starting with "get" or "is")
* @param method
* @return
*/
private static String getFieldNameFromGetter(Method method) {
// implemented from ideas in : https://stackoverflow.com/questions/13192734/getting-a-property-field-name-using-getter-method-of-a-pojo-java-bean/13514566
String methodName = method.getName();
if (methodName.startsWith("get")) {
return Introspector.decapitalize(methodName.substring(3));
} else if (methodName.startsWith("is")) {
return Introspector.decapitalize(methodName.substring(2));
} else {
logger.warn("Method does not start with get or is, using the mehod name '{}' directly", methodName);
return methodName;
}
}
} and then my graphql endpoint looks like this: import javax.servlet.annotation.WebServlet;
import graphql.schema.GraphQLSchema;
import graphql.servlet.SimpleGraphQLServlet;
import io.leangen.graphql.GraphQLSchemaGenerator;
import io.leangen.graphql.metadata.strategy.query.BeanResolverBuilder;
import io.leangen.graphql.metadata.strategy.query.FilteredResolverBuilder;
import Query;
import SnakeCaseOperationNameGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@WebServlet(urlPatterns = "/graphql")
public class GraphQLEndpoint extends SimpleGraphQLServlet {
private static final Logger logger = LoggerFactory.getLogger(GraphQLEndpoint.class);
public GraphQLEndpoint() {
super(new Builder(buildSchema()));
}
private static GraphQLSchema buildSchema() {
logger.info("Initialising graphql schema");
// see https://github.com/leangen/graphql-spqr/issues/32 for the recipe I followed to snake case the schema
FilteredResolverBuilder customBean = new BeanResolverBuilder("org.path.to.my.models.package")
.withOperationNameGenerator(new SnakeCaseOperationNameGenerator());
Query query = new Query();
GraphQLSchema schema = new GraphQLSchemaGenerator()
.withOperationsFromSingleton(query)
.withNestedResolverBuilders(customBean) //use the custom builders for nested operations
.generate();
return schema;
}
} |
Cool! Glad you got it figured out. Thanks for sharing the solution :) |
We are migrating from REST to GraphQL and the front end code is all setup w/ snake_case attributes. Is there a way to configure spqr to create the schema to resolve attributes named this way? Right now we are getting UnknownArgument errors for attributes like campaign_id vs campaignId.
edit: I've fixed the UnknownArgument, but now I'm getting FieldUndefined.
The text was updated successfully, but these errors were encountered: