Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Feature/aliased scalars #659

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 39 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ and join the team!
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**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)
Expand All @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GenericApplicationContext> {

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<String, GraphQLScalarType> 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<String, GraphQLScalarType> predefinedScalars,
final ConfigurableEnvironment environment) {
final List<String> 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));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package graphql.kickstart.autoconfigure.web.servlet;
package graphql.kickstart.autoconfigure.scalars;

import graphql.scalars.ExtendedScalars;
import graphql.schema.GraphQLScalarType;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, GraphQLScalarType> extractScalarDefinitions(final Class<?>... classes) {
final Map<String, GraphQLScalarType> scalarTypes = new HashMap<>();
Stream.of(classes).forEach(clazz -> extractScalarField(clazz, scalarTypes));
return scalarTypes;
}

private static void extractScalarField(Class<?> clazz, Map<String, GraphQLScalarType> target) {
ReflectionUtils.doWithFields(clazz, scalarField -> extractedIfScalarField(target, scalarField));
}

private static void extractedIfScalarField(Map<String, GraphQLScalarType> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
"properties": [
{
"name": "graphql.extended-scalars",
"type": "java.util.Set",
"type": "java.util.Set<java.lang.String>",
"description": "List of extended scalars to be used."
}, {
"name": "graphql.aliased-scalars",
"type": "java.util.Map<java.lang.String, java.util.List<java.lang.String>>",
"description": "Aliased scalar definitions."
}
]
}
Original file line number Diff line number Diff line change
@@ -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,\
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
graphql:
aliased-scalars:
BigDecimal: Number, Decimal
String: Text