diff --git a/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java b/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java index 92a23fdd..f134809f 100644 --- a/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java +++ b/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java @@ -2,7 +2,9 @@ package io.spring.sample.graphql.repository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.CrudRepository; +import org.springframework.graphql.data.GraphQlRepository; +@GraphQlRepository public interface ArtifactRepositories extends CrudRepository, QuerydslPredicateExecutor { diff --git a/samples/webmvc-http/src/test/java/io/spring/sample/graphql/project/MockMvcGraphQlTests.java b/samples/webmvc-http/src/test/java/io/spring/sample/graphql/project/MockMvcGraphQlTests.java index 04f70bac..8d385ce7 100644 --- a/samples/webmvc-http/src/test/java/io/spring/sample/graphql/project/MockMvcGraphQlTests.java +++ b/samples/webmvc-http/src/test/java/io/spring/sample/graphql/project/MockMvcGraphQlTests.java @@ -86,4 +86,24 @@ public class MockMvcGraphQlTests { .satisfies(project -> assertThat(project.getReleases()).hasSizeGreaterThan(1)); } + @Test + void querydslRepositorySingle() { + String query = "query { artifactRepository(id: \"spring-releases\") { name } }"; + + this.graphQlTester.query(query) + .execute() + .path("artifactRepository.name") + .entity(String.class).isEqualTo("Spring Releases"); + } + + @Test + void querydslRepositoryMany() { + String query = "query { artifactRepositories { id } }"; + + this.graphQlTester.query(query) + .execute() + .path("artifactRepositories[*].id") + .entityList(String.class).containsExactly("spring-releases", "spring-milestones", "spring-snapshots"); + } + } diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index 040a90d0..19dbb5b8 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -333,6 +333,13 @@ If the repository is `ReactiveQuerydslPredicateExecutor`, the builder returns `DataFetcher>` or `DataFetcher>`. Spring Data supports this variant for MongoDB. +The {repository}/samples/webmvc-http[webmvc-http] sample in the Spring GraphQL repository +uses Querydsl to fetch `artifactRepositories`. + + +[[data-querydsl-customizations]] +==== Customizations + The Querydsl integration allows customizing the request parameters binding onto a `Predicate` by accepting a `QuerydslBinderCustomizer`. Request parameters are bound by default as "is equal to" for each available property in the request. @@ -341,8 +348,21 @@ by default as "is equal to" for each available property in the request. https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[interface and DTO projections] to transform query results before returning these for further GraphQL processing. -The {repository}/samples/webmvc-http[webmvc-http] sample in the Spring GraphQL repository -uses Querydsl to fetch `artifactRepositories`. + +[[data-querydsl-registration]] +==== Auto Registration + +`QuerydslDataFetcher` exposes a `GraphQLTypeVisitor` that finds top-level queries whose +return type matches the domain type of one or more Querydsl repositories, and registers +a `DataFetcher` for each matching query. This includes both queries that return a single +value and queries that return a list of values. + +The repository must be annotated with `@GraphQlRepository`. By default, the name of the +GraphQL type returned by the query must match the simple name of the repository domain +type. Of if they don't match, you can use the `typeName` attribute of +`@GraphQlRepository` to set the GraphQL type name. + +Such repositories are auto-detected in the <>. @@ -732,6 +752,16 @@ public class PersonDataWiring implements RuntimeWiringCustomizer { ---- +[[boot-repositories-querydsl]] +=== Querydsl Repositories + +Spring Data repositories that extend `QuerydslPredicateExecutor` or +`ReactiveQuerydslPredicateExecutor` and are annotated with `@GraphQlRepository` are +detected and considered as candidates for `DataFetcher` +<> for matching top-level queries. + + + [[boot-graphql-web]] === Web Endpoints diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlRepository.java b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlRepository.java new file mode 100644 index 00000000..bb47e4e5 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlRepository.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2021 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 org.springframework.graphql.data; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Repository; + +/** + * Specialization of the {@link Repository} stereotype that marks a repository + * as intended for use in a GraphQL application for data fetching. + * + *

A Spring Data repository that is an + * {@link org.springframework.data.querydsl.QuerydslPredicateExecutor} or + * {@link org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor} is + * eligible for auto-binding to queries whose return type matches the repository + * domain type name. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Repository +public @interface GraphQlRepository { + + /** + * Use this to customize the name of the GraphQL type that matches to the + * repository domain type. + *

By default, if this is not specified, then the simple name of the + * repository domain type is used to match to the GraphQL schema type. + */ + String typeName() default ""; + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java index 4067ec33..ac683e41 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java @@ -44,6 +44,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.mapping.model.EntityInstantiators; @@ -66,6 +67,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; /** * Entry point to create {@link DataFetcher} using repositories through Querydsl. @@ -488,7 +490,7 @@ public abstract class QuerydslDataFetcher { /** - * Visitor that auto-registers Querydsl repositories. + * GraphQLTypeVisitor that auto-registers Querydsl Spring Data repositories. */ private static class RegistrationTypeVisitor extends GraphQLTypeVisitorStub { @@ -505,28 +507,45 @@ public abstract class QuerydslDataFetcher { List> executors, List> reactiveExecutors) { - int size = executors.size() + reactiveExecutors.size(); - Map>> map = new HashMap<>(size); + Map>> map = new HashMap<>(); for (QuerydslPredicateExecutor executor : executors) { - Class repositoryInterface = getRepositoryInterface(executor); - RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); - map.put(metadata.getDomainType().getSimpleName(), (single) -> single ? - QuerydslDataFetcher.builder(executor).single() : - QuerydslDataFetcher.builder(executor).many()); + String typeName = getTypeName(executor); + if (typeName != null) { + map.put(typeName, (single) -> single ? + QuerydslDataFetcher.builder(executor).single() : + QuerydslDataFetcher.builder(executor).many()); + } } for (ReactiveQuerydslPredicateExecutor reactiveExecutor : reactiveExecutors) { - Class repositoryInterface = getRepositoryInterface(reactiveExecutor); - RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); - map.put(metadata.getDomainType().getSimpleName(), (single) -> single ? - QuerydslDataFetcher.builder(reactiveExecutor).single() : - QuerydslDataFetcher.builder(reactiveExecutor).many()); + String typeName = getTypeName(reactiveExecutor); + if (typeName != null) { + map.put(typeName, (single) -> single ? + QuerydslDataFetcher.builder(reactiveExecutor).single() : + QuerydslDataFetcher.builder(reactiveExecutor).many()); + } } return map; } + @Nullable + private String getTypeName(Object repository) { + GraphQlRepository annotation = + AnnotatedElementUtils.findMergedAnnotation(repository.getClass(), GraphQlRepository.class); + + if (annotation == null) { + return null; + } + if (StringUtils.hasText(annotation.typeName())) { + return annotation.typeName(); + } + Class repositoryInterface = getRepositoryInterface(repository); + RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); + return metadata.getDomainType().getSimpleName(); + } + @Override public TraversalControl visitGraphQLFieldDefinition( GraphQLFieldDefinition fieldDefinition, TraverserContext context) { diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java index 09b79af3..e318dd79 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java @@ -247,10 +247,12 @@ class QuerydslDataFetcherTests { } + @GraphQlRepository interface MockRepository extends Repository, QuerydslPredicateExecutor { } + @GraphQlRepository interface ReactiveMockRepository extends Repository, ReactiveQuerydslPredicateExecutor { }