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 16:44:22 UTC

[sling-org-apache-sling-graphql-core] branch SLING-10309/experiment updated: SLING-10309 - move pagination logic to GenericConnection

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 01bb8e1  SLING-10309 - move pagination logic to GenericConnection
01bb8e1 is described below

commit 01bb8e1bb1f49ab6bf11fe5164f893caa24d9fb4
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Tue May 11 18:43:55 2021 +0200

    SLING-10309 - move pagination logic to GenericConnection
---
 .../{ResultsPage.java => Connection.java}          |  12 +-
 .../sling/graphql/api/pagination/Cursor.java       |  16 ++
 .../api/pagination/helpers/GenericConnection.java  | 140 ++++++++++++++
 .../sling/graphql/core/engine/CursorTest.java      |  15 ++
 .../graphql/core/engine/PaginatedHumansTest.java   | 136 ++++++++++++++
 .../sling/graphql/core/engine/PaginationTest.java  | 201 ---------------------
 src/test/resources/paginated-humans-schema.txt     |  50 +++++
 src/test/resources/test-schema.txt                 |  19 --
 8 files changed, 362 insertions(+), 227 deletions(-)

diff --git a/src/main/java/org/apache/sling/graphql/api/pagination/ResultsPage.java b/src/main/java/org/apache/sling/graphql/api/pagination/Connection.java
similarity index 75%
rename from src/main/java/org/apache/sling/graphql/api/pagination/ResultsPage.java
rename to src/main/java/org/apache/sling/graphql/api/pagination/Connection.java
index db8a433..b2c2998 100644
--- a/src/main/java/org/apache/sling/graphql/api/pagination/ResultsPage.java
+++ b/src/main/java/org/apache/sling/graphql/api/pagination/Connection.java
@@ -17,17 +17,15 @@
  * under the License.
  */
 
- package org.apache.sling.graphql.api.pagination;
-
-import java.util.List;
+package org.apache.sling.graphql.api.pagination;
 
 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?
+/** As per https://relay.dev/graphql/connections.htm a "connection"
+ *  is a page of results for a paginated query.
 */
 @ConsumerType
