From 15fb61f911857d46f232f71329be070abf9d5b9f Mon Sep 17 00:00:00 2001
From: Jay Herron <NeedleInAJayStack@protonmail.com>
Date: Sun, 12 Mar 2023 14:48:13 -0600
Subject: [PATCH 1/2] test: Adds Connection functional tests

---
 Tests/GraphitiTests/ConnectionTests.swift | 466 ++++++++++++++++++++++
 1 file changed, 466 insertions(+)
 create mode 100644 Tests/GraphitiTests/ConnectionTests.swift

diff --git a/Tests/GraphitiTests/ConnectionTests.swift b/Tests/GraphitiTests/ConnectionTests.swift
new file mode 100644
index 00000000..90241462
--- /dev/null
+++ b/Tests/GraphitiTests/ConnectionTests.swift
@@ -0,0 +1,466 @@
+import Foundation
+import Graphiti
+import NIO
+import XCTest
+
+class ConnectionTests: XCTestCase {
+    struct Comment: Codable, Identifiable {
+        let id: Int
+        let message: String
+    }
+
+    struct ConnectionTypeResolver {
+        func comments(
+            context _: NoContext,
+            arguments: PaginationArguments
+        ) throws -> Connection<Comment> {
+            return try [
+                Comment(id: 1, message: "Hello"),
+                Comment(id: 2, message: "What's up?"),
+                Comment(id: 3, message: "Goodbye"),
+            ].connection(from: arguments)
+        }
+    }
+
+    let schema = {
+        try! Schema<ConnectionTypeResolver, NoContext> {
+            Type(Comment.self) {
+                Field("id", at: \.id)
+                Field("message", at: \.message)
+            }
+
+            ConnectionType(Comment.self)
+
+            Query {
+                Field("comments", at: ConnectionTypeResolver.comments) {
+                    Argument("first", at: \.first)
+                    Argument("last", at: \.last)
+                    Argument("after", at: \.after)
+                    Argument("before", at: \.before)
+                }
+            }
+        }
+    }()
+
+    let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+
+    /// Test that connection objects work as expected
+    func testConnection() throws {
+        XCTAssertEqual(
+            try schema.execute(
+                request: """
+                {
+                    comments {
+                        edges {
+                            cursor
+                            node {
+                                id
+                                message
+                            }
+                        }
+                        pageInfo {
+                            hasPreviousPage
+                            hasNextPage
+                            startCursor
+                            endCursor
+                        }
+                    }
+                }
+                """,
+                resolver: .init(),
+                context: NoContext(),
+                eventLoopGroup: eventLoopGroup
+            ).wait(),
+            .init(
+                data: [
+                    "comments": [
+                        "edges": [
+                            [
+                                "cursor": "MQ==",
+                                "node": [
+                                    "id": 1,
+                                    "message": "Hello",
+                                ],
+                            ],
+                            [
+                                "cursor": "Mg==",
+                                "node": [
+                                    "id": 2,
+                                    "message": "What's up?",
+                                ],
+                            ],
+                            [
+                                "cursor": "Mw==",
+                                "node": [
+                                    "id": 3,
+                                    "message": "Goodbye",
+                                ],
+                            ],
+                        ],
+                        "pageInfo": [
+                            "hasPreviousPage": false,
+                            "hasNextPage": false,
+                            "startCursor": "MQ==",
+                            "endCursor": "Mw==",
+                        ],
+                    ],
+                ]
+            )
+        )
+    }
+
+    /// Test that `first` argument works as intended
+    func testFirst() throws {
+        XCTAssertEqual(
+            try schema.execute(
+                request: """
+                {
+                    comments(first: 1) {
+                        edges {
+                            node {
+                                id
+                                message
+                            }
+                        }
+                        pageInfo {
+                            hasPreviousPage
+                            hasNextPage
+                            startCursor
+                            endCursor
+                        }
+                    }
+                }
+                """,
+                resolver: .init(),
+                context: NoContext(),
+                eventLoopGroup: eventLoopGroup
+            ).wait(),
+            .init(
+                data: [
+                    "comments": [
+                        "edges": [
+                            [
+                                "node": [
+                                    "id": 1,
+                                    "message": "Hello",
+                                ],
+                            ],
+                        ],
+                        "pageInfo": [
+                            "hasPreviousPage": false,
+                            "hasNextPage": true,
+                            "startCursor": "MQ==",
+                            "endCursor": "MQ==",
+                        ],
+                    ],
+                ]
+            )
+        )
+    }
+
+    /// Test that `after` argument works as intended
+    func testAfter() throws {
+        XCTAssertEqual(
+            try schema.execute(
+                request: """
+                {
+                    comments(after: "MQ==") {
+                        edges {
+                            node {
+                                id
+                                message
+                            }
+                        }
+                        pageInfo {
+                            hasPreviousPage
+                            hasNextPage
+                            startCursor
+                            endCursor
+                        }
+                    }
+                }
+                """,
+                resolver: .init(),
+                context: NoContext(),
+                eventLoopGroup: eventLoopGroup
+            ).wait(),
+            .init(
+                data: [
+                    "comments": [
+                        "edges": [
+                            [
+                                "node": [
+                                    "id": 2,
+                                    "message": "What's up?",
+                                ],
+                            ],
+                            [
+                                "node": [
+                                    "id": 3,
+                                    "message": "Goodbye",
+                                ],
+                            ],
+                        ],
+                        "pageInfo": [
+                            "hasPreviousPage": false,
+                            "hasNextPage": false,
+                            "startCursor": "Mg==",
+                            "endCursor": "Mw==",
+                        ],
+                    ],
+                ]
+            )
+        )
+    }
+
+    /// Test that mixing `first` and `after` arguments works as intended
+    func testFirstAfter() throws {
+        XCTAssertEqual(
+            try schema.execute(
+                request: """
+                {
+                    comments(first: 1, after: "MQ==") {
+                        edges {
+                            node {
+                                id
+                                message
+                            }
+                        }
+                        pageInfo {
+                            hasPreviousPage
+                            hasNextPage
+                            startCursor
+                            endCursor
+                        }
+                    }
+                }
+                """,
+                resolver: .init(),
+                context: NoContext(),
+                eventLoopGroup: eventLoopGroup
+            ).wait(),
+            .init(
+                data: [
+                    "comments": [
+                        "edges": [
+                            [
+                                "node": [
+                                    "id": 2,
+                                    "message": "What's up?",
+                                ],
+                            ],
+                        ],
+                        "pageInfo": [
+                            "hasPreviousPage": false,
+                            "hasNextPage": true,
+                            "startCursor": "Mg==",
+                            "endCursor": "Mg==",
+                        ],
+                    ],
+                ]
+            )
+        )
+    }
+
+    /// Test that `last` argument works as intended
+    func testLast() throws {
+        XCTAssertEqual(
+            try schema.execute(
+                request: """
+                {
+                    comments(last: 1) {
+                        edges {
+                            node {
+                                id
+                                message
+                            }
+                        }
+                        pageInfo {
+                            hasPreviousPage
+                            hasNextPage
+                            startCursor
+                            endCursor
+                        }
+                    }
+                }
+                """,
+                resolver: .init(),
+                context: NoContext(),
+                eventLoopGroup: eventLoopGroup
+            ).wait(),
+            .init(
+                data: [
+                    "comments": [
+                        "edges": [
+                            [
+                                "node": [
+                                    "id": 3,
+                                    "message": "Goodbye",
+                                ],
+                            ],
+                        ],
+                        "pageInfo": [
+                            "hasPreviousPage": true,
+                            "hasNextPage": false,
+                            "startCursor": "Mw==",
+                            "endCursor": "Mw==",
+                        ],
+                    ],
+                ]
+            )
+        )
+    }
+
+    /// Test that `before` argument works as intended
+    func testBefore() throws {
+        XCTAssertEqual(
+            try schema.execute(
+                request: """
+                {
+                    comments(before: "Mw==") {
+                        edges {
+                            node {
+                                id
+                                message
+                            }
+                        }
+                        pageInfo {
+                            hasPreviousPage
+                            hasNextPage
+                            startCursor
+                            endCursor
+                        }
+                    }
+                }
+                """,
+                resolver: .init(),
+                context: NoContext(),
+                eventLoopGroup: eventLoopGroup
+            ).wait(),
+            .init(
+                data: [
+                    "comments": [
+                        "edges": [
+                            [
+                                "node": [
+                                    "id": 1,
+                                    "message": "Hello",
+                                ],
+                            ],
+                            [
+                                "node": [
+                                    "id": 2,
+                                    "message": "What's up?",
+                                ],
+                            ],
+                        ],
+                        "pageInfo": [
+                            "hasPreviousPage": false,
+                            "hasNextPage": false,
+                            "startCursor": "MQ==",
+                            "endCursor": "Mg==",
+                        ],
+                    ],
+                ]
+            )
+        )
+    }
+
+    /// Test that mixing `last` with `before` argument works as intended
+    func testLastBefore() throws {
+        XCTAssertEqual(
+            try schema.execute(
+                request: """
+                {
+                    comments(last: 1, before: "Mw==") {
+                        edges {
+                            node {
+                                id
+                                message
+                            }
+                        }
+                        pageInfo {
+                            hasPreviousPage
+                            hasNextPage
+                            startCursor
+                            endCursor
+                        }
+                    }
+                }
+                """,
+                resolver: .init(),
+                context: NoContext(),
+                eventLoopGroup: eventLoopGroup
+            ).wait(),
+            .init(
+                data: [
+                    "comments": [
+                        "edges": [
+                            [
+                                "node": [
+                                    "id": 2,
+                                    "message": "What's up?",
+                                ],
+                            ],
+                        ],
+                        "pageInfo": [
+                            "hasPreviousPage": true,
+                            "hasNextPage": false,
+                            "startCursor": "Mg==",
+                            "endCursor": "Mg==",
+                        ],
+                    ],
+                ]
+            )
+        )
+    }
+
+    /// Test that mixing `after` with `before` argument works as intended
+    func testAfterBefore() throws {
+        XCTAssertEqual(
+            try schema.execute(
+                request: """
+                {
+                    comments(after: "MQ==", before: "Mw==") {
+                        edges {
+                            node {
+                                id
+                                message
+                            }
+                        }
+                        pageInfo {
+                            hasPreviousPage
+                            hasNextPage
+                            startCursor
+                            endCursor
+                        }
+                    }
+                }
+                """,
+                resolver: .init(),
+                context: NoContext(),
+                eventLoopGroup: eventLoopGroup
+            ).wait(),
+            .init(
+                data: [
+                    "comments": [
+                        "edges": [
+                            [
+                                "node": [
+                                    "id": 2,
+                                    "message": "What's up?",
+                                ],
+                            ],
+                        ],
+                        "pageInfo": [
+                            "hasPreviousPage": false,
+                            "hasNextPage": false,
+                            "startCursor": "Mg==",
+                            "endCursor": "Mg==",
+                        ],
+                    ],
+                ]
+            )
+        )
+    }
+}

