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

feat: integrate extended GraphQL scalars #525

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
54 changes: 40 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ and join the team!
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**

- [WARNING: NoClassDefFoundError when using GraphQL Java Tools > 5.4.x](#warning-noclassdeffounderror-when-using-graphql-java-tools--54x)
- [WARNING: NoClassDefFoundError when using GraphQL Java Tools > 5.4.x](#warning-noclassdeffounderror-when-using-graphql-java-tools--54x)
- [Using Gradle](#using-gradle)
- [Using Maven](#using-maven)
- [Documentation](#documentation)
Expand All @@ -26,21 +26,22 @@ and join the team!
- [Enable Graph*i*QL](#enable-graphiql)
- [Enable Altair](#enable-altair)
- [Enable GraphQL Playground](#enable-graphql-playground)
- [Basic settings](#basic-settings)
- [CDN](#cdn)
- [Custom static resources](#custom-static-resources)
- [Customizing GraphQL Playground](#customizing-graphql-playground)
- [Tabs](#tabs)
- [Basic settings](#basic-settings)
- [CDN](#cdn)
- [Custom static resources](#custom-static-resources)
- [Customizing GraphQL Playground](#customizing-graphql-playground)
- [Tabs](#tabs)
- [Supported GraphQL-Java Libraries](#supported-graphql-java-libraries)
- [GraphQL Java Tools](#graphql-java-tools)
- [GraphQL Annotations](#graphql-annotations)
- [Configuration](#configuration)
- [Root resolvers, directives, type extensions](#root-resolvers-directives-type-extensions)
- [Interfaces](#interfaces)
- [Custom scalars and type functions](#custom-scalars-and-type-functions)
- [Custom Relay and GraphQL Annotation Processor](#custom-relay-and-graphql-annotation-processor)
- [GraphQL Java Tools](#graphql-java-tools)
- [GraphQL Annotations](#graphql-annotations)
- [Configuration](#configuration)
- [Root resolvers, directives, type extensions](#root-resolvers-directives-type-extensions)
- [Interfaces](#interfaces)
- [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)
- [Tracing and Metrics](#tracing-and-metrics)
- [Usage](#usage)
- [Usage](#usage)
- [Contributions](#contributions)
- [Licenses](#licenses)

Expand Down Expand Up @@ -537,6 +538,31 @@ It is possible to define a bean implementing `Relay` and/or `GraphQLAnnotations`
will be passed to the schema builder. Spring dependency injection works as usual. Note that GraphQL
Annotations provides default implementation for these which should be sufficient is most cases.

## Extended scalars

[Extended scalars](https://github.com/graphql-java/graphql-java-extended-scalars) can be enabled by using the
`graphql.extended-scalars` configuration property, e. g.:

```yaml
graphql:
extended-scalars: BigDecimal, Date
```

The available scalars are the following: `BigDecimal`, `BigInteger`, `Byte`, `Char`, `Date`, `DateTime`, `JSON`,
`Locale`, `Long`, `NegativeFloat`, `NegativeInt`, `NonNegativeFloat`, `NonNegativeInt`, `NonPositiveFloat`,
`NonPositiveInt`, `Object`, `PositiveFloat`, `PositiveInt`, `Short`, `Time`, `Url`.

This setting works with both the [GraphQL Java Tools](#graphql-java-tools) and the
[GraphQL Annotations](#graphql-annotations) integration.

When using the [GraphQL Java Tools](#graphql-java-tools) integration, the scalars must also be declared in the GraphQL
Schema:

```graphql
scalar BigDecimal
scalar Date
```

# 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
Expand Up @@ -3,3 +3,6 @@ spring:
name: graphql-subscription-example
server:
port: 9001

graphql:
extended-scalars: BigDecimal
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ schema {
subscription : Subscription
}

scalar BigDecimal

type Query {
hello : String
}
Expand All @@ -17,6 +19,6 @@ type Subscription {
type StockPriceUpdate {
dateTime : String
stockCode : String
stockPrice : Float
stockPriceChange : Float!
stockPrice : BigDecimal
stockPriceChange : BigDecimal!
}
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ SOURCE_COMPATIBILITY=1.8
TARGET_COMPATIBILITY=1.8
### Dependencies
LIB_GRAPHQL_JAVA_VER=16.1
LIB_EXTENDED_SCALARS_VERSION=16.0.0
LIB_SPRING_BOOT_VER=2.4.2
LIB_GRAPHQL_SERVLET_VER=11.0.0
LIB_GRAPHQL_JAVA_TOOLS_VER=11.0.0
Expand Down
1 change: 1 addition & 0 deletions graphql-spring-boot-autoconfigure/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies {
api(project(":graphql-kickstart-spring-boot-starter-tools"))
api(project(":graphql-kickstart-spring-support"))
implementation "org.springframework.boot:spring-boot-autoconfigure"
api "com.graphql-java:graphql-java-extended-scalars:$LIB_EXTENDED_SCALARS_VERSION"
api "com.graphql-java-kickstart:graphql-java-kickstart:$LIB_GRAPHQL_SERVLET_VER"
api "com.graphql-java-kickstart:graphql-java-servlet:$LIB_GRAPHQL_SERVLET_VER"
api "com.graphql-java:graphql-java:$LIB_GRAPHQL_JAVA_VER"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package graphql.kickstart.spring.web.boot;

import graphql.scalars.ExtendedScalars;
import graphql.schema.GraphQLScalarType;
import lombok.NoArgsConstructor;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

@NoArgsConstructor
public class GraphQLExtendedScalarsInitializer implements ApplicationContextInitializer<GenericApplicationContext> {

@Override
public void initialize(final GenericApplicationContext applicationContext) {
final Collection<String> enabledExtendedScalars = getEnabledExtendedScalars(applicationContext);
final Collection<String> validScalarNames = new HashSet<>();
ReflectionUtils.doWithFields(ExtendedScalars.class, scalarField -> {
if (Modifier.isPublic(scalarField.getModifiers()) && Modifier.isStatic(scalarField.getModifiers())
&& scalarField.getType().equals(GraphQLScalarType.class)) {
final GraphQLScalarType graphQLScalarType = (GraphQLScalarType) scalarField.get(null);
if (enabledExtendedScalars.contains(graphQLScalarType.getName())) {
applicationContext.registerBean(
graphQLScalarType.getName(),
GraphQLScalarType.class,
() -> graphQLScalarType
);
}
validScalarNames.add(graphQLScalarType.getName());
}
});
verifyEnabledScalars(enabledExtendedScalars, validScalarNames);
}

private void verifyEnabledScalars(
final Collection<String> enabledExtendedScalars,
final Collection<String> validScalarNames
) {
final Collection<String> invalidScalarNames = new HashSet<>(enabledExtendedScalars);
invalidScalarNames.removeAll(validScalarNames);
if (!invalidScalarNames.isEmpty()) {
throw new ApplicationContextException(String.format(
"Invalid extended scalar name(s) found: %s. Valid names are: %s.",
joinNames(invalidScalarNames),
joinNames(validScalarNames)
)
);
}
}

private String joinNames(final Collection<String> names) {
return names.stream().sorted().collect(Collectors.joining(", "));
}

@SuppressWarnings("unchecked")
private Set<String> getEnabledExtendedScalars(final GenericApplicationContext applicationContext) {
return (Set<String>) applicationContext.getEnvironment()
.getProperty("graphql.extended-scalars", Collection.class, Collections.emptySet())
.stream().map(String::valueOf).collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@
}
],
"properties": [
{
"name": "graphql.extended-scalars",
"type": "java.util.Set",
"description": "List of extended scalars to be used."
}
]
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
org.springframework.context.ApplicationContextInitializer=\
graphql.kickstart.spring.web.boot.GraphQLExtendedScalarsInitializer
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
graphql.kickstart.spring.web.boot.GraphQLWebAutoConfiguration,\
graphql.kickstart.spring.web.boot.GraphQLWebsocketAutoConfiguration,\
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package graphql.kickstart.spring.web.boot.test.extendedscalars;

import graphql.scalars.ExtendedScalars;
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.TestPropertySource;

import java.util.AbstractMap;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = ExtendedScalarAutoConfigurationTest.ExtendedScalarsTestApplication.class
)
@TestPropertySource(properties = "graphql.extended-scalars=BigDecimal")
@DisplayName("Testing extended scalars auto configuration")
public class ExtendedScalarAutoConfigurationTest {

@Autowired
private ApplicationContext applicationContext;

@Test
@DisplayName("The extended scalars initializer should be properly picked up by Spring auto configuration.")
void testAutoConfiguration() {
assertThat(applicationContext.getBeansOfType(GraphQLScalarType.class))
.containsOnly(new AbstractMap.SimpleEntry<>("BigDecimal", ExtendedScalars.GraphQLBigDecimal));
}

@SpringBootApplication
public static class ExtendedScalarsTestApplication {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package graphql.kickstart.spring.web.boot.test.extendedscalars;

import graphql.kickstart.spring.web.boot.GraphQLExtendedScalarsInitializer;
import graphql.scalars.ExtendedScalars;
import graphql.schema.GraphQLScalarType;
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;

import java.util.AbstractMap;
import java.util.Collections;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

@DisplayName("Testing extended scalars configuration")
public class ExtendedScalarsTest {

@Test
@DisplayName("Should throw exception at context initialization when invalid extended scalar name is provided.")
void shouldThrowErrorOnStartupIfExtendedScalarDoesNotExists() {
// GIVEN
final SpringApplication application = setupTestApplication("Long,Short,Datee,BadDecimal");
// THEN
assertThatExceptionOfType(ApplicationContextException.class)
.isThrownBy(application::run)
.withMessage("Invalid extended scalar name(s) found: BadDecimal, Datee. Valid names are: BigDecimal, " +
"BigInteger, Byte, Char, Date, DateTime, JSON, Locale, Long, NegativeFloat, NegativeInt, " +
"NonNegativeFloat, NonNegativeInt, NonPositiveFloat, NonPositiveInt, Object, PositiveFloat, " +
"PositiveInt, Short, Time, Url.");
}

@Test
@DisplayName("Should not create any extended scalars by default.")
void shouldNotDeclareAnyExtendedScalarsByDefault() {
// GIVEN
final SpringApplication application = setupTestApplication(null);
// WHEN
final ConfigurableApplicationContext context = application.run();
// THEN
assertThat(context.getBeansOfType(GraphQLScalarType.class)).isEmpty();
}

@Test
@DisplayName("Should declare the configured extended scalars.")
void shouldDeclareTheConfiguredScalars() {
// GIVEN
final SpringApplication application = setupTestApplication("Long,Short,BigDecimal,Date");
// WHEN
final ConfigurableApplicationContext context = application.run();
// THEN
assertThat(context.getBeansOfType(GraphQLScalarType.class))
.containsOnly(
new AbstractMap.SimpleEntry<>("Long", ExtendedScalars.GraphQLLong),
new AbstractMap.SimpleEntry<>("Short", ExtendedScalars.GraphQLShort),
new AbstractMap.SimpleEntry<>("BigDecimal", ExtendedScalars.GraphQLBigDecimal),
new AbstractMap.SimpleEntry<>("Date", ExtendedScalars.Date)
);
}

private SpringApplication setupTestApplication(final String extendedScalarValue) {
final StandardEnvironment standardEnvironment = new StandardEnvironment();
standardEnvironment.getPropertySources().addFirst(new MapPropertySource("testProperties",
Collections.singletonMap("graphql.extended-scalars", extendedScalarValue)));
final SpringApplication application = new SpringApplication(GraphQLExtendedScalarsInitializer.class);
application.setWebApplicationType(WebApplicationType.NONE);
application.setEnvironment(standardEnvironment);
return application;
}
}