Skip to content
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

Closed
efenderbosch opened this issue Oct 2, 2017 · 13 comments
Closed

Attribute naming strategy? #32

efenderbosch opened this issue Oct 2, 2017 · 13 comments

Comments

@efenderbosch
Copy link

efenderbosch commented Oct 2, 2017

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.

@kaqqao
Copy link
Member

kaqqao commented Oct 3, 2017

Yup, this is really easily done. The interesting interfaces are OperationNameGenerator which controls the name generation for fields i.e. queries and mutations, and ResolverArgumentBuilder which controls the process of mapping operation arguments, including generating their names.

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 getSnakeCaseName, you could also look for e.g. @JsonProperty if you're already using Jackson, or whatever other strategy is appropriate for your case.

Does this help?

@efenderbosch
Copy link
Author

Getting this exception now:


java.lang.ExceptionInInitializerError
	at com.company.graphql.GraphQLControllerTest.<clinit>(GraphQLControllerTest.java:101)
	at sun.misc.Unsafe.ensureClassInitialized(Native Method)
	at sun.reflect.UnsafeFieldAccessorFactory.newFieldAccessor(UnsafeFieldAccessorFactory.java:43)
	at sun.reflect.ReflectionFactory.newFieldAccessor(ReflectionFactory.java:156)
	at java.lang.reflect.Field.acquireFieldAccessor(Field.java:1088)
	at java.lang.reflect.Field.getFieldAccessor(Field.java:1069)
	at java.lang.reflect.Field.get(Field.java:393)
	at org.junit.runners.model.FrameworkField.get(FrameworkField.java:73)
	at org.junit.runners.model.TestClass.getAnnotatedFieldValues(TestClass.java:230)
	at org.junit.runners.ParentRunner.classRules(ParentRunner.java:255)
	at org.junit.runners.ParentRunner.withClassRules(ParentRunner.java:244)
	at org.junit.runners.ParentRunner.classBlock(ParentRunner.java:194)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:362)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: com.google.inject.CreationException: Unable to create injector, see the following errors:

1) Error injecting constructor, java.lang.IllegalArgumentException: Neither the method/field public java.lang.String com.company.models.coupon.CouponRule.getName() nor the declaring class are annotated with GraphQLQuery
  at com.company.graphql.guice.GraphQLSchemaProvider.<init>(GraphQLSchemaProvider.java:29)
  while locating com.company.graphql.guice.GraphQLSchemaProvider
  at com.company.graphql.GraphQLTestModule.configure(GraphQLTestModule.java:31)
  while locating graphql.schema.GraphQLSchema
    for the 1st parameter of com.company.graphql.guice.GraphQLProvider.<init>(GraphQLProvider.java:17)
  while locating com.company.graphql.guice.GraphQLProvider
  at com.company.graphql.GraphQLTestModule.configure(GraphQLTestModule.java:32)
  while locating graphql.GraphQL
    for the 1st parameter of com.company.graphql.GraphQLExecutor.<init>(GraphQLExecutor.java:22)
  at com.company.graphql.GraphQLTestModule.configure(GraphQLTestModule.java:51)
  while locating com.company.graphql.GraphQLExecutor

1 error
	at com.google.inject.internal.Errors.throwCreationExceptionIfErrorsExist(Errors.java:470)
	at com.google.inject.internal.InternalInjectorCreator.injectDynamically(InternalInjectorCreator.java:184)
	at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:110)
	at com.google.inject.Guice.createInjector(Guice.java:99)
	at com.google.inject.Guice.createInjector(Guice.java:73)
	at com.google.inject.Guice.createInjector(Guice.java:62)
	at com.company.graphql.GraphQLTestInjector.<clinit>(GraphQLTestInjector.java:16)
	... 18 more
