diff --git a/lib/graphql/resolver.ex b/lib/graphql/resolver.ex index 0debca35..f26089cc 100644 --- a/lib/graphql/resolver.ex +++ b/lib/graphql/resolver.ex @@ -1328,9 +1328,37 @@ defmodule AshGraphql.Graphql.Resolver do end end - defp paginate_with_offset(%Ash.Page.Offset{results: results, count: count, more?: more?}) do - {:ok, %{results: results, count: count, more?: more?}} - end + defp paginate_with_offset(%Ash.Page.Offset{ + results: results, + count: count, + more?: more?, + offset: offset, + limit: limit + }) do + total_pages = get_total_pages(count, limit) + has_next_page = more? + has_previous_page = offset > 0 + page_number = get_current_page(%Ash.Page.Offset{limit: limit, offset: offset}, total_pages) + last_page = total_pages + + {:ok, + %{ + results: results, + count: count, + more?: more?, + has_next_page: has_next_page, + has_previous_page: has_previous_page, + page_number: page_number, + last_page: last_page + }} + end + + defp get_total_pages(count, _) when count in [0, nil], do: 1 + defp get_total_pages(_, nil), do: 1 + defp get_total_pages(total_count, limit), do: ceil(total_count / limit) + + defp get_current_page(%Ash.Page.Offset{limit: limit, offset: offset}, total), + do: min(ceil(offset / limit) + 1, total) defp paginate(_resource, _gql_query, _action, %Ash.Page.Keyset{} = keyset, relay?) do paginate_with_keyset(keyset, relay?) diff --git a/lib/resource/resource.ex b/lib/resource/resource.ex index 6f8102ec..c9914d79 100644 --- a/lib/resource/resource.ex +++ b/lib/resource/resource.ex @@ -3832,6 +3832,46 @@ defmodule AshGraphql.Resource do ] end + defp add_pagination_metadata(fields, schema, true) do + [ + %Absinthe.Blueprint.Schema.FieldDefinition{ + description: "Whether or not there is a next page", + identifier: :has_next_page, + module: schema, + name: "has_next_page", + __reference__: ref(__ENV__), + type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :boolean} + }, + %Absinthe.Blueprint.Schema.FieldDefinition{ + description: "Whether or not there is a previous page", + identifier: :has_previous_page, + module: schema, + name: "has_previous_page", + __reference__: ref(__ENV__), + type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :boolean} + }, + %Absinthe.Blueprint.Schema.FieldDefinition{ + description: "The number of the current page", + identifier: :page_number, + module: schema, + name: "page_number", + __reference__: ref(__ENV__), + type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :integer} + }, + %Absinthe.Blueprint.Schema.FieldDefinition{ + description: "The number of the last page", + identifier: :last_page, + module: schema, + name: "last_page", + __reference__: ref(__ENV__), + type: %Absinthe.Blueprint.TypeReference.NonNull{of_type: :integer} + } + | fields + ] + end + + defp add_pagination_metadata(fields, _, _), do: fields + defp add_count_to_page(fields, schema, true) do [ %Absinthe.Blueprint.Schema.FieldDefinition{ @@ -3865,19 +3905,10 @@ defmodule AshGraphql.Resource do of_type: type } } - }, - %Absinthe.Blueprint.Schema.FieldDefinition{ - description: "Whether or not there is a next page", - identifier: :more?, - module: schema, - name: "has_next_page", - __reference__: ref(__ENV__), - type: %Absinthe.Blueprint.TypeReference.NonNull{ - of_type: :boolean - } } ] - |> add_count_to_page(schema, countable?), + |> add_count_to_page(schema, countable?) + |> add_pagination_metadata(schema, countable?), identifier: String.to_atom("page_of_#{type}"), module: schema, name: Macro.camelize("page_of_#{type}"), diff --git a/test/paginate_test.exs b/test/paginate_test.exs index 447ffae6..8e5d738b 100644 --- a/test/paginate_test.exs +++ b/test/paginate_test.exs @@ -128,6 +128,142 @@ defmodule AshGraphql.PaginateTest do :ok end + test "retruns pagination metadata" do + doc = """ + query PaginatedPosts { + paginatedPosts(limit: 1, sort: [{field: TEXT}]) { + count + hasNextPage + hasPreviousPage + pageNumber + lastPage + results{ + text + } + } + } + """ + + assert {:ok, + %{ + data: %{ + "paginatedPosts" => %{ + "count" => 5, + "hasNextPage" => true, + "hasPreviousPage" => false, + "pageNumber" => 1, + "lastPage" => 5, + "results" => [ + %{"text" => "a"} + ] + } + } + }} = Absinthe.run(doc, AshGraphql.Test.Schema) + end + + test "pagination metadata is correct when offset is 2" do + doc = """ + query PaginatedPosts { + paginatedPosts(limit: 1, offset: 2, sort: [{field: TEXT}]) { + count + hasNextPage + hasPreviousPage + pageNumber + lastPage + results{ + text + } + } + } + """ + + assert {:ok, + %{ + data: %{ + "paginatedPosts" => %{ + "count" => 5, + "hasNextPage" => true, + "hasPreviousPage" => true, + "pageNumber" => 3, + "lastPage" => 5, + "results" => [ + %{"text" => "c"} + ] + } + } + }} = Absinthe.run(doc, AshGraphql.Test.Schema) + end + + test "pagination metadata is correct when offset is 4" do + doc = """ + query PaginatedPosts { + paginatedPosts(limit: 1, offset: 4, sort: [{field: TEXT}]) { + count + hasNextPage + hasPreviousPage + pageNumber + lastPage + results{ + text + } + } + } + """ + + assert {:ok, + %{ + data: %{ + "paginatedPosts" => %{ + "count" => 5, + "hasNextPage" => false, + "hasPreviousPage" => true, + "pageNumber" => 5, + "lastPage" => 5, + "results" => [ + %{"text" => "e"} + ] + } + } + }} = Absinthe.run(doc, AshGraphql.Test.Schema) + end + + test "pagination metadata is correct when the is no limit" do + doc = """ + query PaginatedPosts { + paginatedPosts(sort: [{field: TEXT}]) { + count + hasNextPage + hasPreviousPage + pageNumber + lastPage + results{ + text + } + } + } + """ + + assert {:ok, + %{ + data: %{ + "paginatedPosts" => %{ + "count" => 5, + "hasNextPage" => false, + "hasPreviousPage" => false, + "pageNumber" => 1, + "lastPage" => 1, + "results" => [ + %{"text" => "a"}, + %{"text" => "b"}, + %{"text" => "c"}, + %{"text" => "d"}, + %{"text" => "e"} + ] + } + } + }} = Absinthe.run(doc, AshGraphql.Test.Schema) + end + test "default_limit records are fetched" do doc = """ query PaginatedPosts {