-public interface ResultsPage<T> {
-    List<Edge<T>> getEdges();
+public interface Connection<T> {
+    Iterable<Edge<T>> getEdges();
     PageInfo getPageInfo();
 }
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
index c4509da..24311cb 100644
--- a/src/main/java/org/apache/sling/graphql/api/pagination/Cursor.java
+++ b/src/main/java/org/apache/sling/graphql/api/pagination/Cursor.java
@@ -22,6 +22,7 @@
 import java.util.Base64;
 
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.osgi.annotation.versioning.ProviderType;
 
 /** Cursor for our paginated results.
@@ -33,11 +34,26 @@ public class Cursor {
     private final String rawValue;
     private final String encoded;
 
+    /** These constants can be used to identify uninitialized cursors */
+    public static final String INVALID_CURSOR_KEY = "$INVALID$CURSOR$" + Cursor.class.getName();
+    public static final String ENCODED_INVALID_CURSOR_KEY = Cursor.encode(INVALID_CURSOR_KEY);
+
     public Cursor(String rawValue) {
         this.rawValue = rawValue == null ? "" : rawValue;
         this.encoded = encode(this.rawValue);
     }
 
+    public static Cursor fromEncodedString(@Nullable String encoded) {
+        if(encoded == null) {
+            return null;
+        }
+        encoded = encoded.trim();
+        if(encoded.length() == 0) {
+            return null;
+        }
+        return new Cursor(decode(encoded));
+    }
+
     public boolean isEmpty() {
         return rawValue == null || rawValue.length() == 0;
     }
diff --git a/src/main/java/org/apache/sling/graphql/api/pagination/helpers/GenericConnection.java b/src/main/java/org/apache/sling/graphql/api/pagination/helpers/GenericConnection.java
new file mode 100644
index 0000000..8cd8aa0
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/api/pagination/helpers/GenericConnection.java
@@ -0,0 +1,140 @@
+/*
+ * 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.helpers;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.sling.graphql.api.pagination.Connection;
+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.osgi.annotation.versioning.ConsumerType;
+
+/** As per https://relay.dev/graphql/connections.htm a "connection"
+ *  is a page of results for a paginated query.
+*/
+@ConsumerType
+public class GenericConnection<T> implements Connection<T> {
+
+    private final List<Edge<T>> edges;
+
+    static class LocalPageInfo implements PageInfo {
+        Cursor startCursor = null;
+        Cursor endCursor = null;
+        boolean hasPreviousPage;
+        boolean hasNextPage;
+
+        @Override
+        public Cursor getStartCursor() {
+            return startCursor;
+        }
+
+        @Override
+        public Cursor getEndCursor() {
+            return endCursor;
+        }
+
+        @Override
+        public boolean isHasPreviousPage() {
+            return hasPreviousPage;
+        }
+
+        @Override
+        public boolean isHasNextPage() {
+            return hasNextPage;
+        }
+    };
+
+    private final LocalPageInfo pageInfo = new LocalPageInfo();
+
+    private Edge<T> newEdge(final T node, final Function<T, String> cursorStringFunction) {
+        return new Edge<T>() {
+            @Override
+            public T getNode() {
+                return node;
+            }
+
+            @Override
+            public Cursor getCursor() {
+                return new Cursor(cursorStringFunction.apply(node));
+            }
+        };
+    }
+
+    /** Build a Connection that will output the supplied data, optionally skipping items
+     *  at the beginning and considering a set maximum of items.
+     * 
+     *  @param dataIterator the connection's data - must include the item that startAfter points to
+     *  @param cursorStringFunction extracts a String from an object of type T to create a Cursor
+     *  @param startAfter if not null, data up to and including the item which has this cursor is ignored
+     *  @param maxItemsReturned at most this many items are considered
+    */
+    public GenericConnection(Iterator<T> dataIterator, Function<T, String> cursorStringFunction, final Cursor startAfter, int maxItemsReturned) {
+
+        // Need to visit the stream first to setup the PageInfo, which graphql-java
+        // apparently uses before visiting all the edges
+        edges = new ArrayList<>();
+        boolean inRange = false;
+        int itemsToAdd = maxItemsReturned;
+        while(itemsToAdd > 0 && dataIterator.hasNext()) {
+            final T node = dataIterator.next();
+            boolean addThisNode = false;
+            if(!inRange) {
+                if(startAfter == null) {
+                    inRange = true;
+                    addThisNode = true;
+                    pageInfo.hasPreviousPage = false;
+                } else {
+                    final String rawCursor = cursorStringFunction.apply(node);
+                    inRange = startAfter.getRawValue().equals(rawCursor);
+                    pageInfo.hasPreviousPage = true;
+                }
+            } else {
+                addThisNode = true;
+            }
+
+            if(addThisNode) {
+                final Edge<T> toAdd = newEdge(node, cursorStringFunction);
+                if(pageInfo.startCursor == null) {
+                    pageInfo.startCursor = toAdd.getCursor();
+                }
+                pageInfo.endCursor = toAdd.getCursor();
+                edges.add(toAdd);
+                itemsToAdd--;
+            }
+        }
+
+        if(!inRange) {
+            throw new RuntimeException("Start cursor not found in supplied data:" + startAfter);
+        }
+        pageInfo.hasNextPage = dataIterator.hasNext();
+    }
+
+    public Iterable<Edge<T>> getEdges() {
+        return edges::iterator;
+    }
+
+    public PageInfo getPageInfo() {
+        return pageInfo;
+    }
+}
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
index db62524..7f83d1e 100644
--- a/src/test/java/org/apache/sling/graphql/core/engine/CursorTest.java
+++ b/src/test/java/org/apache/sling/graphql/core/engine/CursorTest.java
@@ -21,6 +21,7 @@ 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 static org.junit.Assert.assertNull;
 
 import java.util.UUID;
 
@@ -63,4 +64,18 @@ public class CursorTest {
     public void testEmptyValue() {
         assertWithValue("", true);
     }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueDecoding() {
+        Cursor.decode("This is not base64");
+    }
+
+    @Test
+    public void testfromEncodedString() {
+        final String key = UUID.randomUUID().toString();
+        final String encoded = Cursor.encode(key);
+        assertEquals(encoded, Cursor.fromEncodedString("   " + encoded + "   ").toString());
+        assertNull(Cursor.fromEncodedString(null));
+        assertNull(Cursor.fromEncodedString("\t\n  "));
+    }
 }
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/PaginatedHumansTest.java b/src/test/java/org/apache/sling/graphql/core/engine/PaginatedHumansTest.java
new file mode 100644
index 0000000..e3a134c
--- /dev/null
+++ b/src/test/java/org/apache/sling/graphql/core/engine/PaginatedHumansTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+
+import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.apache.sling.graphql.core.mocks.TestUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+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.Connection;
+import org.apache.sling.graphql.api.pagination.Cursor;
+import org.apache.sling.graphql.api.pagination.helpers.GenericConnection;
+import org.junit.Test;
+
+public class PaginatedHumansTest extends ResourceQueryTestBase {
+
+    protected String getTestSchemaName() {
+        return "paginated-humans-schema";
+    }
+
+    static class HumansPageFetcher implements SlingDataFetcher<Connection<HumanDTO>> {
+        private List<HumanDTO> humans = new ArrayList<>();
+
+        HumansPageFetcher(List<HumanDTO> humans) {
+            this.humans = humans;
+        }
+
+        @Override
+        public @Nullable Connection<HumanDTO> get(@NotNull SlingDataFetcherEnvironment e) throws Exception {
+            final Cursor afterCursor = Cursor.fromEncodedString(e.getArgument("after"));
+            final int limit = e.getArgument("limit", 2);
+            return new GenericConnection<>(humans.iterator(), HumanDTO::getId, afterCursor, limit);
+        }
+    }
+
+    @Override
+    protected void setupAdditionalServices() {
+        final List<HumanDTO> humans = new ArrayList<>();
+
+        for(int i=1 ; i < 100 ; i++) {
+            humans.add(new HumanDTO("human-" + i, "Luke-" + i, "Tatooine"));
+        }
+        TestUtil.registerSlingDataFetcher(context.bundleContext(), "humans/paginated", new HumansPageFetcher(humans));
+    }
+
+    private void assertPageInfo(String json, Cursor startCursor, Cursor endCursor, Boolean hasPreviousPage, Boolean hasNextPage) {
+        assertThat(json, hasJsonPath("$.data.paginatedHumans.pageInfo.startCursor", equalTo(startCursor.toString())));
+        assertThat(json, hasJsonPath("$.data.paginatedHumans.pageInfo.endCursor", equalTo(endCursor.toString())));
+        assertThat(json, hasJsonPath("$.data.paginatedHumans.pageInfo.hasPreviousPage", equalTo(hasPreviousPage)));
+        assertThat(json, hasJsonPath("$.data.paginatedHumans.pageInfo.hasNextPage", equalTo(hasNextPage)));
+    }
+
+    private void assertEdges(String json, int startIndex, int endIndex) {
+        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.paginatedHumans.edges[" + dataIndex + "].node.id", equalTo(id)));
+            assertThat(json, hasJsonPath("$.data.paginatedHumans.edges[" + dataIndex + "].node.name", equalTo(name)));
+            assertThat(json, hasJsonPath("$.data.paginatedHumans.edges[" + dataIndex + "].cursor", equalTo(c.toString())));
+            dataIndex++;
+        }
+        final int count = endIndex - startIndex + 1;
+        assertThat(json, hasJsonPath("$.data.paginatedHumans.edges.length()", equalTo(count)));
+    }
+
+    @Test
+    public void noArguments() throws Exception {
+        final String json = queryJSON("{ paginatedHumans {"
+            + " pageInfo { startCursor endCursor hasPreviousPage hasNextPage }"
+            + " edges { cursor node { id name }}"
+            +"}}");
+        assertEdges(json, 1, 2);
+        assertPageInfo(json, new Cursor("human-1"), new Cursor("human-2"), false, true );
+    }
+
+    @Test
+    public void startCursorAndLimit() throws Exception {
+        final Cursor start = new Cursor("human-5");
+        final String json = queryJSON("{ paginatedHumans(after:\"" + start + "\", limit:6) {"
+            + " pageInfo { startCursor endCursor hasPreviousPage hasNextPage }"
+            + " edges { cursor node { id name }}"
+            +"}}");
+        assertEdges(json, 6, 11);
+        assertPageInfo(json, new Cursor("human-6"), new Cursor("human-11"), true, true);
+    }
+
+    @Test
+    public void startCursorNearEnd() throws Exception {
+        final Cursor start = new Cursor("human-94");
+        final String json = queryJSON("{ paginatedHumans(after:\"" + start + "\", limit:60) {"
+            + " pageInfo { startCursor endCursor hasPreviousPage hasNextPage }"
+            + " edges { cursor node { id name }}"
+            +"}}");
+        assertEdges(json, 95, 99);
+        assertPageInfo(json, new Cursor("human-95"), new Cursor("human-99"), true, false);
+    }
+
+    @Test
+    public void afterCursorNotFound() throws Exception {
+        final Cursor notInDataSet = new Cursor("This is not a key from our data set");
+        final String json = queryJSON("{ paginatedHumans(after:\"" + notInDataSet + "\", limit:60) {"
+            + " pageInfo { startCursor endCursor hasPreviousPage hasNextPage }"
+            + " edges { cursor node { id name }}"
+            +"}}");
+        assertThat(json, hasJsonPath("errors"));
+        assertTrue(json.contains("Start cursor not found"));
+    }
+}
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
deleted file mode 100644
index 26d9448..0000000
--- a/src/test/java/org/apache/sling/graphql/core/engine/PaginationTest.java
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * 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 java.util.ArrayList;
-import java.util.List;
-
-import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
-
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.apache.sling.graphql.core.mocks.TestUtil;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-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;
-import org.junit.Test;
-
-public class PaginationTest extends ResourceQueryTestBase {
-
-    static class HumansPageFetcher implements SlingDataFetcher<Object> {
-        private List<HumanDTO> humans = new ArrayList<>();
-
-        HumansPageFetcher(List<HumanDTO> humans) {
-            this.humans = humans;
-        }
-
-        @Override
-        public @Nullable Object get(@NotNull SlingDataFetcherEnvironment e) throws Exception {
-            final String cursor = e.getArgument("after", "");
-            final int limit = e.getArgument("limit", 2);
-            return new HumansResultPage(humans, cursor, limit);
-        }
-    }
-
-    // TODO this class should be generalized so that data can be provided as 
-    // a data source (Stream?) with "skip after cursor" functionality, and the
-    // rest of this class is then generic.
-    static class HumansResultPage implements ResultsPage<HumanDTO>,PageInfo {
-
-        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 = new Cursor(Cursor.decode(cursor));
-
-            boolean inRange = false;
-            int remaining = limit;
-            for(HumanDTO dto : humans) {
-                if(remaining <= 0) {
-                    hasNextPage = true;
-                    break;
-                } else if(inRange) {
-                    if(remaining -- <= 0) {
-                        inRange = false;
-                    }
-                    edges.add(new Edge<HumanDTO>() {
-                        @Override
-                        public HumanDTO getNode() {
-                            return dto;
-                        }
-
-                        @Override
-                        public Cursor getCursor() {
-                            return new Cursor(dto.getId());
-                        }
-
-                    });
-                    endCursor = new Cursor(dto.getId());
-                } else if(startCursor.isEmpty()) {
-                    inRange = true;
-                    hasPreviousPage = false;
-                } else if(startCursor.getRawValue().equals(dto.getId())) {
-                    inRange = true;
-                    hasPreviousPage = true;
-                }
-            }
-        }
-
-        @Override
-        public List<Edge<HumanDTO>> getEdges() {
-            return edges;
-        }
-
-        @Override
-        public PageInfo getPageInfo() {
-            return this;
-        }
-
-        @Override
-        public Cursor getStartCursor() {
-            return startCursor;
-        }
-
-        @Override
-        public Cursor getEndCursor() {
-            return endCursor;
-        }
-
-        @Override
-        public boolean isHasPreviousPage() {
-            return hasPreviousPage;
-        }
-
-        @Override
-        public boolean isHasNextPage() {
-            return hasNextPage;
-        }
-}
-
-    @Override
-    protected void setupAdditionalServices() {
-        final List<HumanDTO> humans = new ArrayList<>();
-
-        for(int i=0 ; i < 100 ; i++) {
-            humans.add(new HumanDTO("human-" + i, "Luke-" + i, "Tatooine"));
-        }
-        TestUtil.registerSlingTypeResolver(context.bundleContext(), "character/resolver", new CharacterTypeResolver());
-        TestUtil.registerSlingDataFetcher(context.bundleContext(), "humans/paginated", new HumansPageFetcher(humans));
-    }
-
-    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)));
-    }
-
-    private void assertEdges(String json, int startIndex, int endIndex) {
-        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(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++;
-        }
-        final int count = endIndex - startIndex + 1;
-        assertThat(json, hasJsonPath("$.data.paginatedQuery.edges.length()", equalTo(count)));
-    }
-
-    @Test
-    public void noArguments() throws Exception {
-        final String json = queryJSON("{ paginatedQuery {"
-            + " pageInfo { startCursor endCursor hasPreviousPage hasNextPage }"
-            + " edges { cursor node { id name }}"
-            +"}}");
-        assertPageInfo(json, new Cursor(null), new Cursor("human-2"), false, true );
-        assertEdges(json, 1, 2);
-    }
-
-    @Test
-    public void startCursorAndLimit() throws Exception {
-        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, new Cursor("human-5"), new Cursor("human-11"), true, true);
-        assertEdges(json, 6, 11);
-    }
-
-    @Test
-    public void startCursorNearEnd() throws Exception {
-        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, new Cursor("human-94"), new Cursor("human-99"), true, false);
-        assertEdges(json, 95, 99);
-    }
-}
diff --git a/src/test/resources/paginated-humans-schema.txt b/src/test/resources/paginated-humans-schema.txt
new file mode 100644
index 0000000..b222cd6
--- /dev/null
+++ b/src/test/resources/paginated-humans-schema.txt
@@ -0,0 +1,50 @@
+# * 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.
+
+# This directive maps fields to our Sling data fetchers
+directive @fetcher(
+    name : String,
+    options : String = "",
+    source : String = ""
+) on FIELD_DEFINITION
+
+type Query {
+    paginatedHumans (after : String, limit : Int) : PaginatedHumans @fetcher(name:"humans/paginated")
+}
+
+type Human {
+  id: ID!
+  name: String!
+  address: String
+}
+
+type PageInfo {
+    startCursor : String
+    endCursor : String
+    hasPreviousPage : Boolean
+    hasNextPage : Boolean
+}
+
+type HumanEdge {
+    cursor: String
+    node: Human
+}
+
+type PaginatedHumans {
+    edges : [HumanEdge]
+    pageInfo : PageInfo
+}
\ No newline at end of file
diff --git a/src/test/resources/test-schema.txt b/src/test/resources/test-schema.txt
index f9fb5d7..e7fb1b0 100644
--- a/src/test/resources/test-schema.txt
+++ b/src/test/resources/test-schema.txt
@@ -44,9 +44,6 @@ type Query {
 
     # Test interface query
     interfaceQuery: CharactersAsInterface @fetcher(name:"character/fetcher")
-
-    # Test pagination
-    paginatedQuery (after : String, limit : Int) : PaginatedHumans @fetcher(name:"humans/paginated")
 }
 
 interface CharacterInterface @resolver(name:"character/resolver" source:"CharacterInterface") {
@@ -112,19 +109,3 @@ type Droid implements CharacterInterface {
   primaryFunction: String
 }
 
-type PageInfo {
-    startCursor : String
-    endCursor : String
-    hasPreviousPage : Boolean
-    hasNextPage : Boolean
-}
-
-type HumanEdge {
-    cursor: String
-    node: Human
-}
-
-type PaginatedHumans {
-    edges : [HumanEdge]
-    pageInfo : PageInfo
-}
\ No newline at end of file