I can\'t find out how to upload files if i use graphql-java, can someone show me a demo? I will be appreciated!
reference : https://github.com/graphql-java-kickstar
The main problem is that graphql-java-tools
might have issues to do the field mapping for resolvers that contain fields of not basic types like List
, String
, Integer
, Boolean
, etc...
We solved this issue by just creating our own custom scalar that is basically like ApolloScalar.Upload
. But instead of returning an object of the type Part
, we return our own resolver type FileUpload
which contains the contentType as String
and the inputStream as byte[]
, then the field mapping works and we can read the byte[]
within the resolver.
First, set up the new type to be used in the resolver:
public class FileUpload {
private String contentType;
private byte[] content;
public FileUpload(String contentType, byte[] content) {
this.contentType = contentType;
this.content = content;
}
public String getContentType() {
return contentType;
}
public byte[] getContent() {
return content;
}
}
Then we make a custom scalar that looks pretty much like ApolloScalars.Upload
, but returns our own resolver type FileUpload
:
public class MyScalars {
public static final GraphQLScalarType FileUpload = new GraphQLScalarType(
"FileUpload",
"A file part in a multipart request",
new Coercing<FileUpload, Void>() {
@Override
public Void serialize(Object dataFetcherResult) {
throw new CoercingSerializeException("Upload is an input-only type");
}
@Override
public FileUpload parseValue(Object input) {
if (input instanceof Part) {
Part part = (Part) input;
try {
String contentType = part.getContentType();
byte[] content = new byte[part.getInputStream().available()];
part.delete();
return new FileUpload(contentType, content);
} catch (IOException e) {
throw new CoercingParseValueException("Couldn't read content of the uploaded file");
}
} else if (null == input) {
return null;
} else {
throw new CoercingParseValueException(
"Expected type " + Part.class.getName() + " but was " + input.getClass().getName());
}
}
@Override
public FileUpload parseLiteral(Object input) {
throw new CoercingParseLiteralException(
"Must use variables to specify Upload values");
}
});
}
In the resolver, you would now be able to get the file from the resolver arguments:
public class FileUploadResolver implements GraphQLMutationResolver {
public Boolean uploadFile(FileUpload fileUpload) {
String fileContentType = fileUpload.getContentType();
byte[] fileContent = fileUpload.getContent();
// Do something in order to persist the file :)
return true;
}
}
In the schema, you declare it like:
scalar FileUpload
type Mutation {
uploadFile(fileUpload: FileUpload): Boolean
}
Let me know if it doesn't work for you :)
define a scalar type in our schema
scalar Upload
and we should configure GraphQLScalarType for Upload, use this below:
@Configuration
public class GraphqlConfig {
@Bean
public GraphQLScalarType uploadScalarDefine() {
return ApolloScalars.Upload;
}
}
then we would define a mutation in schema and a GraphQLMutationResolver for testMultiFilesUpload
type Mutation {
testMultiFilesUpload(files: [Upload!]!): Boolean
}
here is Resolver:
public Boolean testMultiFilesUpload(List<Part> parts, DataFetchingEnvironment env) {
// get file parts from DataFetchingEnvironment, the parts parameter is not use
List<Part> attachmentParts = env.getArgument("files");
int i = 1;
for (Part part : attachmentParts) {
String uploadName = "copy" + i;
try {
part.write("your path:" + uploadName);
} catch (IOException e) {
e.printStackTrace();
}
i++;
}
return true;
}
}
configure a jackson deserializer for javax.servlet.http.Part
and register it to ObjectMapper
public class PartDeserializer extends JsonDeserializer<Part> {
@Override
public Part deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return null;
}
}
why we return null? because the List<Part> parts
always null ,In the resolver's method, get the parts argument from the DataFetchingEnvironment;
environment.getArgument("files")
register it to ObjectMapper:
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
SimpleModule module = new SimpleModule();
module.addDeserializer(Part.class, new PartDeserializer());
objectMapper.registerModule(module);
return objectMapper;
}
operations { "query": "mutation($files: [Upload!]!) {testMultiFilesUpload(files:$files)}", "variables": {"files": [null,null] } } map { "file0": ["variables.files.0"] , "file1":["variables.files.1"]} file0 your file file1 your file
like this:
remember to select the form-data option
through this we can upload multiple files
Just to add onto the answers above, for anyone like me who could find 0 examples of file upload with the GraphQLSchemaGenerator vs the schema first approach, you have to just create a TypeMapper and add that to your GraphQLSchemaGenerator:
public class FileUploadMapper implements TypeMapper {
@Override
public GraphQLOutputType toGraphQLType(
final AnnotatedType javaType, final OperationMapper operationMapper,
final Set<Class<? extends TypeMapper>> mappersToSkip, final BuildContext buildContext) {
return MyScalars.FileUpload;
}
@Override
public GraphQLInputType toGraphQLInputType(
final AnnotatedType javaType, final OperationMapper operationMapper,
final Set<Class<? extends TypeMapper>> mappersToSkip, final BuildContext buildContext) {
return MyScalars.FileUpload;
}
@Override
public boolean supports(final AnnotatedType type) {
return type.getType().equals(FileUpload.class); //class of your fileUpload POJO from the previous answer
}
}
then in your GraphQL @Configuration file where you are building your GraphQLSchema:
public GraphQLSchema schema(GraphQLSchemaGenerator schemaGenerator) {
return schemaGenerator
.withTypeMappers(new FileUploadMapper()) //add this line
.generate();
}
Then in your mutation resolver
@GraphQLMutation(name = "fileUpload")
public void fileUpload(
@GraphQLArgument(name = "file") FileUpload fileUpload //type here must be the POJO.class referenced in your TypeMapper
) {
//do something with the byte[] from fileUpload.getContent();
return;
}