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

Support of Unions #17

Open
a-marcel opened this issue May 1, 2017 · 11 comments
Open

Support of Unions #17

a-marcel opened this issue May 1, 2017 · 11 comments

Comments

@a-marcel
Copy link
Contributor

a-marcel commented May 1, 2017

Hello,

if i try to create a union Type

type Foo {
  name: String
}
union SingleUnion = Foo
type Root {
  single: SingleUnion
}

an error occurs

RootTypeProvider.java:[3,60] cannot find symbol
[ERROR] symbol:   class SingleUnion

Marcel

@a-marcel
Copy link
Contributor Author

a-marcel commented May 2, 2017

Hello,

i see in the code that you support Unions in general, but for my line above, the file is not written.

Do you've any idea ?

Thanks
Marcel

@brimworks
Copy link
Contributor

We can parse schemas with Union types, we just need to add logic in here:

https://github.com/Distelli/graphql-apigen/blob/master/apigen/src/main/resources/graphql-apigen.stg

...so it can generate the union types. Something like this:

unionTypeFileName(model) ::= "<if(model.unionType)><model.name>.java<endif>"
unionTypeGenerator(model) ::= <<
package <model.packageName>;
...
>>

Feel free to take a stab at it.

Cheers,
-Brian

@brimworks
Copy link
Contributor

...and of course you will also want to write a TypeProvider:

unionTypeProviderFileName(model) ::= "<if(model.unionType)><model.name>TypeProvider.java<endif>"
unionTypeProviderGenerator(model) ::= <<
...
>>

@a-marcel
Copy link
Contributor Author

a-marcel commented May 2, 2017

The type should be something like this

GraphQLUnionType PetType = newUnionType()
        .name("Pet")
        .possibleType(CatType)
        .possibleType(DogType)
        .typeResolver(new TypeResolver() {
            @Override
            public GraphQLObjectType getType(TypeResolutionEnvironment env) {
                if (env.getObject() instanceof Cat) {
                    return CatType;
                }
                if (env.getObject() instanceof Dog) {
                    return DogType;
                }
                return null;
            }
        })
        .build();
    

@a-marcel
Copy link
Contributor Author

a-marcel commented May 2, 2017

Hello,

i build some Object Local:

my schema:

interface DefaultPage {
	id: String
	url: String
}

type Contact implements DefaultPage {
	id: String
}

union Page = Contact

type helloWorldQuery {
	page(url: String): [Page]
}

i added in the generated Guice Binder:

  types.addBinding("Page")
                .toProvider(...PageTypeProvider.class);

create a PageTypeProvider


import javax.inject.Inject;
import javax.inject.Provider;

import com.distelli.graphql.MethodDataFetcher;

import graphql.Scalars;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLUnionType;
import graphql.schema.TypeResolver;

public class PageTypeProvider implements Provider<GraphQLUnionType> {

	@Inject
    protected ContactTypeProvider contactProvider;

	
	@Override
	public GraphQLUnionType get() {
		
		return GraphQLUnionType.newUnionType()
				.name("Page")
				.possibleType(contactProvider.get())
				
				.typeResolver(new TypeResolver() {

					@Override
					public GraphQLObjectType getType(Object object) {
						return contactProvider.get();
					}
		          
		        })
				.build();
	}

}

possibleType is repeatable.
i guess here it's better to have a MultiBindingGuice typeResolver.

and i create a Page object

public interface Page {

	public static class Builder {

		private String _id;

		public Builder() {
		}

		public Builder(Page src) {

			_id = src.getId();

		}

		public Builder withId(String _id) {
			this._id = _id;
			return this;
		}

		public Page build() {
			return new Impl(this);
		}
	}

	public static class Impl implements Page {

		private String _id;

		protected Impl(Builder builder) {

			this._id = builder._id;

		}

		@Override
		public String getId() {
			return _id;
		}

		@Override
		public String toString() {
			return "Page{"

					+ "id=" + _id

					+ "}";
		}
		// TODO: equals(Object) & hashCode()
	}

	public default String getId() {
		return null;
	}
}

and i bind a custom HelloWorldQuery class.

I pick all information from https://medium.com/the-graphqlhub/graphql-tour-interfaces-and-unions-7dd5be35de0d

my Query is:

