lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb



# Copyright 2024 Block, Inc.
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#
# frozen_string_literal: true

require "elastic_graph/graphql/resolvers/resolvable_value"

module ElasticGraph
  class GraphQL
    module Resolvers
      module RelayConnection
        # Provides the `PageInfo` field values required by the relay spec.
        #
        # The relay connections spec defines an algorithm behind `hasPreviousPage` and `hasNextPage`:
        # https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo.Fields
        #
        # However, it has a couple bugs as currently written (https://github.com/facebook/relay/issues/2787),
        # so we have implemented our own algorithm instead. It would be nice to calculate `hasPreviousPage`
        # and `hasNextPage` on-demand in a resolver, so we do not spend any effort on it if the client has
        # not requested those fields, but it is quite hard to calculate them after the fact: we need to know
        # whether we removed any leading or trailing items while processing the list to accurately answer
        # the question, "do we have a page before or after the one we are returning?".
        #
        # Note: it's not clear what values `hasPreviousPage` and `hasNextPage` should have when we are returning
        # a blank page (the client isn't being returned any cursors to continue paginating from!). This logic,
        # as written, will normally cause both fields to be `true` (our request of `size: size + 1` will get us
        # a list of 1 document, which will then be removed, causing `items.first` and `items.last` to
        # both change to `nil`). However, if the datastore returns an empty list to us than `false` will be returned
        # for one or both fields, based on the presence or absence of the `before`/`after` cursors in the pagination
        # arguments. Regardless, given that it's not clear what the correct value is, we are just doing the
        # least-effort thing and not putting any special handling for this case in place.
        class PageInfo < ResolvableValue.new(
          # The array of nodes for this page before we applied necessary truncation.
          :before_truncation_nodes,
          # The array of edges for this page.
          :edges,
          # The paginator built from the field arguments.
          :paginator
        )
          # @dynamic initialize, with, before_truncation_nodes, edges, paginator

          def start_cursor
            edges.first&.cursor
          end

          def end_cursor
            edges.last&.cursor
          end

          def has_previous_page
            # If we dropped the first node during truncation then it means we removed some leading docs, indicating a previous page.
            return true if edges.first&.node != before_truncation_nodes.first

            # Nothing exists both before and after the same cursor, and there is therefore no page before that set of results.
            return false if paginator.before == paginator.after

            # If an `after` cursor was passed then there is definitely at least one doc before the page we are
            # returning (the one matching the cursor), assuming the client did not construct a cursor by hand
            # (which we do not support).
            !!paginator.after
          end

          def has_next_page
            # If we dropped the last node during truncation then it means we removed some trailing docs, indicating a next page.
            return true if edges.last&.node != before_truncation_nodes.last

            # Nothing exists both before and after the same cursor, and there is therefore no page after that set of results.
            return false if paginator.before == paginator.after

            # If a `before` cursor was passed then there is definitely at least one doc after the page we are
            # returning (the one matching the cursor), assuming the client did not construct a cursor by hand
            # (which we do not support).
            !!paginator.before
          end
        end
      end
    end
  end
end