Skip to main content

Schema connections

What are schema connections?

So far in schema generation, we have covered how GraphQL types are inferred, how query arguments for types are created, and how sift resolvers work. But all of these only allow querying down to a single node of a type. Schema connections is the ability to query over collections of nodes of a type. For example, if we want to query all markdown nodes by some criteria, it will allow us to write queries such as:

{
  allMarkdownRemark(filter: {frontmatter: {tags: {in: "wordpress"}}}) {
    edges {
      node {
        ...
      }
    }
  }
}

Other features covered by schema connections are aggregators and reducers such as distinct, group and totalCount, edges, skip, limit, and more.

Connection/Edge

A connection is an abstraction that describes a collection of nodes of a type, and how to query and navigate through them. In the above example query, allMarkdownRemark is a Connection Type. Its field edges is analogous to results. Each Edge points at a node (in the collection of all markdownRemark nodes), but it also points to the logical next and previous nodes, relative to the node in the collection (meaningful if you provided a sort arg).

Fun Fact: This stuff is all based on relay connections concepts

The ConnectionType also defines input args to perform paging using the skip/limit pattern. The actual logic for paging is defined in the graphql-skip-limit library in arrayconnection.js. It is invoked as the last part of the run-sift function. To aid in paging, the ConnectionType also defines a pageInfo field with a hasNextPage field.

The ConnectionType is defined in the graphql-skip-limit connection.js file. Its construction function takes a Type, and uses it to create a connectionType. E.g passing in MarkdownRemark Type would result in a MarkdownRemarkConnection type whose edges field would be of type MarkdownRemarkEdge.

GroupConnection

A GroupConnection is a Connection with extended functionality. Instead of simply providing the means to access nodes in a collection, it allows you to group those nodes by one of its fields. It is a Connection Type itself, but with 3 new fields: field, fieldValue, and totalCount. It adds a new input argument to ConnectionType whose value can be any (possibly nested) field on the original type.

The creation of the GroupConnection is handled in build-connection-fields.js. It’s added as the group field to the top level type connection. This is most easily shown in the below diagram.

structs mdConn MarkdownRemarkConnection (allMarkdownRemark) pageInfo edges group distinct totalCount mdEdge MarkdownRemarkEdge node next previous mdConn:edges->mdEdge mdGroupConn MarkdownRemarkGroupConnectionConnection pageInfo edges field fieldValue totalCount mdConn:group->mdGroupConn mdGroupConnEdge MarkdownRemarkGroupConnectionEdge node next previous mdGroupConn:edges->mdGroupConnEdge

Let’s see this in practice. Say we were trying to group all markdown nodes by their author. We would query the top level MarkdownRemarkConnection (allMarkdownRemark) which would return a MarkdownRemarkConnection with this new group input argument, which would return a MarkdownRemarkGroupConnectionConnection field. E.g:

{
  allMarkdownRemark {
    group(field: frontmatter___author) {
      fieldValue
      edges {
        node {
          frontmatter {
            title
          }
        },
      },
    }
  }
}

Field enum value

The frontmatter___author value is interesting. It describes a nested field. I.e, we want to group all markdown nodes by their frontmatter.author field. The author field in each frontmatter subobject. So why not use a period? The problem is that GraphQL doesn’t allow periods in fields names, so we instead use ___, and then in the resolver, we convert it back to a period.

The second interesting thing is that frontmatter___author is not a string, but rather a GraphQL enum. You can verify this by using intellisense in GraphiQL to see all possible values. This implies that Gatsby has generated all possible field names. Which is true! To do this, we create an exampleValue and then use the flat library to flatten the nested object into string keys, using ___ delimiters. This is handled by the data-tree-utils.js/buildFieldEnumValues function.

Note, the same enum mechanism is used for creation of distinct fields

Group Resolver

The resolver for the Group type is created in build-connection-fields.js. It operates on the result of the core connection query (e.g allMarkdownRemark), which is a Connection object with edges. From these edges, we retrieve all the nodes (each edge has a node field). And now we can use lodash to group those nodes by the fieldname argument (e.g field: frontmatter___author).

If sorting was specified (see below), we sort the groups by fieldname, and then apply any skip/limit arguments using the graphql-skip-limit library. Finally we are ready to fill in our field, fieldValue, and totalCount fields on each group, and we can return our resolved node.

Input filter creation

Just like in gql type input filters, we must generate standard input filters on our connectiontype arguments. As a reminder, these allow us to query any fields by predicates such as { eq: "value" }, or { glob: "foo*" }. This is covered by the same functions (in infer-graphql-object-type.js), except that we’re passing in Connection types instead of basic types. The only difference is that we use the sort field (see below)

Sorting

A sort argument can be added to the Connection type (not the GroupConnection type). You can sort by any (possibly nested) field in the connection results. These are enums that are created via the same mechanism described in enum fields. Except that the inference of these enums occurs in infer-graphql-input-type.js.

The Sort Input Type itself is created in build-node-connections.js and implemented by create-sort-field.js. The actual sorting occurs in run-sift (below).

Connection Resolver (sift)

Finally, we’re ready to define the resolver for our Connection type (in build-node-connections.js). This is where we come up with the name all${type} (e.g allMarkdownRemark) that is so common in Gatsby queries. The resolver is fairly simple. It uses the sift.js library to query across all nodes of the same type in redux. The big difference is that we supply the connection: true parameter to run-sift.js which is where sorting, and pagination is actually executed. See Querying with Sift for how this actually works.


Was this helpful? edit this page on GitHub