Add tests for Neo4j
Also mention Neo4j as compatible with the reactive stack and the QuerydslPredicateExecutor. See gh-734
This commit is contained in:
committed by
rstoyanchev
parent
e9f68948ec
commit
f4c5003376
@@ -50,14 +50,14 @@ You can now register the above `DataFetcher` through a
|
||||
<<execution.graphqlsource.runtimewiring-configurer>>.
|
||||
|
||||
The `DataFetcher` builds a Querydsl `Predicate` from GraphQL arguments, and uses it to
|
||||
fetch data. Spring Data supports `QuerydslPredicateExecutor` for JPA, MongoDB, and LDAP.
|
||||
fetch data. Spring Data supports `QuerydslPredicateExecutor` for JPA, MongoDB, Neo4j, and LDAP.
|
||||
|
||||
NOTE: For a single argument that is a GraphQL input type, `QuerydslDataFetcher` nests one
|
||||
level down, and uses the values from the argument sub-map.
|
||||
|
||||
If the repository is `ReactiveQuerydslPredicateExecutor`, the builder returns
|
||||
`DataFetcher<Mono<Account>>` or `DataFetcher<Flux<Account>>`. Spring Data supports this
|
||||
variant for MongoDB.
|
||||
variant for MongoDB and Neo4j.
|
||||
|
||||
|
||||
[[data.querydsl.build]]
|
||||
|
||||
@@ -51,9 +51,11 @@ dependencies {
|
||||
testImplementation 'org.hibernate:hibernate-core'
|
||||
testImplementation 'org.hibernate.validator:hibernate-validator'
|
||||
testImplementation 'org.springframework.data:spring-data-mongodb'
|
||||
testImplementation 'org.springframework.data:spring-data-neo4j:7.1.1'
|
||||
testImplementation 'org.mongodb:mongodb-driver-sync'
|
||||
testImplementation 'org.mongodb:mongodb-driver-reactivestreams'
|
||||
testImplementation 'org.testcontainers:mongodb'
|
||||
testImplementation 'org.testcontainers:neo4j'
|
||||
testImplementation 'org.testcontainers:junit-jupiter'
|
||||
testImplementation 'org.springframework.security:spring-security-core'
|
||||
testImplementation 'com.querydsl:querydsl-core'
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.springframework.graphql.data.query.neo4j;
|
||||
|
||||
import org.springframework.data.neo4j.core.schema.Id;
|
||||
import org.springframework.data.neo4j.core.schema.Node;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@Node
|
||||
public class Author {
|
||||
|
||||
@Id
|
||||
String id;
|
||||
|
||||
String firstName;
|
||||
|
||||
String lastName;
|
||||
|
||||
// needed for response mapping
|
||||
public Author() {
|
||||
|
||||
}
|
||||
|
||||
public Author(String id, String firstName, String lastName) {
|
||||
this.id = id;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return this.firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return this.lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return this.firstName + " " + this.lastName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020-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.query.neo4j;
|
||||
|
||||
import org.springframework.data.neo4j.core.schema.Id;
|
||||
import org.springframework.data.neo4j.core.schema.Node;
|
||||
import org.springframework.data.neo4j.core.schema.Relationship;
|
||||
|
||||
@Node
|
||||
public class Book {
|
||||
|
||||
@Id
|
||||
String id;
|
||||
|
||||
String name;
|
||||
|
||||
@Relationship("AUTHOR")
|
||||
Author author;
|
||||
|
||||
// needed for response mapping
|
||||
public Book() {
|
||||
|
||||
}
|
||||
|
||||
public Book(String id, String name, Author author) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Author getAuthor() {
|
||||
return this.author;
|
||||
}
|
||||
|
||||
public void setAuthor(Author author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.springframework.graphql.data.query.neo4j;
|
||||
|
||||
import org.springframework.data.neo4j.repository.Neo4jRepository;
|
||||
import org.springframework.graphql.data.GraphQlRepository;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@GraphQlRepository
|
||||
public interface BookNeo4jRepository extends Neo4jRepository<Book, String> {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.springframework.graphql.data.query.neo4j;
|
||||
|
||||
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
|
||||
import org.springframework.graphql.data.GraphQlRepository;
|
||||
|
||||
/**
|
||||
* @author Gerrit Meier
|
||||
*/
|
||||
@GraphQlRepository
|
||||
public interface BookReactiveNeo4jRepository extends ReactiveNeo4jRepository<Book, String> {
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* http://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.query.neo4j;
|
||||
|
||||
import graphql.schema.DataFetcher;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.driver.AuthTokens;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.GraphDatabase;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
|
||||
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
|
||||
import org.springframework.data.repository.query.QueryByExampleExecutor;
|
||||
import org.springframework.graphql.BookSource;
|
||||
import org.springframework.graphql.GraphQlSetup;
|
||||
import org.springframework.graphql.ResponseHelper;
|
||||
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
|
||||
import org.springframework.graphql.data.query.ScrollPositionCursorStrategy;
|
||||
import org.springframework.graphql.data.query.ScrollSubrange;
|
||||
import org.springframework.graphql.data.query.WindowConnectionAdapter;
|
||||
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.WebGraphQlRequest;
|
||||
import org.springframework.graphql.server.WebGraphQlResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.testcontainers.containers.Neo4jContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link QueryByExampleDataFetcher}.
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class QueryByExampleDataFetcherNeo4jTests {
|
||||
|
||||
@Container
|
||||
static Neo4jContainer neo4jContainer = new Neo4jContainer(DockerImageName.parse("neo4j:5")).withRandomPassword();
|
||||
|
||||
@Autowired
|
||||
private BookNeo4jRepository repository;
|
||||
|
||||
|
||||
@Test
|
||||
void shouldFetchSingleItems() {
|
||||
Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
|
||||
repository.save(book);
|
||||
|
||||
Consumer<GraphQlSetup> tester = setup -> {
|
||||
WebGraphQlRequest request = request("{ bookById(id: 42) {name}}");
|
||||
Mono<WebGraphQlResponse> responseMono = setup.toWebGraphQlHandler().handleRequest(request);
|
||||
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
|
||||
|
||||
assertThat(actualBook.getName()).isEqualTo(book.getName());
|
||||
};
|
||||
|
||||
// explicit wiring
|
||||
tester.accept(graphQlSetup("bookById", QueryByExampleDataFetcher.builder(repository).single()));
|
||||
|
||||
// auto registration
|
||||
tester.accept(graphQlSetup(repository));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFetchMultipleItems() {
|
||||
Book book1 = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
|
||||
Book book2 = new Book("53", "Breaking Bad", new Author("0", "", "Heisenberg"));
|
||||
repository.saveAll(Arrays.asList(book1, book2));
|
||||
|
||||
Consumer<GraphQlSetup> tester = graphQlSetup -> {
|
||||
WebGraphQlRequest request = request("{ books {name}}");
|
||||
Mono<WebGraphQlResponse> responseMono = graphQlSetup.toWebGraphQlHandler().handleRequest(request);
|
||||
|
||||
List<String> names = ResponseHelper.forResponse(responseMono).toList("books", Book.class)
|
||||
.stream().map(Book::getName).collect(Collectors.toList());
|
||||
|
||||
assertThat(names).containsExactlyInAnyOrder(book1.getName(), book2.getName());
|
||||
};
|
||||
|
||||
// explicit wiring
|
||||
tester.accept(graphQlSetup("books", QueryByExampleDataFetcher.builder(repository).many()));
|
||||
|
||||
// auto registration
|
||||
tester.accept(graphQlSetup(repository));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFetchWindow() {
|
||||
|
||||
repository.saveAll(List.of(
|
||||
new Book("1", "Nineteen Eighty-Four", new Author("0", "George", "Orwell")),
|
||||
new Book("2", "The Great Gatsby", new Author("0", "F. Scott", "Fitzgerald")),
|
||||
new Book("3", "Catch-22", new Author("0", "Joseph", "Heller")),
|
||||
new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams")),
|
||||
new Book("53", "Breaking Bad", new Author("0", "", "Heisenberg"))));
|
||||
|
||||
Consumer<GraphQlSetup> tester = graphQlSetup -> {
|
||||
|
||||
Mono<WebGraphQlResponse> response = graphQlSetup
|
||||
.toWebGraphQlHandler()
|
||||
.handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\"")));
|
||||
|
||||
List<Map<String, Object>> edges = ResponseHelper.forResponse(response).toEntity("books.edges", List.class);
|
||||
assertThat(edges.size()).isEqualTo(2);
|
||||
assertThat(edges.get(0).get("cursor")).isEqualTo("O_4");
|
||||
assertThat(edges.get(1).get("cursor")).isEqualTo("O_5");
|
||||
|
||||
Map<String, Object> pageInfo = ResponseHelper.forResponse(response).toEntity("books.pageInfo", Map.class);
|
||||
assertThat(pageInfo.size()).isEqualTo(4);
|
||||
assertThat(pageInfo.get("startCursor")).isEqualTo("O_4");
|
||||
assertThat(pageInfo.get("endCursor")).isEqualTo("O_5");
|
||||
assertThat(pageInfo.get("hasPreviousPage")).isEqualTo(true);
|
||||
assertThat(pageInfo.get("hasNextPage")).isEqualTo(false);
|
||||
};
|
||||
|
||||
// explicit wiring
|
||||
|
||||
ScrollPositionCursorStrategy cursorStrategy = new ScrollPositionCursorStrategy();
|
||||
|
||||
DataFetcher<Iterable<Book>> dataFetcher =
|
||||
QueryByExampleDataFetcher.builder(repository).cursorStrategy(cursorStrategy).scrollable();
|
||||
|
||||
GraphQlSetup graphQlSetup = paginationSetup(cursorStrategy).queryFetcher("books", dataFetcher);
|
||||
tester.accept(graphQlSetup);
|
||||
|
||||
// auto registration
|
||||
graphQlSetup = paginationSetup(cursorStrategy).runtimeWiring(createRuntimeWiringConfigurer(repository));
|
||||
tester.accept(graphQlSetup);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFavorExplicitWiring() {
|
||||
BookNeo4jRepository mockRepository = mock(BookNeo4jRepository.class);
|
||||
Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
|
||||
when(mockRepository.findBy(any(), any())).thenReturn(Optional.of(book));
|
||||
|
||||
// 1) Automatic registration only
|
||||
WebGraphQlHandler handler = graphQlSetup(mockRepository).toWebGraphQlHandler();
|
||||
Mono<WebGraphQlResponse> responseMono = handler.handleRequest(request("{ bookById(id: 1) {name}}"));
|
||||
|
||||
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
|
||||
assertThat(actualBook.getName()).isEqualTo("Hitchhiker's Guide to the Galaxy");
|
||||
|
||||
// 2) Automatic registration and explicit wiring
|
||||
handler = graphQlSetup(mockRepository)
|
||||
.queryFetcher("bookById", env -> new Book("53", "Breaking Bad", new Author("0", "", "Heisenberg")))
|
||||
.toWebGraphQlHandler();
|
||||
|
||||
responseMono = handler.handleRequest(request("{ bookById(id: 1) {name}}"));
|
||||
|
||||
actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
|
||||
assertThat(actualBook.getName()).isEqualTo("Breaking Bad");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFetchSingleItemsWithInterfaceProjection() {
|
||||
Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
|
||||
repository.save(book);
|
||||
|
||||
DataFetcher<?> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookProjection.class).single();
|
||||
WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
|
||||
|
||||
Mono<WebGraphQlResponse> responseMono = handler.handleRequest(request("{ bookById(id: 42) {name}}"));
|
||||
|
||||
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
|
||||
assertThat(actualBook.getName()).isEqualTo("Hitchhiker's Guide to the Galaxy by Douglas Adams");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFetchSingleItemsWithDtoProjection() {
|
||||
Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
|
||||
repository.save(book);
|
||||
|
||||
DataFetcher<?> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookDto.class).single();
|
||||
WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
|
||||
|
||||
Mono<WebGraphQlResponse> responseMono = handler.handleRequest(request("{ bookById(id: 42) {name}}"));
|
||||
|
||||
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
|
||||
assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
|
||||
}
|
||||
|
||||
private static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
|
||||
return GraphQlSetup.schemaResource(BookSource.schema).queryFetcher(fieldName, fetcher);
|
||||
}
|
||||
|
||||
private static GraphQlSetup graphQlSetup(@Nullable QueryByExampleExecutor<?> executor) {
|
||||
return GraphQlSetup.schemaResource(BookSource.schema)
|
||||
.runtimeWiring(createRuntimeWiringConfigurer(executor));
|
||||
}
|
||||
|
||||
private static GraphQlSetup paginationSetup(ScrollPositionCursorStrategy cursorStrategy) {
|
||||
return GraphQlSetup.schemaResource(BookSource.paginationSchema)
|
||||
.connectionSupport(new WindowConnectionAdapter(cursorStrategy));
|
||||
}
|
||||
|
||||
private static RuntimeWiringConfigurer createRuntimeWiringConfigurer(QueryByExampleExecutor<?> executor) {
|
||||
return QueryByExampleDataFetcher.autoRegistrationConfigurer(
|
||||
(executor != null ? Collections.singletonList(executor) : Collections.emptyList()),
|
||||
Collections.emptyList(),
|
||||
new ScrollPositionCursorStrategy(),
|
||||
new ScrollSubrange(ScrollPosition.offset(), 10, true));
|
||||
}
|
||||
|
||||
private WebGraphQlRequest request(String query) {
|
||||
return new WebGraphQlRequest(
|
||||
URI.create("/"), new HttpHeaders(), null, Collections.emptyMap(),
|
||||
Collections.singletonMap("query", query), "1", null);
|
||||
}
|
||||
|
||||
|
||||
interface BookProjection {
|
||||
|
||||
@Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")
|
||||
String getName();
|
||||
}
|
||||
|
||||
|
||||
static class BookDto {
|
||||
|
||||
private final String name;
|
||||
|
||||
public BookDto(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "The book is: " + name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableNeo4jRepositories(considerNestedRepositories = true)
|
||||
static class TestConfig extends AbstractNeo4jConfig {
|
||||
|
||||
@Bean
|
||||
public Driver driver() {
|
||||
return GraphDatabase.driver(neo4jContainer.getBoltUrl(), AuthTokens.basic("neo4j", neo4jContainer.getAdminPassword()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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
|
||||
*
|
||||
* http://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.query.neo4j;
|
||||
|
||||
import graphql.schema.DataFetcher;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.cypherdsl.core.renderer.Dialect;
|
||||
import org.neo4j.driver.AuthTokens;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.GraphDatabase;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig;
|
||||
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories;
|
||||
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
|
||||
import org.springframework.graphql.BookSource;
|
||||
import org.springframework.graphql.GraphQlSetup;
|
||||
import org.springframework.graphql.ResponseHelper;
|
||||
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
|
||||
import org.springframework.graphql.data.query.ScrollPositionCursorStrategy;
|
||||
import org.springframework.graphql.data.query.ScrollSubrange;
|
||||
import org.springframework.graphql.data.query.WindowConnectionAdapter;
|
||||
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.WebGraphQlRequest;
|
||||
import org.springframework.graphql.server.WebGraphQlResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.testcontainers.containers.Neo4jContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link QueryByExampleDataFetcher}.
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class QueryByExampleDataFetcherReactiveNeo4jDbTests {
|
||||
|
||||
@Container
|
||||
static Neo4jContainer neo4jContainer = new Neo4jContainer(DockerImageName.parse("neo4j:5")).withRandomPassword();
|
||||
|
||||
@Autowired
|
||||
private BookReactiveNeo4jRepository repository;
|
||||
|
||||
@Autowired
|
||||
private Driver driver;
|
||||
|
||||
@BeforeEach
|
||||
void cleanDatabase() {
|
||||
try (var session = driver.session()) {
|
||||
session.run("MATCH (n) detach delete n").consume();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReactivelyFetchSingleItems() {
|
||||
Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
|
||||
repository.save(book).block();
|
||||
|
||||
Consumer<GraphQlSetup> tester = setup -> {
|
||||
WebGraphQlRequest request = request("{ bookById(id: 42) {name}}");
|
||||
Mono<WebGraphQlResponse> responseMono = setup.toWebGraphQlHandler().handleRequest(request);
|
||||
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
|
||||
|
||||
assertThat(actualBook.getName()).isEqualTo(book.getName());
|
||||
};
|
||||
|
||||
// explicit wiring
|
||||
tester.accept(graphQlSetup("bookById", QueryByExampleDataFetcher.builder(repository).single()));
|
||||
|
||||
// auto registration
|
||||
tester.accept(graphQlSetup(repository));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFetchSingleItemsReactivelyWithInterfaceProjection() {
|
||||
Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
|
||||
repository.save(book).block();
|
||||
|
||||
DataFetcher<?> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookProjection.class).single();
|
||||
WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
|
||||
|
||||
Mono<WebGraphQlResponse> responseMono = handler.handleRequest(request("{ bookById(id: 42) {name}}"));
|
||||
|
||||
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
|
||||
assertThat(actualBook.getName()).isEqualTo("Hitchhiker's Guide to the Galaxy by Douglas Adams");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFetchSingleItemsReactivelyWithDtoProjection() {
|
||||
Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
|
||||
repository.save(book).block();
|
||||
|
||||
DataFetcher<?> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookDto.class).single();
|
||||
WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
|
||||
|
||||
Mono<WebGraphQlResponse> responseMono = handler.handleRequest(request("{ bookById(id: 42) {name}}"));
|
||||
|
||||
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
|
||||
assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReactivelyFetchMultipleItems() {
|
||||
Book book1 = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
|
||||
Book book2 = new Book("53", "Breaking Bad", new Author("0", "", "Heisenberg"));
|
||||
repository.saveAll(Flux.just(book1, book2)).blockLast();
|
||||
|
||||
Consumer<GraphQlSetup> tester = setup -> {
|
||||
WebGraphQlRequest request = request("{ books {name}}");
|
||||
Mono<WebGraphQlResponse> responseMono = setup.toWebGraphQlHandler().handleRequest(request);
|
||||
|
||||
List<String> names = ResponseHelper.forResponse(responseMono).toList("books", Book.class)
|
||||
.stream().map(Book::getName).collect(Collectors.toList());
|
||||
|
||||
assertThat(names).containsExactlyInAnyOrder("Breaking Bad", "Hitchhiker's Guide to the Galaxy");
|
||||
};
|
||||
|
||||
// explicit wiring
|
||||
tester.accept(graphQlSetup("books", QueryByExampleDataFetcher.builder(repository).many()));
|
||||
|
||||
// auto registration
|
||||
tester.accept(graphQlSetup(repository));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFetchWindow() {
|
||||
|
||||
repository.saveAll(Flux.just(
|
||||
new Book("1", "Nineteen Eighty-Four", new Author("0", "George", "Orwell")),
|
||||
new Book("2", "The Great Gatsby", new Author("1", "F. Scott", "Fitzgerald")),
|
||||
new Book("3", "Catch-22", new Author("2", "Joseph", "Heller")),
|
||||
new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("3", "Douglas", "Adams")),
|
||||
new Book("53", "Breaking Bad", new Author("4", "", "Heisenberg"))))
|
||||
.blockLast();
|
||||
|
||||
Consumer<GraphQlSetup> tester = graphQlSetup -> {
|
||||
|
||||
Mono<WebGraphQlResponse> response = graphQlSetup
|
||||
.toWebGraphQlHandler()
|
||||
.handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\"")));
|
||||
|
||||
List<Map<String, Object>> edges = ResponseHelper.forResponse(response).toEntity("books.edges", List.class);
|
||||
assertThat(edges.size()).isEqualTo(2);
|
||||
assertThat(edges.get(0).get("cursor")).isEqualTo("O_4");
|
||||
assertThat(edges.get(1).get("cursor")).isEqualTo("O_5");
|
||||
|
||||
Map<String, Object> pageInfo = ResponseHelper.forResponse(response).toEntity("books.pageInfo", Map.class);
|
||||
assertThat(pageInfo.size()).isEqualTo(4);
|
||||
assertThat(pageInfo.get("startCursor")).isEqualTo("O_4");
|
||||
assertThat(pageInfo.get("endCursor")).isEqualTo("O_5");
|
||||
assertThat(pageInfo.get("hasPreviousPage")).isEqualTo(true);
|
||||
assertThat(pageInfo.get("hasNextPage")).isEqualTo(false);
|
||||
};
|
||||
|
||||
// explicit wiring
|
||||
|
||||
ScrollPositionCursorStrategy cursorStrategy = new ScrollPositionCursorStrategy();
|
||||
|
||||
DataFetcher<Mono<Iterable<Book>>> dataFetcher =
|
||||
QueryByExampleDataFetcher.builder(repository).cursorStrategy(cursorStrategy).scrollable();
|
||||
|
||||
GraphQlSetup graphQlSetup = paginationSetup(cursorStrategy).queryFetcher("books", dataFetcher);
|
||||
tester.accept(graphQlSetup);
|
||||
|
||||
// auto registration
|
||||
graphQlSetup = paginationSetup(cursorStrategy).runtimeWiring(createRuntimeWiringConfigurer(repository));
|
||||
tester.accept(graphQlSetup);
|
||||
}
|
||||
|
||||
private static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
|
||||
return GraphQlSetup.schemaResource(BookSource.schema).queryFetcher(fieldName, fetcher);
|
||||
}
|
||||
|
||||
private static GraphQlSetup graphQlSetup(@Nullable ReactiveQueryByExampleExecutor<?> executor) {
|
||||
return GraphQlSetup.schemaResource(BookSource.schema)
|
||||
.runtimeWiring(createRuntimeWiringConfigurer(executor));
|
||||
}
|
||||
|
||||
private static GraphQlSetup paginationSetup(ScrollPositionCursorStrategy cursorStrategy) {
|
||||
return GraphQlSetup.schemaResource(BookSource.paginationSchema)
|
||||
.connectionSupport(new WindowConnectionAdapter(cursorStrategy));
|
||||
}
|
||||
|
||||
private static RuntimeWiringConfigurer createRuntimeWiringConfigurer(ReactiveQueryByExampleExecutor<?> executor) {
|
||||
return QueryByExampleDataFetcher.autoRegistrationConfigurer(
|
||||
Collections.emptyList(),
|
||||
(executor != null ? Collections.singletonList(executor) : Collections.emptyList()),
|
||||
new ScrollPositionCursorStrategy(),
|
||||
new ScrollSubrange(ScrollPosition.offset(), 10, true));
|
||||
}
|
||||
|
||||
private WebGraphQlRequest request(String query) {
|
||||
return new WebGraphQlRequest(
|
||||
URI.create("/"), new HttpHeaders(), null, Collections.emptyMap(),
|
||||
Collections.singletonMap("query", query), "1", null);
|
||||
}
|
||||
|
||||
|
||||
interface BookProjection {
|
||||
|
||||
@Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")
|
||||
String getName();
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class BookDto {
|
||||
|
||||
private final String name;
|
||||
|
||||
public BookDto(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "The book is: " + name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableReactiveNeo4jRepositories(considerNestedRepositories = true)
|
||||
static class TestConfig extends AbstractReactiveNeo4jConfig {
|
||||
|
||||
@Bean
|
||||
public Driver driver() {
|
||||
return GraphDatabase.driver(neo4jContainer.getBoltUrl(), AuthTokens.basic("neo4j", neo4jContainer.getAdminPassword()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public org.neo4j.cypherdsl.core.renderer.Configuration cypherDslConfiguration() {
|
||||
return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig()
|
||||
.withDialect(Dialect.NEO4J_5)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user