Caused by: java.lang.IllegalArgumentException: Neither the method/field public java.lang.String com.company.models.coupon.CouponRule.getName() nor the declaring class are annotated with GraphQLQuery
	at io.leangen.graphql.metadata.strategy.query.AnnotatedOperationNameGenerator.queryName(AnnotatedOperationNameGenerator.java:34)
	at io.leangen.graphql.metadata.strategy.query.AnnotatedOperationNameGenerator.generateQueryName(AnnotatedOperationNameGenerator.java:18)
	at io.leangen.graphql.metadata.strategy.query.DelegatingOperationNameGenerator.lambda$generateQueryName$169(DelegatingOperationNameGenerator.java:25)
	at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
	at java.util.Spliterators$ArraySpliterator.tryAdvance(Spliterators.java:958)
	at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126)
	at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:464)
	at io.leangen.graphql.metadata.strategy.query.DelegatingOperationNameGenerator.generateName(DelegatingOperationNameGenerator.java:57)
	at io.leangen.graphql.metadata.strategy.query.DelegatingOperationNameGenerator.generateQueryName(DelegatingOperationNameGenerator.java:25)
	at io.leangen.graphql.metadata.strategy.query.PublicResolverBuilder.lambda$buildQueryResolvers$152(PublicResolverBuilder.java:52)
	at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
	at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
	at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
	at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
	at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
	at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
	at io.leangen.graphql.metadata.strategy.query.PublicResolverBuilder.buildQueryResolvers(PublicResolverBuilder.java:59)
	at io.leangen.graphql.metadata.strategy.query.PublicResolverBuilder.buildQueryResolvers(PublicResolverBuilder.java:35)
	at io.leangen.graphql.generator.OperationRepository.lambda$buildQueryResolvers$42(OperationRepository.java:88)
	at io.leangen.graphql.generator.OperationRepository.lambda$null$44(OperationRepository.java:100)
	at java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:267)
	at java.util.Iterator.forEachRemaining(Iterator.java:116)
	at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
	at java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:270)
	at java.util.Collections$2.tryAdvance(Collections.java:4717)
	at java.util.Collections$2.forEachRemaining(Collections.java:4725)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
	at io.leangen.graphql.generator.OperationRepository.buildResolvers(OperationRepository.java:102)
	at io.leangen.graphql.generator.OperationRepository.buildQueryResolvers(OperationRepository.java:87)
	at io.leangen.graphql.generator.OperationRepository.buildNestedQueries(OperationRepository.java:83)
	at io.leangen.graphql.generator.OperationRepository.getNestedQueries(OperationRepository.java:59)
	at io.leangen.graphql.generator.OperationRepository.getChildQueries(OperationRepository.java:65)
	at io.leangen.graphql.generator.mapping.common.ObjectTypeMapper.getFields(ObjectTypeMapper.java:80)
	at io.leangen.graphql.generator.mapping.common.ObjectTypeMapper.toGraphQLType(ObjectTypeMapper.java:37)
	at io.leangen.graphql.generator.mapping.common.ObjectTypeMapper.toGraphQLType(ObjectTypeMapper.java:29)
	at io.leangen.graphql.generator.mapping.common.CachingMapper.graphQLType(CachingMapper.java:28)
	at io.leangen.graphql.generator.mapping.common.AbstractionCollectingMapper.toGraphQLType(AbstractionCollectingMapper.java:28)
	at io.leangen.graphql.generator.OperationMapper.toGraphQLType(OperationMapper.java:150)
	at io.leangen.graphql.generator.OperationMapper.toGraphQLOperation(OperationMapper.java:122)
	at io.leangen.graphql.generator.OperationMapper.lambda$generateQueries$20(OperationMapper.java:82)
	at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
	at io.leangen.graphql.generator.OperationMapper.generateQueries(OperationMapper.java:83)
	at io.leangen.graphql.generator.OperationMapper.<init>(OperationMapper.java:67)
	at io.leangen.graphql.GraphQLSchemaGenerator.generate(GraphQLSchemaGenerator.java:692)
	at com.company.graphql.guice.GraphQLSchemaProvider.<init>(GraphQLSchemaProvider.java:60)
	at com.company.graphql.guice.GraphQLSchemaProvider$$FastClassByGuice$$11cad6b4.newInstance(<generated>)
	at com.google.inject.internal.DefaultConstructionProxyFactory$FastClassProxy.newInstance(DefaultConstructionProxyFactory.java:89)
	at com.google.inject.internal.ConstructorInjector.provision(ConstructorInjector.java:111)
	at com.google.inject.internal.ConstructorInjector.construct(ConstructorInjector.java:90)
	at com.google.inject.internal.ConstructorBindingImpl$Factory.get(ConstructorBindingImpl.java:268)
	at com.google.inject.internal.BoundProviderFactory.get(BoundProviderFactory.java:61)
	at com.google.inject.internal.ProviderToInternalFactoryAdapter$1.call(ProviderToInternalFactoryAdapter.java:46)
	at com.google.inject.internal.InjectorImpl.callInContext(InjectorImpl.java:1092)
	at com.google.inject.internal.ProviderToInternalFactoryAdapter.get(ProviderToInternalFactoryAdapter.java:40)
	at com.google.inject.internal.SingletonScope$1.get(SingletonScope.java:194)
	at com.google.inject.internal.InternalFactoryToProviderAdapter.get(InternalFactoryToProviderAdapter.java:41)
	at com.google.inject.internal.SingleParameterInjector.inject(SingleParameterInjector.java:38)
	at com.google.inject.internal.SingleParameterInjector.getAll(SingleParameterInjector.java:62)
	at com.google.inject.internal.ConstructorInjector.provision(ConstructorInjector.java:110)
	at com.google.inject.internal.ConstructorInjector.construct(ConstructorInjector.java:90)
	at com.google.inject.internal.ConstructorBindingImpl$Factory.get(ConstructorBindingImpl.java:268)
	at com.google.inject.internal.BoundProviderFactory.get(BoundProviderFactory.java:61)
	at com.google.inject.internal.ProviderToInternalFactoryAdapter$1.call(ProviderToInternalFactoryAdapter.java:46)
	at com.google.inject.internal.InjectorImpl.callInContext(InjectorImpl.java:1092)
	at com.google.inject.internal.ProviderToInternalFactoryAdapter.get(ProviderToInternalFactoryAdapter.java:40)
	at com.google.inject.internal.SingletonScope$1.get(SingletonScope.java:194)
	at com.google.inject.internal.InternalFactoryToProviderAdapter.get(InternalFactoryToProviderAdapter.java:41)
	at com.google.inject.internal.SingleParameterInjector.inject(SingleParameterInjector.java:38)
	at com.google.inject.internal.SingleParameterInjector.getAll(SingleParameterInjector.java:62)
	at com.google.inject.internal.ConstructorInjector.provision(ConstructorInjector.java:110)
	at com.google.inject.internal.ConstructorInjector.construct(ConstructorInjector.java:90)
	at com.google.inject.internal.ConstructorBindingImpl$Factory.get(ConstructorBindingImpl.java:268)
	at com.google.inject.internal.ProviderToInternalFactoryAdapter$1.call(ProviderToInternalFactoryAdapter.java:46)
	at com.google.inject.internal.InjectorImpl.callInContext(InjectorImpl.java:1092)
	at com.google.inject.internal.ProviderToInternalFactoryAdapter.get(ProviderToInternalFactoryAdapter.java:40)
	at com.google.inject.internal.SingletonScope$1.get(SingletonScope.java:194)
	at com.google.inject.internal.InternalFactoryToProviderAdapter.get(InternalFactoryToProviderAdapter.java:41)
	at com.google.inject.internal.InternalInjectorCreator$1.call(InternalInjectorCreator.java:205)
	at com.google.inject.internal.InternalInjectorCreator$1.call(InternalInjectorCreator.java:199)
	at com.google.inject.internal.InjectorImpl.callInContext(InjectorImpl.java:1085)
	at com.google.inject.internal.InternalInjectorCreator.loadEagerSingletons(InternalInjectorCreator.java:199)
	at com.google.inject.internal.InternalInjectorCreator.injectDynamically(InternalInjectorCreator.java:180)
	... 23 more

