You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by bd...@apache.org on 2021/05/11 08:48:39 UTC

[sling-org-apache-sling-graphql-core] branch SLING-10309/experiment updated: SLING-10309 - add base64-encoded Cursor

This is an automated email from the ASF dual-hosted git repository.

bdelacretaz pushed a commit to branch SLING-10309/experiment
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-core.git


The following commit(s) were added to refs/heads/SLING-10309/experiment by this push:
     new 1322a7e  SLING-10309 - add base64-encoded Cursor
1322a7e is described below

commit 1322a7e9abe43a5ee7b45aebdfa486a7cf38baf6
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Tue May 11 10:48:20 2021 +0200

    SLING-10309 - add base64-encoded Cursor
---
 .../sling/graphql/api/pagination/Cursor.java       | 60 ++++++++++++++++++++
 .../apache/sling/graphql/api/pagination/Edge.java  |  7 ++-
 .../sling/graphql/api/pagination/PageInfo.java     | 11 ++--
 .../sling/graphql/api/pagination/ResultsPage.java  |  3 +
 .../pagination/{Edge.java => package-info.java}    | 14 ++---
 .../sling/graphql/core/engine/CursorTest.java      | 66 ++++++++++++++++++++++
 .../sling/graphql/core/engine/PaginationTest.java  | 42 +++++++-------
 7 files changed, 171 insertions(+), 32 deletions(-)

diff --git a/src/main/java/org/apache/sling/graphql/api/pagination/Cursor.java b/src/main/java/org/apache/sling/graphql/api/pagination/Cursor.java
new file mode 100644
index 0000000..975502f
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/api/pagination/Cursor.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.sling.graphql.api.pagination;
+
+import java.util.Base64;
+
+import org.jetbrains.annotations.NotNull;
+import org.osgi.annotation.versioning.ProviderType;
+
+/** Edges provide paginated data as per https://relay.dev/graphql/connections.htm */
+ @ProviderType
+public class Cursor {
+    private final String rawValue;
+    private final String encoded;
+
+    public Cursor(String rawValue) {
+        this.rawValue = rawValue == null ? "" : rawValue;
+        this.encoded = encode(this.rawValue);
+    }
+
+    public boolean isEmpty() {
+        return rawValue == null || rawValue.length() == 0;
+    }
+
+    @NotNull
+    public static String encode(String rawValue) {
+        return Base64.getEncoder().encodeToString(rawValue.getBytes());
+    }
+
+    @NotNull
+    public static String decode(String encodedValue) {
+        return new String(Base64.getDecoder().decode(encodedValue));
+    }
+
+    @Override
+    public String toString() {
+        return encoded;
+    }
+
+    public String getRawValue() {
+        return rawValue;
+    }
+}
diff --git a/src/main/java/org/apache/sling/graphql/api/pagination/Edge.java b/src/main/java/org/apache/sling/graphql/api/pagination/Edge.java
index 9b234db..62fbde7 100644
--- a/src/main/java/org/apache/sling/graphql/api/pagination/Edge.java
+++ b/src/main/java/org/apache/sling/graphql/api/pagination/Edge.java
@@ -19,8 +19,11 @@
 
  package org.apache.sling.graphql.api.pagination;
 
- /** Edges provide paginated data as per https://relay.dev/graphql/connections.htm */
+import org.osgi.annotation.versioning.ConsumerType;
+
+/** Edges provide paginated data as per https://relay.dev/graphql/connections.htm */
+@ConsumerType
 public interface Edge<T> {
     T getNode();
-    String getCursor();
+    Cursor getCursor();
 }
diff --git a/src/main/java/org/apache/sling/graphql/api/pagination/PageInfo.java b/src/main/java/org/apache/sling/graphql/api/pagination/PageInfo.java
index dd07408..06a6d51 100644
--- a/src/main/java/org/apache/sling/graphql/api/pagination/PageInfo.java
+++ b/src/main/java/org/apache/sling/graphql/api/pagination/PageInfo.java
@@ -19,10 +19,13 @@
 
  package org.apache.sling.graphql.api.pagination;
 
