graphql-spqr icon indicating copy to clipboard operation
graphql-spqr copied to clipboard

Question regarding Joda LocalData and byte arrays

Open Schetnan opened this issue 7 years ago • 11 comments

I am getting exceptions with type mapping with field types of joda LocalDate and byte arrays. Is there a solution for these two types?

nested exception is io.leangen.graphql.generator.exceptions.TypeMappingException: Argument arg0 of operation "supported" has different types in different resolver methods. Types found: [org.joda.time.DateTimeFieldType, org.joda.time.DurationFieldType].

Schetnan avatar Jan 12 '18 21:01 Schetnan

Ah, it seems those are unrelated arguments that both got the generic name arg0. This happens when you neither have the -parameters compiler option (so Java doesn't keep parameter names) nor the @GraphQLArgument annotation.

But, more importantly, why do you have two methods for the query called supported? Is this intentional?

Furthermore, Joda types are not supported out of the box and you have to register custom TypeMappers to handle them. You'll very likely want to treat them as custom scalars, which means you'll have to create the scalars first.

As for the byte array, I believe this will end up being mapped as a list of integers, which is probably not what you want. How exactly do you expect the byte array to be treated?

kaqqao avatar Jan 12 '18 22:01 kaqqao

Most importantly, I can't really help you without any code samples as I have to idea what you're trying to achieve.

kaqqao avatar Jan 12 '18 22:01 kaqqao

Bojan,

Thank you for your response!

The following query loads an extensive object graph..

@GraphQLQuery(name="plans")
public Plan[] getPlans(@GraphQLArgument(name="grp") Integer grp, @GraphQLArgument(name="site")String site, @GraphQLArgument(name="effective")Long e) {

    return loadPlans(group, site, effective.toString());
}

The object in the graph that is causing the exception is the following:

public class Document implements Serializable {

private static final long serialVersionUID = 1L;
private Long id;
private String documentType;
private LocalDate updateDate;
private String documentName;
private String author;
private String contentId;
private byte[] document;
//private Long size

public Document() {
	
}
	
public Long getId() {
	return id;
}
public void setId(Long id) {
	this.id = id;
}
public String getProductId() {
	return productId;
}
public void setProductId(String productId) {
	this.productId = productId;
}

// public LocalDate getUpdateDate() { // return updateDate; // } // public void setUpdateDate(LocalDate updateDate) { // this.updateDate = updateDate; // } public String getDocumentName() { return documentName; } public void setDocumentName(String documentName) { this.documentName = documentName; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getContentId() { return contentId; } public void setContentId(String contentId) { this.contentId = contentId; }

public String getDocumentType() {
	return documentType;
}

public void setDocumentType(String documentType) {
	this.documentType = documentType;
}

// public byte[] getDocument() { // return document; // } // // public void setDocument(byte[] document) { // this.document = document; // }

}

Notice that I have commented out the get/set Document and get/set UpdateDate. When I comment those out, everything works great. It seems that the issue is due to the type mapping of the LocalDate and the byte[]. I am not sure what the "supported" thing is all about. It doesn't exist in the domain model. It seems to be misleading.

You mentioned: "Furthermore, Joda types are not supported out of the box and you have to register custom TypeMappers to handle them. You'll very likely want to treat them as custom scalars, which means you'll have to create the scalars first."

I am new to graphQL and to graphql-spqr. Might there be some examples on how custom TypeMappers might be implemented, configured? I am not sure what you mean by custom scalars. I apologize for my ignorance.

Regarding the byte[]. Ultimately, it is a document that will be sent back to the browser via a web controller.

Schetnan avatar Jan 15 '18 16:01 Schetnan

Ok, I'll experiment with this a bit to see what is happening. I wanted to create a separate module for Joda types anyway, so I might as well do it now. I'll respond here again with the results.

But the byte array part isn't really a good fit for for GraphQL with JSON, I'm afraid. JSON is a textual format, so representing binary data is a no-go, unless you Base64 encode it or similar, but that only makes sense for tiny binaries.

Otherwise, file downloads are normally handled outside of GraphQL, while file uploads are handled in a roundabout way of sticking the binary into the context... So unless you're willing to go for Base64 or forsake JSON completely (and no client library currently works with non-JSON formats), you'll have to redesign that part.

kaqqao avatar Jan 15 '18 17:01 kaqqao

Yes, that makes sense. The domain model is a back end domain model that I don't have much control over unless I provide wrappers or which we do in the web tier for many things. Is there a way to have the type converter ignore a given attribute? I gave @GraphQLIgnore a try, but it didn't seem to compile with my code.

Schetnan avatar Jan 15 '18 17:01 Schetnan

@GraphQLIgnore was until my last commit only usable as a meta-annotation for ignorable arguments. To filter specific methods, you could add a filter to the ResolverBuilder, e.g.

ResolverBuilder skipping = new AnnotatedResolverBuilder()
         .withFilters(member -> !member.getName().equals("fieldOrMethodNameToSkip");
generator.withOperationsFromSingleton(bean, skipping);

kaqqao avatar Jan 18 '18 09:01 kaqqao

The next release will include the option to use @GraphQLIgnore the way you'd expect. I'm planning to release it this weekend.

kaqqao avatar Jan 18 '18 09:01 kaqqao

0.9.6 is out and, among other changes, @GraphQLIgnore can now be placed on fields and methods.

Byte arrays are by default encoded/decoded to/from Base64 strings.

I'm working on modules for common libraries like joda-time.

kaqqao avatar Feb 07 '18 22:02 kaqqao

Just encountered the same issue today. Is there a way to tell the library to read only annotated methods, and not all public methods in the Type class?

I tried

    GraphQLSchema executableSchema = new GraphQLSchemaGenerator()
        .withResolverBuilders(
            //Resolve by annotations
            new AnnotatedResolverBuilder())

but it still tries to read un-annotated methods. Any other alternative, apart from marking all of them (and future ones) as @GraphQLIgnore?

stoyandekov7 avatar Jul 27 '18 16:07 stoyandekov7

@stoyandekov7 On the top level, only annotated methods are exposed by default. On deeper level, all public getters plus annotated methods are exposed. The code you posted only sets the top-level behavior to its default, so it does not effectively change anything. I presume you instead wanted it to apply to deeper levels. In that case, use withNestedResolverBuilders:

GraphQLSchema executableSchema = new GraphQLSchemaGenerator()
        .withNestedResolverBuilders(new AnnotatedResolverBuilder())

This will ignore the public getters if they're not explicitly annotated. Is that what you were looking for?

kaqqao avatar Jul 29 '18 21:07 kaqqao

Thanks for your response @kaqqao, what you describe is what I am looking for, however withNestedResolverBuilders doesn't seem to be achieving it.

I am looking to achieve a behaviour like:

Service (expose 1 query and 1 mutation):

class UserService {
	@GraphQLQuery(name = "user")
	public User getUser(@GraphQLArgument(name = "username")@GraphQLNonNull String username) {
		...
	}

	@GraphQLMutation(name = "addUser")
	public User addUser(@GraphQLArgument(name = "user") User user) {
		...
	}
}

Type:

class User {
	
	private SomeWrappedObject o;
	
	//expose this in the query schema
	@GraphQLQuery(name = "username")
	public String getUsername() {
		o.getUsername();
	}
	
        //expose this in the mutation schema
	@GraphQLMutation(name = "username")
	public void setUsername(String username) {
		o.setUsername(username);
	}
	
	// do NOT expose this in the Query schema
	public SomeWrappedObject getO() {
		return o;
	}
	
	// do NOT expose this in the Mutation schema
	public setFavouriteColor(String color) {
		o.setColor(color);
	}
}

GraphQL:

GraphQLSchema executableSchema = new GraphQLSchemaGenerator()
        .withOperationsFromSingletons(new UserService())
        .withNestedResolverBuilders(
            //Resolve by annotations
            new AnnotatedResolverBuilder())
        .generate();

However even using withNestedResolverBuilders, seems to try to parse all public methods and expose them on the schema.

Thank you for your time and effort in assisting me with this!

stoyandekov7 avatar Jul 30 '18 08:07 stoyandekov7