From 574ed4510db08629e3cc14a595b07f2347cc3eb2 Mon Sep 17 00:00:00 2001
From: Jay Herron <NeedleInAJayStack@protonmail.com>
Date: Sun, 12 Mar 2023 14:48:24 -0600
Subject: [PATCH 2/2] doc: Adds connections to user guide

---
 UsageGuide.md | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 101 insertions(+)

diff --git a/UsageGuide.md b/UsageGuide.md
index 4f4af9cc..be295f25 100644
--- a/UsageGuide.md
+++ b/UsageGuide.md
@@ -398,6 +398,107 @@ Each time an event fires, the following message will be generated:
 }
 ```
 
+## Cursor Connections
+
+This package supports pagination using the [Relay-based GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). To use this pagination style you must:
+
+1. Ensure any `node` types implement the `Identifiable` protocol (they must have a unique `id` field)
+2. Change the relevant resolver types to use `PaginationArguments` and return a `Connection`
+3. Add the `PaginationArguments` arguments to the schema declaration
+
+Here's an example using the schema above:
+
+```swift
+struct Person: Codable, Identifiable {
+    let id: Int
+    let name: String
+}
+
+let characters = [
+    Person(id: 1, name: "Johnny Utah"),
+    Person(id: 2, name: "Bodhi"),
+]
+
+struct PersonResolver {
+    func people(context: NoContext, arguments: PaginationArguments) throws -> Connection<Person> {
+        return try characters.connection(from: arguments)
+    }
+}
+
+struct PointBreakAPI : API {
+    typealias ContextType = NoContext
+    let resolver = PersonResolver()
+    let schema = try! Schema<PersonResolver, NoContext> {
+        Type(Person.self) {
+            Field("id", at: \.id)
+            Field("name", at: \.name)
+        }
+        ConnectionType(Person.self)
+        Query {
+            Field("people", at: PersonResolver.people) {
+                Argument("first", at: \.first)
+                Argument("last", at: \.last)
+                Argument("after", at: \.after)
+                Argument("before", at: \.before)
+            }
+        }
+    }
+}
+```
+
+A request string for this might be:
+
+```graphql
+{
+    people {
+        edges {
+            cursor
+            node {
+                id
+                name
+            }
+        }
+        pageInfo {
+            hasPreviousPage
+            hasNextPage
+            startCursor
+            endCursor
+        }
+    }
+}
+```
+
+The result of this query is a `GraphQLResult` that encodes to the following JSON:
+
+```json
+{
+    "people": {
+        "edges": [
+            {
+                "cursor": "MQ==",
+                "node": {
+                    "id": 1,
+                    "name": "Johnny Utah"
+                }
+            },
+            {
+                "cursor": "Mg==",
+                "node": {
+                    "id": 2,
+                    "name": "Bodhi"
+                }
+            },
+        ],
+        "pageInfo": {
+            "hasPreviousPage": false,
+            "hasNextPage": false,
+            "startCursor": "MQ==",
+            "endCursor": "Mg=="
+        }
+    }
+}
+```
+
 ## Federation
 
 Federation allows you split your GraphQL API into smaller services and link them back together so clients see a single larger API. More information can be found [here](https://www.apollographql.com/docs/federation). To enable federation you must:
