Search code examples
javagraphql-java

Schema Generator for ApacheCayenne classes


I'm trying to use SPQR to generate GraphQL schema from a Cayenne generated class. Cayenne class looks like this

public class MyCayenneClass {
  public static final Property<Integer> A_PROPERTY = Property.create("aProperty", Integer.class);
  public static final Property<Integer> ANOTHER_PROPERTY = Property.create("anotherProperty", String.class);

  public void setAProperty(Integer aProperty) {
      writeProperty("aProperty", aProperty);
  }
  public Integer getAProperty() {
      return (Integer)readProperty("aProperty");
  }

  public void setAnotherProperty(String anotherProperty) {
      writeProperty("anotherProperty", anotherProperty);
  }
  public String getAnotherProperty() {
      return (String)readProperty("anotherProperty");
  }
}

As the class isn't a simple POJO, SPQR throws an exception and the schema isn't generated.

Error: QUERY_ROOT fields must be an object with field names as keys or a function which returns such an object.

What's the best approach here (without modifying the cayenne class (i.e. annotating a method)?

GraphQLEndPoing.java

@WebServlet(urlPatterns = "/graphql")
public class GraphQLEndpoint extends SimpleGraphQLServlet {

public GraphQLEndpoint() {
    super(buildSchema());
}

//This method used SPQR
private static GraphQLSchema buildSchema() {
    GraphQLSchema schemaGenerator = new GraphQLSchemaGenerator()
            .withOperationsFromSingletons(myRepository) //register the beans
            .generate();
    return schemaGenerator;
}

 private static final MyRepository myRepository;

 static {
     myRepository= new MyRepository ();
 }
}

MyRepository.java

public class MyRepository{

private MyLibService libService;

 @GraphQLQuery
 public MyCayenneClass  find(Integer id) {
    List<MyCayenneClass> myList= libService.fetchById(new Integer[] {id});
    return myList.get(0);
 }
}

*FYI. If I declare the schema. Code will work just fine

schema {
  query: Query
}

type Query {
  find(id: Int): MyCayenneClass
}

type ConcContract {    
 id: ID
  aProperty: Int
  anotherProperty: String        
}

Solution

  • From SPQR's perspective, this isn't really different from a POJO, as SPQR cares only about the types.

    By default, for all nested classes (MyCayenneClass in your case), everything that looks like a getter will be exposed. For top-level classes (MyRepository in your case), only annotated methods are exposed by default. And at least one top-level method must be exposed, otherwise you have an invalid schema.

    The error, as it stands, just means not a single top-level query was discovered. I see the @GraphQLQuery annotation is commented out. Is that intentional? With the default config, this would not expose any query.

    You can register a different ResolverBuilder, e.g. PublicResolverBuilder (or your own implementation/extension) if you want to expose un-annotated methods.

    E.g.

    generator.withOperationsFromSingleton(new MyRepository(), new PublicResolverBuilder())
    

    This would expose all public methods from that class.

    Here's a slightly simplified example I tried with v0.9.6 and seems to work as expected (I know you're using a rather old version from the error text).

    public class MyRepository {
    
        @GraphQLQuery //not commented out
        public MyCayenneClass find(Integer in) {
            return new MyCayenneClass();
        }
    }
    
    // extends CayenneDataObject because I don't know where to get the 
    // writeProperty and readProperty from
    // but shouldn't change anything from SPQR's perspective
    public class MyCayenneClass extends CayenneDataObject {
        public static final Property<Integer> A_PROPERTY = Property.create("aProperty", Integer.class);
        public static final Property<String> ANOTHER_PROPERTY = Property.create("anotherProperty", String.class);
    
        public void setAProperty(Integer aProperty) {
            writeProperty("aProperty", aProperty);
        }
    
        public Integer getAProperty() {
            return (Integer)readProperty("aProperty");
        }
    
        public void setAnotherProperty(String anotherProperty) {
            writeProperty("anotherProperty", anotherProperty);
        }
    
        public String getAnotherProperty() {
            return (String)readProperty("anotherProperty");
        }
    }
    

    There's many more customizations you can apply, depending on what you end up needing, but from the question as it stands, it doesn't seem you need anything extra...

    To override the ResolverBuilder used for nested classes, you have 2 options.

    1) Register it globally, so all nested types use it:

    generator.withNestedResolverBuilders(customBuilder)
    

    2) Or per type:

    .withNestedResolverBuildersForType(MyCayenneClass.class, new BeanResolverBuilder())
    

    But this is very rarely needed...