@efenderbosch
Copy link
Author

efenderbosch commented Oct 3, 2017

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 withOperationsFromSingleton are annotated with @GraphQLQuery, @GraphQLMutation and @GraphQLArgument.

@kaqqao
Copy link
Member

kaqqao commented Oct 3, 2017

Yup, it's common to not have any annotations on the model.
Knowing that, you can drop the DelegatingOperationNameGenerator and just register a custom one that makes snake case names, then use with e.g. BeanResolverBuilder, and register that using withNestedResolverBuilders (that one will be used for 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.

@efenderbosch
Copy link
Author

efenderbosch commented Oct 3, 2017

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: { shopify_tags (application_id: "blah-blah-blah") { id, application_id, entity_type, name } }

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}

@efenderbosch
Copy link
Author

efenderbosch commented Oct 3, 2017

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 AnnotatedArgumentBuilder but they are never triggered. Breakpoints in SnakeCaseOperationNameGenerator are triggered, but they are for methods that aren't queries on the models.

@efenderbosch
Copy link
Author

So simply updating the @GraphQLQuery and @GraphQLArgument annotations to use snake_case seems sufficient to accept those parameters without any custom code , but the "projection" or returned fields still expects camelCased attributes. What does the introspection to build the list of attributes that can be returned in the result of a query/mutation?

@efenderbosch
Copy link
Author

efenderbosch commented Oct 4, 2017

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 SnakeCaseOperationNameGenerator:

@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));
        }

@kaqqao
Copy link
Member

kaqqao commented Oct 4, 2017

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?

@efenderbosch
Copy link
Author

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 SnakeCaseOperationNameGenerator is only used for withNestedResolverBuilders

@kaqqao
Copy link
Member

kaqqao commented Oct 4, 2017

Glad you sorted it out 👍

@josemduarte
Copy link

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;

    }
}

@kaqqao
Copy link
Member

kaqqao commented Jun 25, 2018

Cool! Glad you got it figured out. Thanks for sharing the solution :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants