From c78f05626a122e281b27a3418d4b19cd149e7d5a Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 13 Nov 2020 11:52:30 +0100 Subject: [PATCH] Add graphQL sample application This commit adds a sample application to the repository. This application should not be published as part of the build. In this application, we're trying to showcase the various features and use cases with our Spring graphQL integration. We're mixing here data fetchers backed by datastores or remote hypermedia APIs. Closes gh-15 --- graphql-sample/build.gradle | 23 ++++++++ .../graphql/GraphQLSampleApplication.java | 27 +++++++++ .../sample/graphql/project/Project.java | 56 ++++++++++++++++++ .../graphql/project/ProjectDataWiring.java | 32 ++++++++++ .../sample/graphql/project/ProjectStatus.java | 16 +++++ .../sample/graphql/project/Release.java | 54 +++++++++++++++++ .../sample/graphql/project/ReleaseStatus.java | 16 +++++ .../graphql/project/SpringProjectsClient.java | 47 +++++++++++++++ .../repository/ArtifactRepositories.java | 7 +++ .../ArtifactRepositoriesInitializer.java | 28 +++++++++ .../repository/ArtifactRepository.java | 58 +++++++++++++++++++ .../ArtifactRepositoryDataWiring.java | 23 ++++++++ .../src/main/resources/application.properties | 1 + .../src/main/resources/schema.graphqls | 43 ++++++++++++++ settings.gradle | 1 + 15 files changed, 432 insertions(+) create mode 100644 graphql-sample/build.gradle create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/GraphQLSampleApplication.java create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/project/Project.java create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/project/ProjectDataWiring.java create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/project/ProjectStatus.java create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/project/Release.java create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/project/ReleaseStatus.java create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/project/SpringProjectsClient.java create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoriesInitializer.java create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepository.java create mode 100644 graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java create mode 100644 graphql-sample/src/main/resources/application.properties create mode 100644 graphql-sample/src/main/resources/schema.graphqls diff --git a/graphql-sample/build.gradle b/graphql-sample/build.gradle new file mode 100644 index 00000000..acd1592c --- /dev/null +++ b/graphql-sample/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'org.springframework.boot' version '2.4.0-SNAPSHOT' + id 'io.spring.dependency-management' version '1.0.10.RELEASE' + id 'java' +} +group = 'com.example' +version = '0.0.1-SNAPSHOT' +description = "GraphQL sample application" +sourceCompatibility = '1.8' + +dependencies { + implementation project(':spring-graphql-web') + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-hateoas' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'com.h2database:h2' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/GraphQLSampleApplication.java b/graphql-sample/src/main/java/io/spring/sample/graphql/GraphQLSampleApplication.java new file mode 100644 index 00000000..d25264c3 --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/GraphQLSampleApplication.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.sample.graphql; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GraphQLSampleApplication { + public static void main(String[] args) { + SpringApplication.run(GraphQLSampleApplication.class, args); + } +} diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/project/Project.java b/graphql-sample/src/main/java/io/spring/sample/graphql/project/Project.java new file mode 100644 index 00000000..2f3f1742 --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/project/Project.java @@ -0,0 +1,56 @@ +package io.spring.sample.graphql.project; + +import java.util.List; + +public class Project { + + private String slug; + + private String name; + + private String repositoryUrl; + + private ProjectStatus status; + + private List releases; + + public String getSlug() { + return this.slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getRepositoryUrl() { + return this.repositoryUrl; + } + + public void setRepositoryUrl(String repositoryUrl) { + this.repositoryUrl = repositoryUrl; + } + + public ProjectStatus getStatus() { + return this.status; + } + + public void setStatus(ProjectStatus status) { + this.status = status; + } + + public List getReleases() { + return this.releases; + } + + public void setReleases(List releases) { + this.releases = releases; + } +} diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/project/ProjectDataWiring.java b/graphql-sample/src/main/java/io/spring/sample/graphql/project/ProjectDataWiring.java new file mode 100644 index 00000000..bfc73ddd --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/project/ProjectDataWiring.java @@ -0,0 +1,32 @@ +package io.spring.sample.graphql.project; + +import graphql.schema.idl.RuntimeWiring; + +import org.springframework.boot.graphql.RuntimeWiringCustomizer; +import org.springframework.stereotype.Component; + +@Component +public class ProjectDataWiring implements RuntimeWiringCustomizer { + + private final SpringProjectsClient client; + + public ProjectDataWiring(SpringProjectsClient client) { + this.client = client; + } + + @Override + public void customize(RuntimeWiring.Builder builder) { + builder + .type("QueryType", typeWiring -> + typeWiring.dataFetcher("project", env -> { + String slug = env.getArgument("slug"); + return client.fetchProject(slug); + })) + .type("Project", typeWiring -> + typeWiring.dataFetcher("releases", env -> { + Project project = env.getSource(); + return client.fetchProjectReleases(project.getSlug()); + }) + ); + } +} diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/project/ProjectStatus.java b/graphql-sample/src/main/java/io/spring/sample/graphql/project/ProjectStatus.java new file mode 100644 index 00000000..0f37b2cd --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/project/ProjectStatus.java @@ -0,0 +1,16 @@ +package io.spring.sample.graphql.project; + +import java.util.Arrays; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum ProjectStatus { + ACTIVE, COMMUNITY, INCUBATING, ATTIC; + + @JsonCreator + public static ProjectStatus fromName(String name) { + return Arrays.stream(ProjectStatus.values()) + .filter(type -> type.name().equals(name)) + .findFirst().orElse(ProjectStatus.ACTIVE); + } +} diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/project/Release.java b/graphql-sample/src/main/java/io/spring/sample/graphql/project/Release.java new file mode 100644 index 00000000..db5c17b6 --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/project/Release.java @@ -0,0 +1,54 @@ +package io.spring.sample.graphql.project; + +public class Release { + + private String version; + + private ReleaseStatus status; + + private String referenceDocUrl; + + private String apiDocUrl; + + private boolean current; + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + + public ReleaseStatus getStatus() { + return this.status; + } + + public void setStatus(ReleaseStatus status) { + this.status = status; + } + + public String getReferenceDocUrl() { + return this.referenceDocUrl; + } + + public void setReferenceDocUrl(String referenceDocUrl) { + this.referenceDocUrl = referenceDocUrl; + } + + public String getApiDocUrl() { + return this.apiDocUrl; + } + + public void setApiDocUrl(String apiDocUrl) { + this.apiDocUrl = apiDocUrl; + } + + public boolean isCurrent() { + return this.current; + } + + public void setCurrent(boolean current) { + this.current = current; + } +} diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/project/ReleaseStatus.java b/graphql-sample/src/main/java/io/spring/sample/graphql/project/ReleaseStatus.java new file mode 100644 index 00000000..8c36053c --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/project/ReleaseStatus.java @@ -0,0 +1,16 @@ +package io.spring.sample.graphql.project; + +import java.util.Arrays; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum ReleaseStatus { + GENERAL_AVAILABILITY, MILESTONE, SNAPSHOT; + + @JsonCreator + public static ReleaseStatus fromName(String name) { + return Arrays.stream(ReleaseStatus.values()) + .filter(type -> type.name().equals(name)) + .findFirst().orElse(ReleaseStatus.GENERAL_AVAILABILITY); + } +} diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/project/SpringProjectsClient.java b/graphql-sample/src/main/java/io/spring/sample/graphql/project/SpringProjectsClient.java new file mode 100644 index 00000000..c2463df4 --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/project/SpringProjectsClient.java @@ -0,0 +1,47 @@ +package io.spring.sample.graphql.project; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.client.Hop; +import org.springframework.hateoas.client.Traverson; +import org.springframework.hateoas.server.core.TypeReferences; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class SpringProjectsClient { + + private static final TypeReferences.CollectionModelType releaseCollection = + new TypeReferences.CollectionModelType() { }; + + private final Traverson traverson; + + public SpringProjectsClient(RestTemplateBuilder builder) { + RestTemplate restTemplate = builder.messageConverters(Traverson.getDefaultMessageConverters(MediaTypes.HAL_JSON)).build(); + this.traverson = new Traverson(URI.create("https://spring.io/api/"), MediaTypes.HAL_JSON); + this.traverson.setRestOperations(restTemplate); + } + + public Project fetchProject(String projectSlug) { + return this.traverson.follow("projects") + .follow(Hop.rel("project").withParameter("id", projectSlug)) + .toObject(Project.class); + } + + public List fetchProjectReleases(String projectSlug) { + CollectionModel releases = this.traverson.follow("projects") + .follow(Hop.rel("project").withParameter("id", projectSlug)) + .follow(Hop.rel("releases")) + .toObject(releaseCollection); + return new ArrayList(releases.getContent()); + } + +} diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java b/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java new file mode 100644 index 00000000..44cd614e --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java @@ -0,0 +1,7 @@ +package io.spring.sample.graphql.repository; + +import org.springframework.data.repository.CrudRepository; + +public interface ArtifactRepositories extends CrudRepository { + +} diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoriesInitializer.java b/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoriesInitializer.java new file mode 100644 index 00000000..46b2fc74 --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoriesInitializer.java @@ -0,0 +1,28 @@ +package io.spring.sample.graphql.repository; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +@Component +public class ArtifactRepositoriesInitializer implements ApplicationRunner { + + private final ArtifactRepositories repositories; + + public ArtifactRepositoriesInitializer(ArtifactRepositories repositories) { + this.repositories = repositories; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + List repositoryList = Arrays.asList( + new ArtifactRepository("spring-releases", "Spring Releases", "https://repo.spring.io/libs-releases"), + new ArtifactRepository("spring-milestones", "Spring Milestones", "https://repo.spring.io/libs-milestones"), + new ArtifactRepository("spring-snapshots", "Spring Snapshots", "https://repo.spring.io/libs-snapshots") + ); + repositories.saveAll(repositoryList); + } +} diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepository.java b/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepository.java new file mode 100644 index 00000000..60751f9e --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepository.java @@ -0,0 +1,58 @@ +package io.spring.sample.graphql.repository; + +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +public class ArtifactRepository { + + @Id + private String id; + + private String name; + + private String url; + + private boolean snapshotsEnabled; + + public ArtifactRepository(String id, String name, String url) { + this.id = id; + this.name = name; + this.url = url; + } + + public ArtifactRepository() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public boolean isSnapshotsEnabled() { + return snapshotsEnabled; + } + + public void setSnapshotsEnabled(boolean snapshotsEnabled) { + this.snapshotsEnabled = snapshotsEnabled; + } +} diff --git a/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java b/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java new file mode 100644 index 00000000..830c20d4 --- /dev/null +++ b/graphql-sample/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java @@ -0,0 +1,23 @@ +package io.spring.sample.graphql.repository; + +import graphql.schema.idl.RuntimeWiring; + +import org.springframework.boot.graphql.RuntimeWiringCustomizer; +import org.springframework.stereotype.Component; + +@Component +public class ArtifactRepositoryDataWiring implements RuntimeWiringCustomizer { + + private final ArtifactRepositories repositories; + + public ArtifactRepositoryDataWiring(ArtifactRepositories repositories) { + this.repositories = repositories; + } + + @Override + public void customize(RuntimeWiring.Builder builder) { + builder.type("QueryType", typeWiring -> typeWiring + .dataFetcher("artifactRepositories", env -> this.repositories.findAll()) + .dataFetcher("artifactRepository", env -> this.repositories.findById(env.getArgument("id")))); + } +} diff --git a/graphql-sample/src/main/resources/application.properties b/graphql-sample/src/main/resources/application.properties new file mode 100644 index 00000000..d589e7c7 --- /dev/null +++ b/graphql-sample/src/main/resources/application.properties @@ -0,0 +1 @@ +management.endpoints.web.exposure.include=health,metrics,info \ No newline at end of file diff --git a/graphql-sample/src/main/resources/schema.graphqls b/graphql-sample/src/main/resources/schema.graphqls new file mode 100644 index 00000000..c46733ce --- /dev/null +++ b/graphql-sample/src/main/resources/schema.graphqls @@ -0,0 +1,43 @@ +schema { + query: QueryType +} + +type QueryType { + artifactRepositories : [ArtifactRepository] + artifactRepository(id : ID!) : ArtifactRepository + project(slug: ID!): Project +} + +type ArtifactRepository { + id: ID! + name: String! + url: String! + snapshotsEnabled: Boolean +} + +type Project { + slug: ID! + name: String! + repositoryUrl: String! + status: ProjectStatus! + releases: [Release] +} + +type Release { + version: String! + status: ReleaseStatus! + current: Boolean +} + +enum ProjectStatus { + ACTIVE + COMMUNITY + INCUBATING + ATTIC +} + +enum ReleaseStatus { + GENERAL_AVAILABILITY + MILESTONE + SNAPSHOT +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index f561df35..21a5c37d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,3 +15,4 @@ pluginManagement { rootProject.name = 'spring-graphql' include 'spring-graphql-web' +include 'graphql-sample'