- /** Information about a results page as per https://relay.dev/graphql/connections.htm */
-public interface PageInfo {
-    String getStartCursor();
-    String getEndCursor();
+import org.osgi.annotation.versioning.ConsumerType;
+
+/** Information about a results page as per https://relay.dev/graphql/connections.htm */
+ @ConsumerType
+ public interface PageInfo {
+    Cursor getStartCursor();
+    Cursor getEndCursor();
     boolean isHasPreviousPage();
     boolean isHasNextPage();
 }
diff --git a/src/main/java/org/apache/sling/graphql/api/pagination/ResultsPage.java b/src/main/java/org/apache/sling/graphql/api/pagination/ResultsPage.java
index a946407..db8a433 100644
--- a/src/main/java/org/apache/sling/graphql/api/pagination/ResultsPage.java
+++ b/src/main/java/org/apache/sling/graphql/api/pagination/ResultsPage.java
@@ -21,9 +21,12 @@
 
 import java.util.List;
 
+import org.osgi.annotation.versioning.ConsumerType;
+
 /** This is what https://relay.dev/graphql/connections.htm calls a "connection".
  *  It actually represents a page of results, so why not call it that?
 */
+@ConsumerType
 public interface ResultsPage<T> {
     List<Edge<T>> getEdges();
     PageInfo getPageInfo();
diff --git a/src/main/java/org/apache/sling/graphql/api/pagination/Edge.java b/src/main/java/org/apache/sling/graphql/api/pagination/package-info.java
similarity index 76%
copy from src/main/java/org/apache/sling/graphql/api/pagination/Edge.java
copy to src/main/java/org/apache/sling/graphql/api/pagination/package-info.java
index 9b234db..ef9663c 100644
--- a/src/main/java/org/apache/sling/graphql/api/pagination/Edge.java
+++ b/src/main/java/org/apache/sling/graphql/api/pagination/package-info.java
@@ -17,10 +17,10 @@
  * under the License.
  */
 
- package org.apache.sling.graphql.api.pagination;
-
- /** Edges provide paginated data as per https://relay.dev/graphql/connections.htm */
-public interface Edge<T> {
-    T getNode();
-    String getCursor();
-}
+ /**
+  * This package contains APIs which are independent of
+  * a specific implementation of the underlying graphQL engine.
+  */
+@Version("0.0.1")
+package org.apache.sling.graphql.api.pagination;
+import org.osgi.annotation.versioning.Version;
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/CursorTest.java b/src/test/java/org/apache/sling/graphql/core/engine/CursorTest.java
new file mode 100644
index 0000000..db62524
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/core/engine/CursorTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.sling.graphql.core.engine;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+
+import java.util.UUID;
+
+import org.apache.sling.graphql.api.pagination.Cursor;
+import org.junit.Test;
+
+public class CursorTest {
+    private final String testValue = UUID.randomUUID().toString();
+
+    @Test
+    public void testEncoding() {
+        final String encoded = Cursor.encode(testValue);
+        assertNotEquals(encoded, testValue);
+        assertEquals(testValue, Cursor.decode(encoded));
+        assertFalse("Encoded cursor should not contain raw value in clear text", encoded.contains(testValue));
+    }
+
+    private void assertWithValue(String value, boolean isEmpty) {
+        final Cursor c = new Cursor(value);
+        if(value == null) {
+            value = "";
+        }
+        assertEquals("Expecting empty Cursor", isEmpty, c.isEmpty());
+        assertEquals(isEmpty, c.toString().length() == 0);
+        assertEquals(value, c.getRawValue());
+        assertEquals(Cursor.encode(value), c.toString());
+    }
+
+    @Test
+    public void testNonEmptyValue() {
+        assertWithValue(testValue, false);
+    }
+
+    @Test
+    public void testNullValue() {
+        assertWithValue(null, true);
+    }
+
+    @Test
+    public void testEmptyValue() {
+        assertWithValue("", true);
+    }
+}
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/PaginationTest.java b/src/test/java/org/apache/sling/graphql/core/engine/PaginationTest.java
index 285ca45..dcee1f3 100644
--- a/src/test/java/org/apache/sling/graphql/core/engine/PaginationTest.java
+++ b/src/test/java/org/apache/sling/graphql/core/engine/PaginationTest.java
@@ -33,6 +33,7 @@ import org.apache.sling.graphql.core.mocks.CharacterTypeResolver;
 import org.apache.sling.graphql.core.mocks.HumanDTO;
 import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingDataFetcherEnvironment;
+import org.apache.sling.graphql.api.pagination.Cursor;
 import org.apache.sling.graphql.api.pagination.Edge;
 import org.apache.sling.graphql.api.pagination.PageInfo;
 import org.apache.sling.graphql.api.pagination.ResultsPage;
@@ -49,7 +50,7 @@ public class PaginationTest extends ResourceQueryTestBase {
 
         @Override
         public @Nullable Object get(@NotNull SlingDataFetcherEnvironment e) throws Exception {
-            final String cursor = e.getArgument("after", null);
+            final String cursor = e.getArgument("after", "");
             final int limit = e.getArgument("limit", 2);
             return new HumansResultPage(humans, cursor, limit);
         }
@@ -57,14 +58,14 @@ public class PaginationTest extends ResourceQueryTestBase {
 
     static class HumansResultPage implements ResultsPage<HumanDTO>,PageInfo {
 
-        private final String startCursor;
-        private String endCursor;
+        private final Cursor startCursor;
+        private Cursor endCursor;
         private boolean hasPreviousPage;
         private boolean hasNextPage;
         private final List<Edge<HumanDTO>> edges = new ArrayList<>();
 
         HumansResultPage(List<HumanDTO> humans, String cursor, int limit) {
-            this.startCursor = cursor == null ? "" : cursor;
+            this.startCursor = new Cursor(Cursor.decode(cursor));
 
             // TODO this should be generalized so that data can be provided as 
             // a data source with "skip after cursor" functionality, and the
@@ -86,16 +87,16 @@ public class PaginationTest extends ResourceQueryTestBase {
                         }
 
                         @Override
-                        public String getCursor() {
-                            return dto.getId();
+                        public Cursor getCursor() {
+                            return new Cursor(dto.getId());
                         }
 
                     });
-                    endCursor = dto.getId();
+                    endCursor = new Cursor(dto.getId());
                 } else if(startCursor.isEmpty()) {
                     inRange = true;
                     hasPreviousPage = false;
-                } else if(startCursor.toString().equals(dto.getId())) {
+                } else if(startCursor.getRawValue().equals(dto.getId())) {
                     inRange = true;
                     hasPreviousPage = true;
                 }
@@ -113,12 +114,12 @@ public class PaginationTest extends ResourceQueryTestBase {
         }
 
         @Override
-        public String getStartCursor() {
+        public Cursor getStartCursor() {
             return startCursor;
         }
 
         @Override
-        public String getEndCursor() {
+        public Cursor getEndCursor() {
             return endCursor;
         }
 
@@ -144,9 +145,9 @@ public class PaginationTest extends ResourceQueryTestBase {
         TestUtil.registerSlingDataFetcher(context.bundleContext(), "humans/paginated", new ConnectionDataFetcher(humans));
     }
 
-    private void assertPageInfo(String json, String startCursor, String endCursor, Boolean hasPreviousPage, Boolean hasNextPage) {
-        assertThat(json, hasJsonPath("$.data.paginatedQuery.pageInfo.startCursor", equalTo(startCursor)));
-        assertThat(json, hasJsonPath("$.data.paginatedQuery.pageInfo.endCursor", equalTo(endCursor)));
+    private void assertPageInfo(String json, Cursor startCursor, Cursor endCursor, Boolean hasPreviousPage, Boolean hasNextPage) {
+        assertThat(json, hasJsonPath("$.data.paginatedQuery.pageInfo.startCursor", equalTo(startCursor.toString())));
+        assertThat(json, hasJsonPath("$.data.paginatedQuery.pageInfo.endCursor", equalTo(endCursor.toString())));
         assertThat(json, hasJsonPath("$.data.paginatedQuery.pageInfo.hasPreviousPage", equalTo(hasPreviousPage)));
         assertThat(json, hasJsonPath("$.data.paginatedQuery.pageInfo.hasNextPage", equalTo(hasNextPage)));
     }
@@ -155,8 +156,9 @@ public class PaginationTest extends ResourceQueryTestBase {
         int dataIndex = 0;
         for(int i=startIndex; i <= endIndex; i++) {
             final String id = "human-" + i;
+            final Cursor c = new Cursor(id);
             final String name = "Luke-" + i;
-            assertThat(json, hasJsonPath("$.data.paginatedQuery.edges[" + dataIndex + "].cursor", equalTo(id)));
+            assertThat(json, hasJsonPath("$.data.paginatedQuery.edges[" + dataIndex + "].cursor", equalTo(c.toString())));
             assertThat(json, hasJsonPath("$.data.paginatedQuery.edges[" + dataIndex + "].node.id", equalTo(id)));
             assertThat(json, hasJsonPath("$.data.paginatedQuery.edges[" + dataIndex + "].node.name", equalTo(name)));
             dataIndex++;
@@ -171,27 +173,29 @@ public class PaginationTest extends ResourceQueryTestBase {
             + " pageInfo { startCursor endCursor hasPreviousPage hasNextPage }"
             + " edges { cursor node { id name }}"
             +"}}");
-        assertPageInfo(json, "", "human-2", false, true );
+        assertPageInfo(json, new Cursor(null), new Cursor("human-2"), false, true );
         assertEdges(json, 1, 2);
     }
 
     @Test
     public void startCursorAndLimit() throws Exception {
-        final String json = queryJSON("{ paginatedQuery(after:\"human-5\", limit:6) {"
+        final Cursor start = new Cursor("human-5");
+        final String json = queryJSON("{ paginatedQuery(after:\"" + start + "\", limit:6) {"
             + " pageInfo { startCursor endCursor hasPreviousPage hasNextPage }"
             + " edges { cursor node { id name }}"
             +"}}");
-        assertPageInfo(json, "human-5", "human-11", true, true);
+        assertPageInfo(json, new Cursor("human-5"), new Cursor("human-11"), true, true);
         assertEdges(json, 6, 11);
     }
 
     @Test
     public void startCursorNearEnd() throws Exception {
-        final String json = queryJSON("{ paginatedQuery(after:\"human-94\", limit:60) {"
+        final Cursor start = new Cursor("human-94");
+        final String json = queryJSON("{ paginatedQuery(after:\"" + start + "\", limit:60) {"
             + " pageInfo { startCursor endCursor hasPreviousPage hasNextPage }"
             + " edges { cursor node { id name }}"
             +"}}");
-        assertPageInfo(json, "human-94", "human-99", true, false);
+        assertPageInfo(json, new Cursor("human-94"), new Cursor("human-99"), true, false);
         assertEdges(json, 95, 99);
     }
 }