Skip to content

Commit

Permalink
add support for apollo file uploads, resolves #61
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy2003 committed Feb 2, 2020
1 parent 32dc14d commit 2b29882
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.leangen.graphql.spqr.spring.autoconfigure;

import io.leangen.graphql.module.Module;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FileUploadAutoConfiguration {

@Bean
@ConditionalOnProperty(name = "graphql.spqr.multipart-upload.enabled", havingValue = "true")
public Internal<Module> uploadModule() {
FileUploadHandler uploadAdapter = new FileUploadHandler();
return new Internal<>(context -> context.getSchemaGenerator()
.withArgumentInjectors(uploadAdapter)
.withTypeMappers(uploadAdapter)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.leangen.graphql.spqr.spring.autoconfigure;

import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Parameter;
import java.util.*;

import graphql.schema.*;
import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.graphql.generator.BuildContext;
import io.leangen.graphql.generator.OperationMapper;
import io.leangen.graphql.generator.mapping.ArgumentInjector;
import io.leangen.graphql.generator.mapping.ArgumentInjectorParams;
import io.leangen.graphql.generator.mapping.TypeMapper;
import io.leangen.graphql.util.ClassUtils;
import org.springframework.web.multipart.MultipartFile;

class FileUploadHandler implements TypeMapper, ArgumentInjector {

public static final GraphQLScalarType FILE_UPLOAD_SCALAR = GraphQLScalarType.newScalar()
.name("FileUpload")
.description("An apollo upload compatible scalar for multipart uploads")
.coercing(new Coercing<MultipartFile, Void>() {

@Override
public Void serialize(Object dataFetcherResult) throws CoercingSerializeException {
throw new CoercingSerializeException("Upload is not a return type");
}

@Override
public MultipartFile parseValue(Object input) throws CoercingParseValueException {
if (input instanceof MultipartFile) {
return (MultipartFile) input;
}
throw new CoercingParseValueException("Expected the input to be parsed by the servlet controller");
}

@Override
public MultipartFile parseLiteral(Object input) throws CoercingParseLiteralException {
throw new CoercingParseLiteralException("Parsing the literal of the upload is not supported");
}
})
.build();

@Override
public GraphQLInputType toGraphQLInputType(AnnotatedType javaType, OperationMapper operationMapper, Set<Class<? extends TypeMapper>> mappersToSkip, BuildContext buildContext) {
return FILE_UPLOAD_SCALAR;
}

@Override
public GraphQLOutputType toGraphQLType(AnnotatedType javaType, OperationMapper operationMapper, Set<Class<? extends TypeMapper>> mappersToSkip, BuildContext buildContext) {
throw new UnsupportedOperationException("FileUpload is not an output type");
}

@Override
public boolean supports(AnnotatedType type) {
return type != null && ClassUtils.isAssignable(MultipartFile.class, type.getType());
}

@Override
public Object getArgumentValue(ArgumentInjectorParams params) {
if ((params.getInput() instanceof MultipartFile)) {
return params.getInput();
}
if (!(params.getInput() instanceof Collection)) {
return null;
}
if (ClassUtils.isAssignable(params.getType().getType(), params.getInput().getClass())) {
return params.getInput();
}
if (ClassUtils.isAssignable(List.class, params.getType().getType())) {
//noinspection rawtypes,unchecked
return new ArrayList((Collection) params.getInput());
}
if (ClassUtils.isAssignable(Set.class, params.getType().getType())) {
//noinspection rawtypes,unchecked
return new LinkedHashSet((Collection) params.getInput());
}
throw new UnsupportedOperationException("Cannot convert " + params.getInput().getClass() + " to " + params.getType());
}

@Override
public boolean supports(AnnotatedType type, Parameter parameter) {
return supports(type)
|| ClassUtils.isAssignable(Iterable.class, type.getType())
&& supports(GenericTypeReflector.getTypeParameter(type, Iterable.class.getTypeParameters()[0]));
}
}

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package io.leangen.graphql.spqr.spring.autoconfigure;

import javax.annotation.PostConstruct;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;

@ConfigurationProperties(prefix = "graphql.spqr")
@SuppressWarnings("WeakerAccess")
public class SpqrProperties {
Expand All @@ -16,6 +16,7 @@ public class SpqrProperties {
private String[] basePackages;
private boolean abstractInputTypeResolution;
private Relay relay = new Relay();
private MultipartUpload multipartUpload = new MultipartUpload();

// Web properties
private Http http = new Http();
Expand Down Expand Up @@ -88,6 +89,14 @@ public void setGui(Gui gui) {
this.gui = gui;
}

public MultipartUpload getMultipartUpload() {
return multipartUpload;
}

public void setMultipartUpload(MultipartUpload multipartUpload) {
this.multipartUpload = multipartUpload;
}

public static class Relay {

private boolean enabled;
Expand Down Expand Up @@ -269,4 +278,20 @@ public void setPageTitle(String pageTitle) {
this.pageTitle = pageTitle;
}
}

public static class MultipartUpload {

private boolean enabled;

public boolean isEnabled() {
return enabled;
}

/**
* @param enabled if enabled a multipart file upload will be activated
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
package io.leangen.graphql.spqr.spring.web;

import java.util.*;
import java.util.stream.Collectors;

import graphql.GraphQL;
import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.graphql.execution.GlobalEnvironment;
import io.leangen.graphql.generator.mapping.ConverterRegistry;
import io.leangen.graphql.metadata.messages.EmptyMessageBundle;
import io.leangen.graphql.metadata.strategy.type.DefaultTypeInfoGenerator;
import io.leangen.graphql.metadata.strategy.value.ValueMapper;
import io.leangen.graphql.spqr.spring.web.dto.GraphQLRequest;
import io.leangen.graphql.util.Defaults;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;
import org.springframework.validation.DataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
public abstract class GraphQLController<R> {

protected final GraphQL graphQL;
protected final GraphQLExecutor<R> executor;
private final ValueMapper valueMapper;


public GraphQLController(GraphQL graphQL, GraphQLExecutor<R> executor) {
this.graphQL = graphQL;
this.executor = executor;
this.valueMapper = Defaults.valueMapperFactory(new DefaultTypeInfoGenerator()).getValueMapper(
Collections.emptyMap(),
new GlobalEnvironment(EmptyMessageBundle.INSTANCE, null, null, new ConverterRegistry(Collections.emptyList(), Collections.emptyList()), null, null, null, null)
);
}

@PostMapping(
Expand All @@ -33,8 +43,8 @@ public GraphQLController(GraphQL graphQL, GraphQLExecutor<R> executor) {
)
@ResponseBody
public Object executeJsonPost(@RequestBody GraphQLRequest requestBody,
GraphQLRequest requestParams,
R request) {
GraphQLRequest requestParams,
R request) {
String query = requestParams.getQuery() == null ? requestBody.getQuery() : requestParams.getQuery();
String operationName = requestParams.getOperationName() == null ? requestBody.getOperationName() : requestParams.getOperationName();
Map<String, Object> variables = requestParams.getVariables().isEmpty() ? requestBody.getVariables() : requestParams.getVariables();
Expand All @@ -49,8 +59,8 @@ public Object executeJsonPost(@RequestBody GraphQLRequest requestBody,
)
@ResponseBody
public Object executeGraphQLPost(@RequestBody String queryBody,
GraphQLRequest graphQLRequest,
R request) {
GraphQLRequest graphQLRequest,
R request) {
String query = graphQLRequest.getQuery() == null ? queryBody : graphQLRequest.getQuery();
return executor.execute(graphQL, new GraphQLRequest(query, graphQLRequest.getOperationName(), graphQLRequest.getVariables()), request);
}
Expand All @@ -63,8 +73,8 @@ public Object executeGraphQLPost(@RequestBody String queryBody,
)
@ResponseBody
public Object executeFormPost(@RequestParam Map<String, String> queryParams,
GraphQLRequest graphQLRequest,
R request) {
GraphQLRequest graphQLRequest,
R request) {
String queryParam = queryParams.get("query");
String operationNameParam = queryParams.get("operationName");

Expand All @@ -83,4 +93,34 @@ public Object executeFormPost(@RequestParam Map<String, String> queryParams,
public Object executeGet(GraphQLRequest graphQLRequest, R request) {
return executor.execute(graphQL, graphQLRequest, request);
}

@PostMapping(
value = "${graphql.spqr.http.endpoint:/graphql}",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE
)
public Object executeMultipartFileUpload(
@RequestParam("operations") String requestString,
@RequestParam("map") String mappingString,
@RequestParam Map<String, MultipartFile> multipartFiles,
R request) {
GraphQLRequest graphQLRequest = valueMapper.fromString(requestString, GenericTypeReflector.annotate(GraphQLRequest.class));
Map<String, List<String>> fileMappings = valueMapper.fromString(mappingString, GenericTypeReflector.annotate((Map.class)));

Map<String, Object> values = new LinkedHashMap<>();
fileMappings.forEach((fileKey, variables) -> {
for (String variable : variables) {
String[] parts = variable.split("\\.");
String path = parts[0] + Arrays.stream(parts).skip(1).collect(Collectors.joining("][", "[", "]"));
values.put(path, multipartFiles.get(fileKey));
}
});

DataBinder binder = new DataBinder(graphQLRequest, "operations");
binder.setIgnoreUnknownFields(false);
binder.setIgnoreInvalidFields(false);
binder.bind(new MutablePropertyValues(values));

return executor.execute(graphQL, graphQLRequest, request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.leangen.graphql.spqr.spring.autoconfigure.MvcAutoConfiguration,\
io.leangen.graphql.spqr.spring.autoconfigure.ReactiveAutoConfiguration,\
io.leangen.graphql.spqr.spring.autoconfigure.SpringDataAutoConfiguration,\
io.leangen.graphql.spqr.spring.autoconfigure.WebSocketAutoConfiguration
io.leangen.graphql.spqr.spring.autoconfigure.WebSocketAutoConfiguration,\
io.leangen.graphql.spqr.spring.autoconfigure.FileUploadAutoConfiguration
Loading

0 comments on commit 2b29882

Please sign in to comment.