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 Aug 15, 2023
1 parent 8751777 commit 70229cc
Show file tree
Hide file tree
Showing 10 changed files with 356 additions and 62 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.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();
}
}

Loading

0 comments on commit 70229cc

Please sign in to comment.