{"query":"{page(url: \"xxx\") { ... on Contact {id}}}

Your stg file is a little complex for me and i'm new in graphql. i hope, it help a little bit :/

Thanks for your work, it helps me a lot, to create a clean service without writing all this JavaClasses.

Marcel

@brimworks
Copy link
Contributor

Thanks Marcel, how do you want the Page interface to interact with the various union types? I was thinking that it would generate various type specific methods which return null unless the union holds a value of that type.

So PageTypeResolver would need to look something more like this if there was two types in the union (aka union Page = Contact | OtherPage):

public class PageTypeProvider implements Provider<GraphQLUnionType> {

	@Inject
    protected ContactTypeProvider contactProvider;
	@Inject
    protected OtherPageTypeProvider otherPageProvider;

	
	@Override
	public GraphQLUnionType get() {
		
		return GraphQLUnionType.newUnionType()
				.name("Page")
				.possibleType(contactProvider.get())
				.possibleType(otherPageProvider.get())
				.typeResolver(new TypeResolver() {

					@Override
					public GraphQLObjectType getType(Object object) {
						Page page = (Page)object;
						if ( null != page.getContact() ) {
						    return contactProvider.get();
						if ( null != page.getOtherPage() ) {
						    return otherPageProvider.get();
						} // TODO: What is the default? what if we return null here?
					}
		          
		        })
				.build();
	}

}

...and the Page class generated would have get fields for getContact() and getOtherPage().

What are your thoughts?

@brimworks
Copy link
Contributor

...or alternatively we generate an enum for the type of value held by the union, and have a special method to obtain that... so the getType() method becomes:

@Override
public GraphQLObjectType getType(Object object) {
    Page page = (Page)object;
    switch ( page.type() ) {
    case Contact: return contactProvider.get();
    case OtherPage: return otherPageProvider.get();
    }
    throw new IllegalStateException("Unknown type="+page.type());
}

...also, don't we need a way to obtain the value for the union type? ...with a data binder?

@a-marcel
Copy link
Contributor Author

a-marcel commented May 3, 2017

Hi, thanks for your answer.

one question, i can answer: it's possible to return null in getType (https://github.com/graphql-java/graphql-java - look at union type).

from my pov its not necessary to create a "type" property because the developer/creator of the schema should know if he need it or not. the getType function should just check if the Object object a valid type and return the mapped provider.

for the data question, i check this now and come back to you.

Marcel

@a-marcel
Copy link
Contributor Author

a-marcel commented May 3, 2017

Hello again,

it's not possible to return null for the getType. If you return null, graphql will fail because he cannot detect the type.

I found two links

https://github.com/graphql-java/graphql-java/blob/f493a19cdd99a78b14bda976aba2e00f47881894/src/test/groovy/graphql/UnionTest.groovy

https://github.com/graphql-java/graphql-java/blob/master/src/test/groovy/graphql/GarfieldSchema.java

in general my classes looks like this, but my response is always empty:

{"data":{"page":[{}]},"error":[]}

if i return multiple objects in my HelloWorldQuery it returns multiple empty objects

public class HelloWorldQuery implements helloWorldQuery {
	@Override
	public List<Object> page(PageArgs args) {
		
		String url = args.getUrl();

		List<Object> res = new ArrayList<Object>();

		res.add(new Contact.Builder().withId("123").build());
		res.add(new Contact.Builder().withId("123").build());

		return res;
	}
}

but i'm not able to get the expected:

query:

{page(url: \"xxx\") { ... on Contact { id }}}

i expect:

{"data":{"page":[{id: "123"}]},"error":[]}

maybe you've an idea, but in general i looks like it works, but graphql filter my objects.

Thanks
Marcel

@a-marcel
Copy link
Contributor Author

a-marcel commented May 4, 2017

Hello again,

i changed my old hello world example to an union graphql example

https://github.com/marcelalburg/vertx_graphql_hello_world_example (includes a test for simple startup)

i created manually : https://github.com/marcelalburg/vertx_graphql_hello_world_example/blob/master/src/main/generated/com/example/apigen/Page.java

https://github.com/marcelalburg/vertx_graphql_hello_world_example/blob/master/src/main/generated/com/example/apigen/PageTypeProvider.java

changed the getPage method to Object (because i like to return different objects) in:
https://github.com/marcelalburg/vertx_graphql_hello_world_example/blob/master/src/main/generated/com/example/apigen/helloWorldQuery.java

and changed the impl of the query:
https://github.com/marcelalburg/vertx_graphql_hello_world_example/blob/master/src/main/java/com/example/guice/HelloWorldImpl.java

this returns currently a Contact Type with some values.

In this example the query returns just [].

maybe you've an idea, because i think, that i don't understand your concept correctly (and i'm still not fit in understanding the java-graphql implementation).

Thanks for your help

Marcel

@a-marcel
Copy link
Contributor Author

a-marcel commented May 5, 2017

Hello again,

sorry for all this messages. But i found the issue now.

i've written a test:

https://github.com/marcelalburg/vertx_graphql_hello_world_example/blob/master/src/test/java/com/example/RawGraphQLWithApiGenObjectsTest.java

There are some issues. The biggest is, that the Guice Provider is always returning a new Instance of a Graphql Type and this is not working together with unions. So, i make my ContactTypeProvider to a Singleton and it works perfectly.

The second one is, it's a nice to have to support interfaces.

I wrote everything for interfaces and the Union Provider and hope you can use it to implement it in your generation file.

This singleton is maybe not the best idea and i hope you've some other nice idea to return always the same instance of a type.

I ask in the GraphQL github, why there is an issue with references in Union Types. I hope they have an idea, so that you don't need to inject provider for the possible types in a union.

Thanks
Marcel

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

2 participants