diff --git a/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/GraphqlPagination.java b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/GraphqlPagination.java new file mode 100644 index 000000000000..2e78dd51c25e --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/GraphqlPagination.java @@ -0,0 +1,12 @@ +package com.baeldung.pagination; + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GraphqlPagination { + public static void main(String[] args) { + SpringApplication.run(GraphqlPagination.class, args); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/BookConnection.java b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/BookConnection.java new file mode 100644 index 000000000000..c0db70b69fcb --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/BookConnection.java @@ -0,0 +1,16 @@ +package com.baeldung.pagination.dto; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class BookConnection { + private final List edges; + private final PageInfo pageInfo; + + public BookConnection(List edges, PageInfo pageInfo) { + this.edges = edges; + this.pageInfo = pageInfo; + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/BookEdge.java b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/BookEdge.java new file mode 100644 index 000000000000..d688aad172c6 --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/BookEdge.java @@ -0,0 +1,16 @@ +package com.baeldung.pagination.dto; + +import com.baeldung.pagination.entity.Book; + +import lombok.Getter; + +@Getter +public class BookEdge { + private final Book node; + private final String cursor; + + public BookEdge(Book node, String cursor) { + this.node = node; + this.cursor = cursor; + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/BookPage.java b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/BookPage.java new file mode 100644 index 000000000000..34d704342f93 --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/BookPage.java @@ -0,0 +1,25 @@ +package com.baeldung.pagination.dto; + +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; + +import com.baeldung.pagination.entity.Book; + +@Getter +public class BookPage { + private final List content; + private final int totalPages; + private final long totalElements; + private final int number; + private final int size; + + public BookPage(Page page) { + this.content = page.getContent(); + this.totalPages = page.getTotalPages(); + this.totalElements = page.getTotalElements(); + this.number = page.getNumber(); + this.size = page.getSize(); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/PageInfo.java b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/PageInfo.java new file mode 100644 index 000000000000..850b05de5b62 --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/dto/PageInfo.java @@ -0,0 +1,14 @@ +package com.baeldung.pagination.dto; + +import lombok.Getter; + +@Getter +public class PageInfo { + private final boolean hasNextPage; + private final String endCursor; + + public PageInfo(boolean hasNextPage, String endCursor) { + this.hasNextPage = hasNextPage; + this.endCursor = endCursor; + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/entity/Book.java b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/entity/Book.java new file mode 100644 index 000000000000..465bf1db2816 --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/entity/Book.java @@ -0,0 +1,42 @@ +package com.baeldung.pagination.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.Setter; + +@Entity +public class Book { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + private String author; + + public Long getId() { + return this.id; + } + + public String getTitle() { + return this.title; + } + + public String getAuthor() { + return this.author; + } + + public void setId(Long id) { + this.id = id; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setAuthor(String author) { + this.author = author; + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/repository/BookRepository.java b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/repository/BookRepository.java new file mode 100644 index 000000000000..e506fd2104e1 --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/repository/BookRepository.java @@ -0,0 +1,16 @@ +package com.baeldung.pagination.repository; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import com.baeldung.pagination.entity.Book; + +@Repository +public interface BookRepository extends PagingAndSortingRepository, CrudRepository { + List findByIdGreaterThanOrderByIdAsc(Long cursor, org.springframework.data.domain.Pageable pageable); + List findAllByOrderByIdAsc(org.springframework.data.domain.Pageable pageable); + boolean existsByIdGreaterThan(Long id); +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/resolver/BookQueryResolver.java b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/resolver/BookQueryResolver.java new file mode 100644 index 000000000000..fee56ae90974 --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/main/java/com/baeldung/pagination/resolver/BookQueryResolver.java @@ -0,0 +1,56 @@ +package com.baeldung.pagination.resolver; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.baeldung.pagination.dto.BookConnection; +import com.baeldung.pagination.dto.BookEdge; +import com.baeldung.pagination.dto.BookPage; +import com.baeldung.pagination.dto.PageInfo; +import com.baeldung.pagination.entity.Book; +import com.baeldung.pagination.repository.BookRepository; + +@Controller +public class BookQueryResolver { + private final BookRepository bookRepository; + + public BookQueryResolver(BookRepository bookRepository) { + this.bookRepository = bookRepository; + } + + @QueryMapping + public BookPage books(@Argument int page, @Argument int size) { + Pageable pageable = PageRequest.of(page, size); + Page bookPage = bookRepository.findAll(pageable); + return new BookPage(bookPage); + } + + @QueryMapping + public BookConnection booksByCursor(@Argument Optional cursor, @Argument int limit) { + List books; + + if (cursor.isPresent()) { + books = bookRepository.findByIdGreaterThanOrderByIdAsc(cursor.get(), PageRequest.of(0, limit)); + } else { + books = bookRepository.findAllByOrderByIdAsc(PageRequest.of(0, limit)); + } + + List edges = books.stream() + .map(book -> new BookEdge(book, book.getId().toString())) + .collect(Collectors.toList()); + String endCursor = books.isEmpty() ? null : books.get(books.size() - 1).getId().toString(); + boolean hasNextPage = !books.isEmpty() && bookRepository.existsByIdGreaterThan(books.get(books.size() - 1).getId()); + + PageInfo pageInfo = new PageInfo(hasNextPage, endCursor); + + return new BookConnection(edges, pageInfo); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-graphql-2/src/main/resources/application-pagination.yml b/spring-boot-modules/spring-boot-graphql-2/src/main/resources/application-pagination.yml new file mode 100644 index 000000000000..93ea6bd559aa --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/main/resources/application-pagination.yml @@ -0,0 +1,23 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb # H2 in-memory DB + username: sa + password: password + driver-class-name: org.h2.Driver + h2: + console: + enabled: true + path: /h2-console + jpa: + hibernate: + ddl-auto: update + show-sql: false + properties: + hibernate.format_sql: true + + graphql: + servlet: + enabled: true + path: /graphql + schema: + locations: classpath:pagination/ \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-graphql-2/src/main/resources/pagination/schema.graphqls b/spring-boot-modules/spring-boot-graphql-2/src/main/resources/pagination/schema.graphqls new file mode 100644 index 000000000000..2e19b385f61e --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/main/resources/pagination/schema.graphqls @@ -0,0 +1,33 @@ +type Book { + id: ID! + title: String + author: String +} + +type BookPage { + content: [Book] + totalPages: Int + totalElements: Int + number: Int + size: Int +} + +type BookEdge { + node: Book + cursor: String +} + +type PageInfo { + hasNextPage: Boolean + endCursor: String +} + +type BookConnection { + edges: [BookEdge] + pageInfo: PageInfo +} + +type Query { + books(page: Int, size: Int): BookPage + booksByCursor(cursor: ID, limit: Int!): BookConnection +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-graphql-2/src/test/java/com/baeldung/pagination/GraphqlPaginationIntegrationTest.java b/spring-boot-modules/spring-boot-graphql-2/src/test/java/com/baeldung/pagination/GraphqlPaginationIntegrationTest.java new file mode 100644 index 000000000000..0c61292b55ad --- /dev/null +++ b/spring-boot-modules/spring-boot-graphql-2/src/test/java/com/baeldung/pagination/GraphqlPaginationIntegrationTest.java @@ -0,0 +1,202 @@ +package com.baeldung.pagination; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureGraphQlTester; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.graphql.test.tester.GraphQlTester; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; + +import com.baeldung.pagination.entity.Book; +import com.baeldung.pagination.repository.BookRepository; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@AutoConfigureGraphQlTester +@ActiveProfiles("pagination") +class GraphqlPaginationIntegrationTest { + + @Autowired + private GraphQlTester graphQlTester; + + @Autowired + private WebTestClient webTestClient; + + @Autowired + private BookRepository bookRepository; + + @BeforeEach + void setup() { + bookRepository.deleteAll(); + + for (int i = 1; i <= 50; i++) { + Book book = new Book(); + book.setTitle("Test Book " + i); + book.setAuthor("Test Author " + i); + bookRepository.save(book); + } + } + + @Test + void givenPageAndSize_whenQueryBooks_thenShouldReturnCorrectPage() { + String query = """ + query { + books(page: 0, size: 5) { + content { + id + title + author + } + totalPages + totalElements + number + size + } + } + """; + + graphQlTester.document(query) + .execute() + .path("data.books") + .entity(BookPageResponse.class) + .satisfies(bookPage -> { + assertEquals(5, bookPage.getContent().size()); + assertEquals(0, bookPage.getNumber()); + assertEquals(5, bookPage.getSize()); + assertEquals(50, bookPage.getTotalElements()); + assertEquals(10, bookPage.getTotalPages()); + }); + } + + @Test + void givenCursorAndLimit_whenQueryBooksByCursor_thenShouldReturnNextBatch() { + // First page + String firstPageQuery = """ + query { + booksByCursor(limit: 5) { + edges { + node { + id + } + cursor + } + pageInfo { + endCursor + hasNextPage + } + } + } + """; + + BookConnectionResponse firstPage = graphQlTester.document(firstPageQuery) + .execute() + .path("data.booksByCursor") + .entity(BookConnectionResponse.class) + .get(); + + assertEquals(5, firstPage.getEdges().size()); + assertTrue(firstPage.getPageInfo().isHasNextPage()); + assertNotNull(firstPage.getPageInfo().getEndCursor()); + + // Second page + String secondPageQuery = String.format(""" + query { + booksByCursor(cursor: "%s", limit: 5) { + edges { + node { + id + } + } + pageInfo { + hasNextPage + } + } + } + """, firstPage.getPageInfo().getEndCursor()); + + graphQlTester.document(secondPageQuery) + .execute() + .path("data.booksByCursor") + .entity(BookConnectionResponse.class) + .satisfies(secondPage -> { + assertEquals(5, secondPage.getEdges().size()); + assertTrue(secondPage.getPageInfo().isHasNextPage()); + }); + } + + private static class BookPageResponse { + private List content; + private int totalPages; + private long totalElements; + private int number; + private int size; + + public List getContent() { + return content; + } + + public int getTotalPages() { + return totalPages; + } + + public long getTotalElements() { + return totalElements; + } + + public int getNumber() { + return number; + } + + public int getSize() { + return size; + } + } + + private static class BookConnectionResponse { + private List edges; + private PageInfoResponse pageInfo; + + public List getEdges() { + return edges; + } + + public PageInfoResponse getPageInfo() { + return pageInfo; + } + } + + private static class BookEdgeResponse { + private Book node; + private String cursor; + + public Book getNode() { + return node; + } + + public String getCursor() { + return cursor; + } + } + + private static class PageInfoResponse { + private boolean hasNextPage; + private String endCursor; + + public boolean isHasNextPage() { + return hasNextPage; + } + + public String getEndCursor() { + return endCursor; + } + } +} \ No newline at end of file