@@ -6,74 +6,98 @@ namespace FSharp.Data.GraphQL.Server.Relay
66open FSharp.Data .GraphQL .Types
77open FSharp.Data .GraphQL .Types .Patterns
88
9- /// Record used to represent Relay node with cursor identifier.
9+ /// <summary >
10+ /// Represents a Relay edge – an object with a cursor and a node.
11+ /// Edges are used to traverse connections in Relay pagination.
12+ /// </summary >
13+ /// <typeparam name =" Node " >The type of the node at the end of this edge.</typeparam >
1014type Edge < 'Node > = {
11- /// Cursor used to identify current node.
15+ /// < summary >Opaque cursor string used to identify this node's position in the connection.</ summary >
1216 Cursor : string
13- /// Object satisfying Relay Node interface definition.
17+ /// < summary >The object at the end of this edge. Must satisfy the Relay Node interface.</ summary >
1418 Node : 'Node
1519}
1620
17- module Edge =
18-
19- let map mapping ( edge : Edge < 'T >) : Edge < 'U > =
20- { Cursor = edge.Cursor ; Node = mapping edge.Node }
21-
22- /// Record used to represent a information about single page of
23- /// results to Relay. Relay uses cursor id to identify the order
24- /// between the pages.
21+ /// < summary >
22+ /// Information about pagination in a Relay connection.
23+ /// Follows the Relay Cursor Connections Specification.
24+ /// </ summary >
25+ /// < remarks >
26+ /// PageInfo provides pagination metadata that allows clients to determine
27+ /// whether more pages are available and where to continue pagination.
28+ /// </ remarks >
2529type PageInfo = {
26- /// Should be true, if the next page is available.
27- /// False if current page is the last page of results.
30+ /// <summary >
31+ /// Indicates whether more items exist following the current page when paginating forward.
32+ /// Returns <c >false</c > if this is the last page.
33+ /// </summary >
2834 HasNextPage : Async < bool >
29- /// Should be true, if the previous page is available.
30- /// False if current page is the first page of results.
35+ /// <summary >
36+ /// Indicates whether more items exist before the current page when paginating backward.
37+ /// Returns <c >false</c > if this is the first page.
38+ /// </summary >
3139 HasPreviousPage : Async < bool >
32- /// Optional cursor used to identify begining of the results.
40+ /// <summary >
41+ /// The cursor corresponding to the first edge in the current page.
42+ /// Returns <c >None</c > if the page is empty.
43+ /// </summary >
3344 StartCursor : Async < string option >
34- /// Optional cursor used to identify the end of the results.
45+ /// <summary >
46+ /// The cursor corresponding to the last edge in the current page.
47+ /// Returns <c >None</c > if the page is empty.
48+ /// </summary >
3549 EndCursor : Async < string option >
3650}
3751
38- /// Record representing Relay connection object. Connection describes
39- /// a set of results (Relay nodes) returned from the server. Instead
40- /// of statically identifying paged results, relay uses notion of the
41- /// cursor, which allows to track result set windows, while the
42- /// result set itself may change over time.
52+ /// <summary >
53+ /// Represents a Relay connection – a paginated set of edges with metadata.
54+ /// Follows the Relay Cursor Connections Specification.
55+ /// </summary >
56+ /// <typeparam name =" Node " >The type of nodes contained in this connection.</typeparam >
57+ /// <remarks >
58+ /// Unlike traditional offset-based pagination, Relay connections use cursors
59+ /// to navigate through results, allowing the result set to change between requests
60+ /// while maintaining consistent pagination behavior.
61+ /// </remarks >
4362type Connection < 'Node > = {
44- /// Optional value describing total number of results avaiable
45- /// at the time.
63+ /// <summary >
64+ /// The total count of items in the entire result set, ignoring pagination.
65+ /// Returns <c >None</c > when the count is not available or would be too expensive to compute.
66+ /// </summary >
4667 TotalCount : Async < int option >
47- /// Information about current results page.
68+ /// <summary >
69+ /// Metadata about the current page, including cursors and availability of adjacent pages.
70+ /// </summary >
71+ /// <seealso cref =" PageInfo " />
4872 PageInfo : PageInfo
49- /// List of edges (Relay nodes with cursors) returned as results.
73+ /// <summary >
74+ /// The list of edges in the current page. Each edge contains a cursor and a node.
75+ /// </summary >
76+ /// <seealso cref =" Edge{T} " />
5077 Edges : Async < Edge < 'Node > seq >
5178}
5279// interface seq<'Node> with
5380// member x.GetEnumerator () = (Seq.map (fun edge -> edge.Node) x.Edges).GetEnumerator()
5481// member x.GetEnumerator () : System.Collections.IEnumerator = upcast (x :> seq<'Node>).GetEnumerator()
5582
56- module Connection =
57-
58- let map mapping ( conn : Connection < 'T >) : Connection < 'U > =
59- {
60- TotalCount = conn.TotalCount
61- PageInfo = conn.PageInfo
62- Edges = async {
63- let! edges = conn.Edges
64- return edges |> Seq.map ( Edge.map mapping)
65- }
66- }
67-
68- /// Slice info union describing Relay cursor progression.
83+ /// <summary >
84+ /// Describes pagination direction and parameters for slicing a Relay connection.
85+ /// </summary >
86+ /// <typeparam name =" Cursor " >The type used to represent cursor values.</typeparam >
87+ /// <remarks >
88+ /// SliceInfo encapsulates the "first/after" (forward) or "last/before" (backward)
89+ /// pagination arguments as defined in the Relay specification.
90+ /// </remarks >
6991type SliceInfo < 'Cursor > =
70- /// Return page of ` first ` results ` after ` provided cursor value.
71- /// If ` after ` value was not provided, start from the beginning of
72- /// the result set.
92+ /// <summary >
93+ /// Forward pagination: retrieve the first N items after a given cursor.
94+ /// If <c >After</c > is <c >ValueNone</c >, starts from the beginning of the result set.
95+ /// </summary >
7396 | Forward of First : int * After : 'Cursor voption
74- /// Return page of ` last ` results ` before ` provided cursor value.
75- /// If ` before ` value was not provided, return ` last ` results of
76- /// the result set.
97+ /// <summary >
98+ /// Backward pagination: retrieve the last N items before a given cursor.
99+ /// If <c >Before</c > is <c >ValueNone</c >, retrieves the last N items from the end of the result set.
100+ /// </summary >
77101 | Backward of Last : int * Before : 'Cursor voption
78102
79103 member this.PageSize =
@@ -86,24 +110,19 @@ type SliceInfo<'Cursor> =
86110 | Forward (_, after) -> after
87111 | Backward (_, before) -> before
88112
89- [<RequireQualifiedAccess>]
90- module Cursor =
91- [<Literal>]
92- let Prefix = " arrayconnection"
93- let toOffset defaultValue cursor =
94- match cursor with
95- | GlobalId ( Prefix, id) ->
96- match System.Int32.TryParse id with
97- | true , num -> num
98- | false , _ -> defaultValue
99- | _ -> defaultValue
100- let ofOffset offset = toGlobalId Prefix ( offset.ToString ())
101-
102113[<AutoOpen>]
103114module Definitions =
104115
105- /// Active pattern used to match context arguments in order
106- /// to construct Relay slice information.
116+ /// <summary >
117+ /// Active pattern that extracts Relay pagination arguments from a GraphQL field context.
118+ /// </summary >
119+ /// <param name =" ctx " >The GraphQL field resolution context.</param >
120+ /// <returns >
121+ /// <c >ValueSome(Forward)</c > if "first" argument is present (with optional "after"),
122+ /// <c >ValueSome(Backward)</c > if "last" argument is present (with optional "before"),
123+ /// or <c >ValueNone</c > if no pagination arguments are found.
124+ /// </returns >
125+ /// <seealso cref =" SliceInfo{T} " />
107126 [<return : Struct>]
108127 let (| SliceInfo | _ |) ( ctx : ResolveFieldContext ) =
109128 match ctx.TryArg " first" , ctx.TryArg " after" with
@@ -115,7 +134,10 @@ module Definitions =
115134 | ValueSome ( last), ( before) -> ValueSome ( Backward ( last, before))
116135 | _, _ -> ValueNone
117136
118- /// Object defintion representing information about pagination in context of Relay connection
137+ /// <summary >
138+ /// GraphQL object type definition for <see cref =" PageInfo " />.
139+ /// Defines the schema for pagination metadata in Relay connections.
140+ /// </summary >
119141 let PageInfo =
120142 Define.Object< PageInfo> (
121143 name = " PageInfo" ,
@@ -148,12 +170,19 @@ module Definitions =
148170 ]
149171 )
150172
151- /// Converts existing output type defintion into an edge in a Relay connection.
152- /// <paramref name =" nodeType " /> must not be a List.
173+ /// <summary >
174+ /// Creates a GraphQL object type definition for a Relay edge wrapping the specified node type.
175+ /// </summary >
176+ /// <param name =" nodeType " >The GraphQL output type definition for nodes. Must not be a list type.</param >
177+ /// <typeparam name =" Node " >The .NET type of nodes in the edge.</typeparam >
178+ /// <returns >An <c >ObjectDef< ; Edge< ; 'Node> ;> ; </c > with "cursor" and "node" fields.</returns >
179+ /// <exception cref =" System.Exception " >Thrown if <paramref name =" nodeType " /> is a list type.</exception >
180+ /// <seealso cref =" Edge{T} " />
181+ /// <seealso cref =" ConnectionOf " />
153182 let EdgeOf ( nodeType : #OutputDef<'Node> ) =
154183 match nodeType with
155184 | List _ ->
156- failwith $" {nodeType.ToString ()} cannot be used as a relay Edge or Connection - only non-list type definitions are allowed"
185+ failwith $" {nodeType.ToString ()} cannot be used as a relay Edge or Connection – only non-list type definitions are allowed"
157186 | Named n ->
158187 Define.Object< Edge< 'Node>> (
159188 name = n.Name + " Edge" ,
@@ -170,8 +199,17 @@ module Definitions =
170199 )
171200 | _ -> failwithf " Unexpected value of nodeType: %O " nodeType
172201
173- /// Converts existing output type definition into Relay-compatible connection.
174- /// <paramref name =" nodeType " /> must not be a List.
202+ /// <summary >
203+ /// Creates a GraphQL object type definition for a Relay connection containing the specified node type.
204+ /// </summary >
205+ /// <param name =" nodeType " >The GraphQL output type definition for nodes. Must not be a list type.</param >
206+ /// <typeparam name =" Node " >The .NET type of nodes in the connection.</typeparam >
207+ /// <returns >
208+ /// An <c >ObjectDef< ; Connection< ; 'Node> ;> ; </c > with "totalCount", "pageInfo", and "edges" fields.
209+ /// </returns >
210+ /// <exception cref =" System.Exception " >Thrown if <paramref name =" nodeType " /> is a list type.</exception >
211+ /// <seealso cref =" Connection{T} " />
212+ /// <seealso cref =" EdgeOf " />
175213 let ConnectionOf ( nodeType : #OutputDef<'Node> ) =
176214 let n =
177215 match nodeType with
@@ -194,22 +232,105 @@ module Definitions =
194232 ]
195233 )
196234
235+ [<RequireQualifiedAccess>]
236+ module Cursor =
237+ /// <summary >Prefix used for array-based connection cursors when encoding as Global IDs.</summary >
238+ [<Literal>]
239+ let Prefix = " arrayconnection"
240+
241+ /// <summary >
242+ /// Decodes a cursor string to an integer offset.
243+ /// </summary >
244+ /// <param name =" defaultValue " >The value to return if decoding fails.</param >
245+ /// <param name =" cursor " >The cursor string to decode (expected to be a Global ID).</param >
246+ /// <returns >The decoded offset, or <paramref name =" defaultValue " /> if parsing fails.</returns >
247+ let toOffset defaultValue cursor =
248+ match cursor with
249+ | GlobalId ( Prefix, id) ->
250+ match System.Int32.TryParse id with
251+ | true , num -> num
252+ | false , _ -> defaultValue
253+ | _ -> defaultValue
254+
255+ /// <summary >
256+ /// Encodes an integer offset as a cursor string using the Global ID format.
257+ /// </summary >
258+ /// <param name =" offset " >The zero-based array offset to encode.</param >
259+ /// <returns >An opaque cursor string suitable for use in Relay pagination.</returns >
260+ let ofOffset offset = toGlobalId Prefix ( offset.ToString ())
261+
262+ module Edge =
263+
264+ /// <summary >
265+ /// Transforms the node in an edge while preserving the cursor.
266+ /// </summary >
267+ /// <param name =" mapping " >The function to transform the node from type <typeparamref name =" T " /> to type <typeparamref name =" U " />.</param >
268+ /// <param name =" edge " >The edge to transform.</param >
269+ /// <typeparam name =" T " >The type of the node in the source edge.</typeparam >
270+ /// <typeparam name =" U " >The type of the node in the resulting edge.</typeparam >
271+ /// <returns >A new edge with the transformed node and the same cursor.</returns >
272+ let map mapping ( edge : Edge < 'T >) : Edge < 'U > =
273+ { Cursor = edge.Cursor; Node = mapping edge.Node }
274+
197275[<RequireQualifiedAccess>]
198276module Connection =
199277
200- /// List of argument definitions used to apply
201- /// Relay's connection forwarding ability.
278+ /// <summary >
279+ /// Transforms a <see cref =" Connection{T} " /> into a <see cref =" Connection{U} " />
280+ /// by applying a mapping function to each node while preserving cursor information and pagination metadata.
281+ /// </summary >
282+ /// <param name =" mapping " >The function to transform nodes from type <typeparamref name =" T " /> to type <typeparamref name =" U " />.</param >
283+ /// <param name =" conn " >The source connection to transform.</param >
284+ /// <typeparam name =" T " >The type of nodes in the source connection.</typeparam >
285+ /// <typeparam name =" U " >The type of nodes in the resulting connection.</typeparam >
286+ /// <returns >A new connection with transformed nodes. Cursors, page info, and total count are preserved unchanged.</returns >
287+ /// <seealso cref =" Edge.map " />
288+ let map mapping ( conn : Connection < 'T >) : Connection < 'U > =
289+ {
290+ TotalCount = conn.TotalCount
291+ PageInfo = conn.PageInfo
292+ Edges = async {
293+ let! edges = conn.Edges
294+ return edges |> Seq.map ( Edge.map mapping)
295+ }
296+ }
297+
298+ /// <summary >
299+ /// Argument definitions for forward pagination ("first" and "after").
300+ /// Use these when defining GraphQL fields that support forward-only pagination.
301+ /// </summary >
302+ /// <seealso cref =" backwardArgs " />
303+ /// <seealso cref =" allArgs " />
202304 let forwardArgs = [ Define.Input ( " first" , Nullable IntType); Define.Input ( " after" , Nullable StringType) ]
203305
204- /// List of argument definitions used to apply
205- /// Relay's connection backwarding ability.
306+ /// <summary >
307+ /// Argument definitions for backward pagination ("last" and "before").
308+ /// Use these when defining GraphQL fields that support backward-only pagination.
309+ /// </summary >
310+ /// <seealso cref =" forwardArgs " />
311+ /// <seealso cref =" allArgs " />
206312 let backwardArgs = [ Define.Input ( " last" , Nullable IntType); Define.Input ( " before" , Nullable StringType) ]
207313
208- /// List of argument definitions used to apply
209- /// Relay's ability to move connections forwards and backwards.
314+ /// <summary >
315+ /// Complete set of argument definitions for bidirectional pagination.
316+ /// Combines <see cref =" forwardArgs " /> and <see cref =" backwardArgs " />.
317+ /// Use these when defining GraphQL fields that support both forward and backward pagination.
318+ /// </summary >
210319 let allArgs = forwardArgs @ backwardArgs
211320
212- /// Construct a Relay Connection object from the provided array.
321+ /// <summary >
322+ /// Creates a Relay connection from an array of nodes using array indices as cursors.
323+ /// </summary >
324+ /// <param name =" array " >The array of nodes to convert into a connection.</param >
325+ /// <typeparam name =" Node " >The type of nodes in the array.</typeparam >
326+ /// <returns >
327+ /// A <see cref =" Connection{T} " /> containing all items from the array.
328+ /// The <see cref =" PageInfo " /> indicates this is a complete result set (no adjacent pages).
329+ /// </returns >
330+ /// <remarks >
331+ /// This is a convenience function for simple scenarios. For proper pagination,
332+ /// use slicing based on <see cref =" SliceInfo{T} " />.
333+ /// </remarks >
213334 let ofArray array =
214335 let edges =
215336 array
0 commit comments