diff --git a/README.md b/README.md index 39a4e782..b0358d83 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,12 @@ and join the team! **Table of Contents** -- [Quick start](#quick-start) - - [Using Gradle](#using-gradle) - - [Using Maven](#using-maven) -- [Documentation](#documentation) -- [Requirements and Downloads](#requirements-and-downloads) - - [Snapshots](#snapshots) + - [Quick start](#quick-start) + - [Using Gradle](#using-gradle) + - [Using Maven](#using-maven) + - [Documentation](#documentation) + - [Requirements and Downloads](#requirements-and-downloads) + - [Snapshots](#snapshots) - [Enable GraphQL Servlet](#enable-graphql-servlet) - [Enable Graph*i*QL](#enable-graphiql) - [Enable Altair](#enable-altair) @@ -35,8 +35,8 @@ and join the team! - [Customizing GraphQL Playground](#customizing-graphql-playground) - [Tabs](#tabs) - [Enable GraphQL Voyager](#enable-graphql-voyager) - - [Basic settings](#graphql-voyager-basic-settings) - - [CDN](#graphql-voyager-cdn) + - [GraphQL Voyager Basic settings](#graphql-voyager-basic-settings) + - [GraphQL Voyager CDN](#graphql-voyager-cdn) - [Customizing GraphQL Voyager](#customizing-graphql-voyager) - [Supported GraphQL-Java Libraries](#supported-graphql-java-libraries) - [GraphQL Java Tools](#graphql-java-tools) @@ -47,11 +47,11 @@ and join the team! - [Custom scalars and type functions](#custom-scalars-and-type-functions) - [Custom Relay and GraphQL Annotation Processor](#custom-relay-and-graphql-annotation-processor) - [Extended scalars](#extended-scalars) + - [Aliased scalars](#aliased-scalars) - [Tracing and Metrics](#tracing-and-metrics) - [Usage](#usage) -- [FAQs](#faqs) - - [WARNING: NoClassDefFoundError when using GraphQL Java Tools > 5.4.x](#warning-noclassdeffounderror-when-using-graphql-java-tools--54x) - + - [FAQs](#faqs) + - [WARNING: NoClassDefFoundError when using GraphQL Java Tools > 5.4.x](#warning-noclassdeffounderror-when-using-graphql-java-tools--54x) - [Contributions](#contributions) - [Licenses](#licenses) @@ -564,6 +564,34 @@ scalar BigDecimal scalar Date ``` +## Aliased scalars + +*Requires version 12.0.1 or greater* + +The starter also supports [aliased scalars](https://github.com/graphql-java/graphql-java-extended-scalars#alias-scalars). +You can define aliases for any standard or extended scalar, as shown in the example below. Note that + the original extended scalar (`BigDecimal`) will *not* be available. You have to use +`graphql.extended-scalars` property to declare it. + +```yaml +graphql: + aliased-scalars: + BigDecimal: Number, Decimal + String: Text +``` + +When using the [GraphQL Java Tools](#graphql-java-tools) integration, the aliased scalars must also be +declared in the GraphQL Schema: + +```graphql +scalar Number +scalar Decimal +scalar Text +``` + +**Note**: *Custom scalar beans cannot be aliased this way. If you need to alias them, you have to +manually declare the aliased scalar bean.* + # Tracing and Metrics [Apollo style tracing](https://github.com/apollographql/apollo-tracing) along with two levels of diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/scalars/GraphQLAliasedScalarsInitializer.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/scalars/GraphQLAliasedScalarsInitializer.java new file mode 100644 index 00000000..f90a3014 --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/scalars/GraphQLAliasedScalarsInitializer.java @@ -0,0 +1,68 @@ +package graphql.kickstart.autoconfigure.scalars; + +import static graphql.kickstart.autoconfigure.scalars.GraphQLScalarUtils.extractScalarDefinitions; + +import graphql.Scalars; +import graphql.scalars.ExtendedScalars; +import graphql.schema.GraphQLScalarType; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; + +@RequiredArgsConstructor +public class GraphQLAliasedScalarsInitializer + implements ApplicationContextInitializer { + + private static final String GRAPHQL_ALIASED_SCALAR_PREFIX = "graphql.aliased-scalars."; + private static final String JOINING_SEPARATOR = ", "; + private static final String NO_BUILT_IN_SCALAR_FOUND + = "Scalar(s) '%s' cannot be aliased. " + + "Only the following scalars can be aliased by configuration: %s. " + + "Note that custom scalar beans cannot be aliased this way."; + + @Override + public void initialize(@NonNull final GenericApplicationContext applicationContext) { + final Map predefinedScalars + = extractScalarDefinitions(Scalars.class, ExtendedScalars.class); + final ConfigurableEnvironment environment = applicationContext.getEnvironment(); + verifyAliasedScalarConfiguration(predefinedScalars, environment); + predefinedScalars.forEach((scalarName, scalarType) -> + ((List) environment.getProperty(GRAPHQL_ALIASED_SCALAR_PREFIX + scalarName, + List.class, Collections.emptyList())) + .stream() + .map(String::valueOf) + .map(alias -> ExtendedScalars.newAliasedScalar(alias).aliasedScalar(scalarType).build()) + .forEach(aliasedScalar -> applicationContext.registerBean(aliasedScalar.getName(), + GraphQLScalarType.class, () -> aliasedScalar))); + } + + private void verifyAliasedScalarConfiguration( + final Map predefinedScalars, + final ConfigurableEnvironment environment) { + final List invalidScalars = environment.getPropertySources().stream() + .filter(pSource -> pSource instanceof EnumerablePropertySource) + .map(pSource -> (EnumerablePropertySource) pSource) + .map(EnumerablePropertySource::getPropertyNames) + .flatMap(Arrays::stream) + .filter(pName -> pName.startsWith(GRAPHQL_ALIASED_SCALAR_PREFIX)) + .map(pName -> pName.replace(GRAPHQL_ALIASED_SCALAR_PREFIX, "")) + .filter(scalarName -> !predefinedScalars.containsKey(scalarName)) + .sorted() + .collect(Collectors.toList()); + if (!invalidScalars.isEmpty()) { + final String validBuildInScalars = predefinedScalars.keySet().stream().sorted() + .collect(Collectors.joining(JOINING_SEPARATOR)); + throw new ApplicationContextException(String.format(NO_BUILT_IN_SCALAR_FOUND, + String.join(JOINING_SEPARATOR, invalidScalars), validBuildInScalars)); + } + } +} diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/web/servlet/GraphQLExtendedScalarsInitializer.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/scalars/GraphQLExtendedScalarsInitializer.java similarity index 98% rename from graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/web/servlet/GraphQLExtendedScalarsInitializer.java rename to graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/scalars/GraphQLExtendedScalarsInitializer.java index 9c33fd42..c066c31c 100644 --- a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/web/servlet/GraphQLExtendedScalarsInitializer.java +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/scalars/GraphQLExtendedScalarsInitializer.java @@ -1,4 +1,4 @@ -package graphql.kickstart.autoconfigure.web.servlet; +package graphql.kickstart.autoconfigure.scalars; import graphql.scalars.ExtendedScalars; import graphql.schema.GraphQLScalarType; diff --git a/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/scalars/GraphQLScalarUtils.java b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/scalars/GraphQLScalarUtils.java new file mode 100644 index 00000000..3c1d5140 --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/autoconfigure/scalars/GraphQLScalarUtils.java @@ -0,0 +1,44 @@ +package graphql.kickstart.autoconfigure.scalars; + +import graphql.schema.GraphQLScalarType; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.util.ReflectionUtils; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class GraphQLScalarUtils { + + /** + * Extract scalar field definitions from helper classes. Public static + * {@link GraphQLScalarType} fields are considered as scalar definitions. + * + * @param classes classes that may contain scalar definitions. + * @return the map of scalar definitions (keys = scalar names, values are scalar type definitions). + * May return an empty map if no definitions found. If multiple source classes define GraphQL + * scalar types with the same definition, then the last one will be included in the map. + */ + public static Map extractScalarDefinitions(final Class... classes) { + final Map scalarTypes = new HashMap<>(); + Stream.of(classes).forEach(clazz -> extractScalarField(clazz, scalarTypes)); + return scalarTypes; + } + + private static void extractScalarField(Class clazz, Map target) { + ReflectionUtils.doWithFields(clazz, scalarField -> extractedIfScalarField(target, scalarField)); + } + + private static void extractedIfScalarField(Map target, Field field) + throws IllegalAccessException { + if (Modifier.isPublic(field.getModifiers()) + && Modifier.isStatic(field.getModifiers()) + && field.getType().equals(GraphQLScalarType.class)) { + final GraphQLScalarType graphQLScalarType = (GraphQLScalarType) field.get(null); + target.put(graphQLScalarType.getName(), graphQLScalarType); + } + } +} diff --git a/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index ee1b8468..0a8e10de 100644 --- a/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -7,8 +7,12 @@ "properties": [ { "name": "graphql.extended-scalars", - "type": "java.util.Set", + "type": "java.util.Set", "description": "List of extended scalars to be used." + }, { + "name": "graphql.aliased-scalars", + "type": "java.util.Map>", + "description": "Aliased scalar definitions." } ] } diff --git a/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 25d26561..b8e6c25a 100644 --- a/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/graphql-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,5 +1,6 @@ org.springframework.context.ApplicationContextInitializer=\ - graphql.kickstart.autoconfigure.web.servlet.GraphQLExtendedScalarsInitializer + graphql.kickstart.autoconfigure.scalars.GraphQLExtendedScalarsInitializer,\ + graphql.kickstart.autoconfigure.scalars.GraphQLAliasedScalarsInitializer org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ graphql.kickstart.autoconfigure.web.servlet.GraphQLWebAutoConfiguration,\ graphql.kickstart.autoconfigure.web.servlet.GraphQLWebSecurityAutoConfiguration,\ diff --git a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/servlet/test/aliasedscalars/AliasedScalarConfigurationTest.java b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/servlet/test/aliasedscalars/AliasedScalarConfigurationTest.java new file mode 100644 index 00000000..435b2fa8 --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/servlet/test/aliasedscalars/AliasedScalarConfigurationTest.java @@ -0,0 +1,33 @@ +package graphql.kickstart.autoconfigure.web.servlet.test.aliasedscalars; + +import static org.assertj.core.api.Assertions.assertThat; + +import graphql.schema.GraphQLScalarType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = AliasedScalarConfigurationTest.AliasedScalarsTestApplication.class) +@ActiveProfiles("aliased-scalars") +@DisplayName("Testing aliased scalars auto configuration") +class AliasedScalarConfigurationTest { + + @Autowired private ApplicationContext applicationContext; + + @Test + @DisplayName( + "The aliased scalars initializer should be properly picked up by Spring auto configuration.") + void testAutoConfiguration() { + assertThat(applicationContext.getBeansOfType(GraphQLScalarType.class)) + .containsOnlyKeys("Decimal", "Number", "Text"); + } + + @SpringBootApplication + public static class AliasedScalarsTestApplication {} +} \ No newline at end of file diff --git a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/servlet/test/aliasedscalars/AliasedScalarsIncorrectConfigurationTest.java b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/servlet/test/aliasedscalars/AliasedScalarsIncorrectConfigurationTest.java new file mode 100644 index 00000000..afda1a5d --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/servlet/test/aliasedscalars/AliasedScalarsIncorrectConfigurationTest.java @@ -0,0 +1,63 @@ +package graphql.kickstart.autoconfigure.web.servlet.test.aliasedscalars; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import graphql.kickstart.autoconfigure.scalars.GraphQLExtendedScalarsInitializer; +import graphql.schema.GraphQLScalarType; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; + +@DisplayName("Testing aliased scalars configuration") +class AliasedScalarsIncorrectConfigurationTest { + + @Test + @DisplayName( + "Should throw exception at context initialization when a non-built in scalar was aliased.") + void shouldThrowErrorOnStartupIfScalarDoesNotExists() { + // GIVEN + final SpringApplication application = setupTestApplication(Collections.singletonMap( + "graphql.aliased-scalars.BugDecimal", + "Number")); + // THEN + assertThatExceptionOfType(ApplicationContextException.class) + .isThrownBy(application::run) + .withMessage( + "Scalar(s) 'BugDecimal' cannot be aliased." + + " Only the following scalars can be aliased by configuration: BigDecimal," + + " BigInteger, Boolean, Byte, Char, Date, DateTime, Float, ID, Int, JSON," + + " Locale, Long, NegativeFloat, NegativeInt, NonNegativeFloat, NonNegativeInt," + + " NonPositiveFloat, NonPositiveInt, Object, PositiveFloat, PositiveInt, Short," + + " String, Time, Url. Note that custom scalar beans cannot be aliased this way."); + } + + @Test + @DisplayName("Should not create any aliased scalars by default.") + void shouldNotDeclareAnyAliasedScalarsByDefault() { + // GIVEN + final SpringApplication application = setupTestApplication(Collections.emptyMap()); + // WHEN + final ConfigurableApplicationContext context = application.run(); + // THEN + assertThat(context.getBeansOfType(GraphQLScalarType.class)).isEmpty(); + } + + private SpringApplication setupTestApplication(final Map properties) { + final StandardEnvironment standardEnvironment = new StandardEnvironment(); + standardEnvironment.getPropertySources().addFirst(new MapPropertySource("testProperties", + properties)); + final SpringApplication application = + new SpringApplication(GraphQLExtendedScalarsInitializer.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.setEnvironment(standardEnvironment); + return application; + } +} diff --git a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/servlet/test/extendedscalars/ExtendedScalarsTest.java b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/servlet/test/extendedscalars/ExtendedScalarsTest.java index 4809b27d..15238f11 100644 --- a/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/servlet/test/extendedscalars/ExtendedScalarsTest.java +++ b/graphql-spring-boot-autoconfigure/src/test/java/graphql/kickstart/autoconfigure/web/servlet/test/extendedscalars/ExtendedScalarsTest.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import graphql.kickstart.autoconfigure.web.servlet.GraphQLExtendedScalarsInitializer; +import graphql.kickstart.autoconfigure.scalars.GraphQLExtendedScalarsInitializer; import graphql.scalars.ExtendedScalars; import graphql.schema.GraphQLScalarType; import java.util.AbstractMap; diff --git a/graphql-spring-boot-autoconfigure/src/test/resources/application-aliased-scalars.yaml b/graphql-spring-boot-autoconfigure/src/test/resources/application-aliased-scalars.yaml new file mode 100644 index 00000000..0092cfe3 --- /dev/null +++ b/graphql-spring-boot-autoconfigure/src/test/resources/application-aliased-scalars.yaml @@ -0,0 +1,4 @@ +graphql: + aliased-scalars: + BigDecimal: Number, Decimal + String: Text \ No newline at end of file