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

add support for apollo file uploads, resolves #61 #62

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.AnnotatedElement;
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.mapping.ArgumentInjector;
import io.leangen.graphql.generator.mapping.ArgumentInjectorParams;
import io.leangen.graphql.generator.mapping.TypeMapper;
import io.leangen.graphql.generator.mapping.TypeMappingEnvironment;
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, Set<Class<? extends TypeMapper>> mappersToSkip, TypeMappingEnvironment env) {
return FILE_UPLOAD_SCALAR;
}

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

@Override
public boolean supports(AnnotatedElement element, 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(null, type)
|| ClassUtils.isAssignable(Iterable.class, type.getType())
&& supports(null, GenericTypeReflector.getTypeParameter(type, Iterable.class.getTypeParameters()[0]));
}
}

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

import io.leangen.graphql.util.Utils;
import org.springframework.boot.context.properties.ConfigurationProperties;

import jakarta.annotation.PostConstruct;
import org.springframework.boot.context.properties.ConfigurationProperties;

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

// Web properties
private Http http = new Http();
Expand Down Expand Up @@ -97,6 +97,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 @@ -321,4 +329,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,30 +1,41 @@
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.ExecutorParams;
import io.leangen.graphql.spqr.spring.web.dto.GraphQLRequest;
import io.leangen.graphql.spqr.spring.web.dto.TransportType;
import io.leangen.graphql.util.Defaults;
import io.leangen.graphql.util.Utils;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.http.MediaType;
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.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 Down Expand Up @@ -114,4 +125,35 @@ public Object executeGetEventStream(GraphQLRequest graphQLRequest, R request) {
private Object get(GraphQLRequest graphQLRequest, R request, TransportType transportType) {
return executor.execute(graphQL, new ExecutorParams<>(graphQLRequest, request, transportType));
}

@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 executeGet(graphQLRequest, request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ 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.FileUploadAutoConfiguration
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package io.leangen.graphql.spqr.spring.autoconfigure;

import java.util.Scanner;

import graphql.scalars.ExtendedScalars;
import graphql.schema.GraphQLSchema;
import graphql.schema.diff.DiffSet;
import graphql.schema.diff.SchemaDiff;
import graphql.schema.diff.reporting.CapturingReporter;
import graphql.schema.idl.*;
import io.leangen.graphql.GraphQLSchemaGenerator;
import io.leangen.graphql.spqr.spring.test.ResolverBuilder_TestConfig;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -14,7 +22,7 @@
import static org.junit.Assert.assertNotNull;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {BaseAutoConfiguration.class, ResolverBuilder_TestConfig.class})
@ContextConfiguration(classes = { BaseAutoConfiguration.class, ResolverBuilder_TestConfig.class, FileUploadAutoConfiguration.class })
@TestPropertySource(locations = "classpath:application.properties")
public class ResolverBuilder_SpqrAutoConfigurationTest {

Expand All @@ -40,41 +48,44 @@ public void schemaGeneratorConfigTest() {

@Test
public void schemaConfigTest() {
assertNotNull(schema);
//Operations sources wired in different ways
// -using the default resolver builder
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromAnnotatedSource_wiredAsComponent"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromAnnotatedSource_wiredAsBean"));

//Operations source wired as bean
// -using additional global resolver builder
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsBean_byCustomGlobalResolverBuilder"));
// -using default resolver builders
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsBean_byMethodName"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsBean_byAnnotation"));
// -using custom resolver builders
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsBean_byStringQualifiedCustomResolverBuilder_wiredAsBean"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsBean_byStringQualifiedCustomResolverBuilder_wiredAsComponent"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsBean_byAnnotationQualifiedCustomResolverBuilder_wiredAsBean"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsBean_byAnnotationQualifiedCustomResolverBuilder_wiredAsComponent"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsBean_byNamedCustomResolverBuilder_wiredAsBean"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsBean_byNamedCustomResolverBuilder_wiredAsComponent"));

//Operations source wired as component
// -using additional global resolver builder
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsComponent_byCustomGlobalResolverBuilder"));
// -using default resolver builders
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsComponent_byMethodName"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsComponent_byAnnotation"));
// -using custom resolver builders
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsComponent_byStringQualifiedCustomResolverBuilder_wiredAsBean"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsComponent_byStringQualifiedCustomResolverBuilder_wiredAsComponent"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsComponent_byAnnotationQualifiedCustomResolverBuilder_wiredAsBean"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsComponent_byAnnotationQualifiedCustomResolverBuilder_wiredAsComponent"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsComponent_byNamedCustomResolverBuilder_wiredAsBean"));
assertNotNull(schema.getQueryType().getFieldDefinition("greetingFromBeanSource_wiredAsComponent_byNamedCustomResolverBuilder_wiredAsComponent"));
assertNotNull(schema.getQueryType().getFieldDefinition("springPageComponent_users"));
printSchema();

String expectedSchemaString = new Scanner(ResolverBuilder_SpqrAutoConfigurationTest.class
.getResourceAsStream("/schema.graphql"), "UTF-8")
.useDelimiter("\\A")
.next();

SchemaParser schemaParser = new SchemaParser();
TypeDefinitionRegistry reg = schemaParser.parse(expectedSchemaString);
SchemaGenerator gen = new SchemaGenerator();

RuntimeWiring.Builder runtimeWiring = RuntimeWiring.newRuntimeWiring()
.scalar(ExtendedScalars.GraphQLLong)
.scalar(FileUploadHandler.FILE_UPLOAD_SCALAR);

GraphQLSchema expected = gen.makeExecutableSchema(reg, runtimeWiring.build());

diff(expected, schema);
diff(schema, expected);
}

private void printSchema() {
SchemaPrinter schemaPrinter = new SchemaPrinter(SchemaPrinter.Options.defaultOptions()
.includeDirectives(false)
.includeScalarTypes(true)
.includeSchemaDefinition(true)
.includeIntrospectionTypes(false));
System.out.println("Augmented Schema:");
System.out.println(schemaPrinter.print(schema));
}

private void diff(GraphQLSchema augmentedSchema, GraphQLSchema expected) {
DiffSet diffSet = DiffSet.diffSet(augmentedSchema, expected);
CapturingReporter capture = new CapturingReporter();
new SchemaDiff(SchemaDiff.Options.defaultOptions())
.diffSchema(diffSet, capture);
Assertions.assertThat(capture.getDangers()).isEmpty();
Assertions.assertThat(capture.getBreakages()).isEmpty();
}
}