From 8ce9f26158772247df983040fb767fa56e24a44e Mon Sep 17 00:00:00 2001 From: shubh-arya Date: Sat, 13 Apr 2024 17:01:50 +0530 Subject: [PATCH] pagination changes. --- pom.xml | 10 ++-- .../application/port/in/CustomerResolver.java | 14 ++++- .../application/port/in/ProductResolver.java | 4 +- .../util/pagination/CursorDecoder.java | 26 +++++++++ .../util/pagination/CursorEncoder.java | 16 ++++++ .../util/pagination/GenericConnection.java | 57 +++++++++++++++++++ .../util/pagination/GenericPageInfo.java | 22 +++++++ src/main/resources/application.yml | 2 + src/main/resources/schema/schema.graphqls | 16 ++++-- 9 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/sarya/graphql/service/util/pagination/CursorDecoder.java create mode 100644 src/main/java/com/sarya/graphql/service/util/pagination/CursorEncoder.java create mode 100644 src/main/java/com/sarya/graphql/service/util/pagination/GenericConnection.java create mode 100644 src/main/java/com/sarya/graphql/service/util/pagination/GenericPageInfo.java diff --git a/pom.xml b/pom.xml index 5a58752..bca415e 100644 --- a/pom.xml +++ b/pom.xml @@ -80,11 +80,11 @@ ${graphql.java} - - - - - + + com.graphql-java + graphql-java-extended-validation + ${graphql.java} + diff --git a/src/main/java/com/sarya/graphql/service/application/port/in/CustomerResolver.java b/src/main/java/com/sarya/graphql/service/application/port/in/CustomerResolver.java index f5fc2f3..f0b04dc 100644 --- a/src/main/java/com/sarya/graphql/service/application/port/in/CustomerResolver.java +++ b/src/main/java/com/sarya/graphql/service/application/port/in/CustomerResolver.java @@ -5,24 +5,34 @@ import com.netflix.graphql.dgs.InputArgument; import com.sarya.graphql.service.codegen.types.Customer; import com.sarya.graphql.service.codegen.types.PaginationInput; +import com.sarya.graphql.service.util.pagination.CursorDecoder; +import com.sarya.graphql.service.util.pagination.CursorEncoder; +import com.sarya.graphql.service.util.pagination.GenericConnection; import graphql.relay.Connection; -import graphql.relay.SimpleListConnection; import graphql.schema.DataFetchingEnvironment; + import java.util.List; import java.util.UUID; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @DgsComponent @Slf4j +@AllArgsConstructor public class CustomerResolver { + + private final CursorEncoder cursorEncoder; + private final CursorDecoder cursorDecoder; + @DgsQuery Connection fetchCustomers( DataFetchingEnvironment dfe, @InputArgument PaginationInput paginationInput ) { log.info("pagination Input: {}", paginationInput); + int offset = cursorDecoder.apply(paginationInput.getAfter(), 0); var customer = Customer.newBuilder().name("James").customerId(UUID.randomUUID()) .email("james@email.com").build(); - return new SimpleListConnection<>(List.of(customer)).get(dfe); + return new GenericConnection<>(cursorEncoder, List.of(customer), offset).get(dfe); } } diff --git a/src/main/java/com/sarya/graphql/service/application/port/in/ProductResolver.java b/src/main/java/com/sarya/graphql/service/application/port/in/ProductResolver.java index 0bf3166..af235ee 100644 --- a/src/main/java/com/sarya/graphql/service/application/port/in/ProductResolver.java +++ b/src/main/java/com/sarya/graphql/service/application/port/in/ProductResolver.java @@ -6,7 +6,7 @@ import com.netflix.graphql.dgs.InputArgument; import com.sarya.graphql.service.application.usecase.CreateProductUseCase; import com.sarya.graphql.service.application.usecase.FetchProductUseCase; -import com.sarya.graphql.service.codegen.types.CreateProduct; +import com.sarya.graphql.service.codegen.types.CreateProductInput; import com.sarya.graphql.service.codegen.types.Product; import com.sarya.graphql.service.codegen.types.ProductStatus; import com.sarya.graphql.service.codegen.types.ProductType; @@ -34,7 +34,7 @@ public List fetchProducts() { } @DgsMutation - public boolean createProduct(@InputArgument CreateProduct input) { + public boolean createProduct(@InputArgument CreateProductInput input) { log.info("incoming message: {}", input); Product output = new Product(); BeanUtils.copyProperties(input, output, Product.class); diff --git a/src/main/java/com/sarya/graphql/service/util/pagination/CursorDecoder.java b/src/main/java/com/sarya/graphql/service/util/pagination/CursorDecoder.java new file mode 100644 index 0000000..f2f66d8 --- /dev/null +++ b/src/main/java/com/sarya/graphql/service/util/pagination/CursorDecoder.java @@ -0,0 +1,26 @@ +package com.sarya.graphql.service.util.pagination; + +import com.google.common.base.Strings; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.function.BiFunction; +import org.apache.tomcat.util.codec.binary.StringUtils; +import org.springframework.stereotype.Component; + +@Component +public class CursorDecoder implements BiFunction { + + @Override + public Integer apply(String cursor, Integer defaultValue) { + if (Strings.isNullOrEmpty(cursor)) { + return defaultValue; + } + var hash = Base64.getDecoder().decode(cursor.getBytes(StandardCharsets.UTF_8)); + var cursorLiteral = StringUtils.newStringUtf8(hash); + try { + return (Integer.valueOf(cursorLiteral.substring(5))); + } catch (NumberFormatException nfe) { + throw new RuntimeException("unparsable cursor: {}" + cursor); + } + } +} diff --git a/src/main/java/com/sarya/graphql/service/util/pagination/CursorEncoder.java b/src/main/java/com/sarya/graphql/service/util/pagination/CursorEncoder.java new file mode 100644 index 0000000..8ac070e --- /dev/null +++ b/src/main/java/com/sarya/graphql/service/util/pagination/CursorEncoder.java @@ -0,0 +1,16 @@ +package com.sarya.graphql.service.util.pagination; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.function.Function; +import org.springframework.stereotype.Component; + +@Component +public class CursorEncoder implements Function { + + @Override + public String apply(Integer offset) { + byte[] bytes = ("prefix" + offset).getBytes(StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(bytes); + } +} diff --git a/src/main/java/com/sarya/graphql/service/util/pagination/GenericConnection.java b/src/main/java/com/sarya/graphql/service/util/pagination/GenericConnection.java new file mode 100644 index 0000000..6be83f6 --- /dev/null +++ b/src/main/java/com/sarya/graphql/service/util/pagination/GenericConnection.java @@ -0,0 +1,57 @@ +package com.sarya.graphql.service.util.pagination; + +import graphql.relay.Connection; +import graphql.relay.DefaultConnection; +import graphql.relay.DefaultConnectionCursor; +import graphql.relay.DefaultEdge; +import graphql.relay.DefaultPageInfo; +import graphql.relay.Edge; +import graphql.relay.PageInfo; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class GenericConnection implements DataFetcher> { + + private final CursorEncoder cursorEncoder; + + private final List data; + private final int offset; + + @Override + public Connection get(DataFetchingEnvironment environment) { + List> edges = buildEdges(); + + if (edges.size() == 0) { + return emptyConnection(); + } + + var firstEdge = edges.get(0); + var lastEdge = edges.get(edges.size() - 1); + + return new DefaultConnection<>( + edges, + new GenericPageInfo(firstEdge.getCursor(), lastEdge.getCursor(), false, false, edges.size()) + ); + } + + private Connection emptyConnection() { + PageInfo pageInfo = new DefaultPageInfo(null, null, false, false); + return new DefaultConnection<>(List.of(), pageInfo); + } + + private List> buildEdges() { + List> edges = new ArrayList<>(); + int ix = 0; + for (T object : data) { + edges.add( + new DefaultEdge<>(object, new DefaultConnectionCursor(cursorEncoder.apply(offset + ix++))) + ); + } + return edges; + } + +} diff --git a/src/main/java/com/sarya/graphql/service/util/pagination/GenericPageInfo.java b/src/main/java/com/sarya/graphql/service/util/pagination/GenericPageInfo.java new file mode 100644 index 0000000..8d24e7a --- /dev/null +++ b/src/main/java/com/sarya/graphql/service/util/pagination/GenericPageInfo.java @@ -0,0 +1,22 @@ +package com.sarya.graphql.service.util.pagination; + +import graphql.relay.ConnectionCursor; +import graphql.relay.DefaultPageInfo; +import lombok.ToString; + +@ToString +public class GenericPageInfo extends DefaultPageInfo { + + private final long totalCount; + + public GenericPageInfo( + ConnectionCursor startCursor, + ConnectionCursor endCursor, + boolean hasPreviousPage, + boolean hasNextPage, + long totalCount + ) { + super(startCursor, endCursor, hasPreviousPage, hasNextPage); + this.totalCount = totalCount; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ba5e5aa..4078e61 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,6 +4,7 @@ spring: graphql: graphiql: enabled: true + # websocket: # path: /graphql schema: @@ -24,6 +25,7 @@ dgs: reload: true graphql: enable-entity-fetcher-custom-scalar-parsing: true + schema-wiring-validation-enabled: true spring: webmvc: asyncdispatch: diff --git a/src/main/resources/schema/schema.graphqls b/src/main/resources/schema/schema.graphqls index dce5edb..df9ea1e 100644 --- a/src/main/resources/schema/schema.graphqls +++ b/src/main/resources/schema/schema.graphqls @@ -1,6 +1,14 @@ +directive @connection on OBJECT +directive @Range(min: Int = 0, max: Int = 2147483647, message: String= "Invalid input value") on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +directive @Size(min : Int = 0, max : Int = 2147483647, message : String = "graphql.validation.Size.message") +on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +directive @NotBlank(message : String = "graphql.validation.NotBlank.message") on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION + scalar Currency scalar UUID scalar BigDecimal + + scalar Long type Query { @@ -9,11 +17,11 @@ type Query { } type Mutation { - createProduct(input: CreateProduct): Boolean + createProduct(input: CreateProductInput): Boolean } -input CreateProduct { - name: String +input CreateProductInput { + name: String @NotBlank productType: ProductType productStatus: ProductStatus active: Boolean @@ -77,6 +85,6 @@ type CustomerEdge { } input PaginationInput { - first: Int = 10 + first: Int = 10 @Range(min: 1, max: 1000) after: String }