improvement: split keyset_page_of and page_of types

improvement: add `start_keyset` and `end_keyset` to `keyset_page_of` type
improvement: add `count` to relay fields if there exists a countable relay query
This commit is contained in:
Zach Daniel 2022-10-20 17:51:10 -04:00
parent b1bdc49ec5
commit 07e0c6170c
7 changed files with 256 additions and 96 deletions

View file

@ -18,6 +18,7 @@ spark_locals_without_parens = [
get: 2,
get: 3,
identity: 1,
keyset_field: 1,
list: 2,
list: 3,
lookup_identities: 1,

View file

@ -202,8 +202,7 @@ defmodule AshGraphql.Graphql.Resolver do
{:ok, opts}
{:ok, page_opts} ->
page_fields = get_page_fields(pagination)
field_names = resolution |> fields(page_fields) |> names_only()
field_names = resolution |> fields([]) |> names_only()
page =
if Enum.any?(field_names, &(&1 == :count)) do
@ -310,9 +309,10 @@ defmodule AshGraphql.Graphql.Resolver do
results: results,
more?: more,
after: after_cursor,
before: before_cursor
before: before_cursor,
count: count
},
true
relay?
) do
{start_cursor, end_cursor} =
case results do
@ -340,6 +340,7 @@ defmodule AshGraphql.Graphql.Resolver do
{more, not Enum.empty?(results)}
end
if relay? do
{
:ok,
%{
@ -349,6 +350,7 @@ defmodule AshGraphql.Graphql.Resolver do
has_next_page: has_next_page,
has_previous_page: has_previous_page
},
count: count,
edges:
Enum.map(results, fn result ->
%{
@ -358,18 +360,9 @@ defmodule AshGraphql.Graphql.Resolver do
end)
}
}
else
{:ok, %{results: results, count: count, start_keyset: start_cursor, end_keyset: end_cursor}}
end
defp paginate(
_resource,
_action,
%Ash.Page.Keyset{
results: results,
count: count
},
false
) do
{:ok, %{results: results, count: count}}
end
defp paginate(_resource, _action, %Ash.Page.Offset{results: results, count: count}, _) do
@ -1171,6 +1164,14 @@ defmodule AshGraphql.Graphql.Resolver do
Absinthe.Resolution.put_result(resolution, {:ok, Map.get(parent, field)})
end
def resolve_keyset(
%{source: parent} = resolution,
_field
) do
parent.__metadata__
Absinthe.Resolution.put_result(resolution, {:ok, Map.get(parent.__metadata__, :keyset)})
end
def resolve_composite_id(
%{source: parent} = resolution,
{_resource, _fields}

View file

@ -33,6 +33,11 @@ defmodule AshGraphql.Resource.Info do
Extension.get_opt(resource, [:graphql], :attribute_types, [])
end
@doc "The field name to place the keyset of a result in"
def keyset_field(resource) do
Extension.get_opt(resource, [:graphql], :keyset_field, nil)
end
@doc "Graphql field name (attribute/relationship/calculation/arguments) overrides for the resource"
def field_names(resource) do
Extension.get_opt(resource, [:graphql], :field_names, [])

View file

@ -243,6 +243,15 @@ defmodule AshGraphql.Resource do
doc:
"A nested keyword list of action names, to argument name remappings. i.e `create: [arg_name: :new_name]`"
],
keyset_field: [
type: :atom,
doc: """
If set, the keyset will be displayed on all read actions in this field.
It will always be `nil` unless at least one of the read actions on a resource uses keyset pagination.
It will also be nil on any mutation results.
"""
],
attribute_types: [
type: :keyword_list,
doc:
@ -858,9 +867,14 @@ defmodule AshGraphql.Resource do
# sobelow_skip ["DOS.StringToAtom"]
defp query_type(%{type: :list, relay?: relay?}, _resource, action, type) do
if action.pagination do
if relay? do
cond do
relay? ->
String.to_atom("#{type}_connection")
else
action.pagination.keyset? ->
String.to_atom("keyset_page_of_#{type}")
true ->
String.to_atom("page_of_#{type}")
end
else
@ -1182,6 +1196,8 @@ defmodule AshGraphql.Resource do
List.wrap(filter_input(resource, schema)) ++
filter_field_types(resource, schema) ++
List.wrap(page_of(resource, schema)) ++
List.wrap(relay_page(resource, schema)) ++
List.wrap(keyset_page_of(resource, schema)) ++
enum_definitions(resource, schema, __ENV__) ++
managed_relationship_definitions(resource, schema)
end
@ -2223,8 +2239,7 @@ defmodule AshGraphql.Resource do
end)
end
# sobelow_skip ["DOS.StringToAtom"]
defp page_of(resource, schema) do
defp relay_page(resource, schema) do
type = AshGraphql.Resource.Info.type(resource)
paginatable? =
@ -2240,6 +2255,14 @@ defmodule AshGraphql.Resource do
|> queries()
|> Enum.any?(& &1.relay?)
countable? =
resource
|> queries()
|> Enum.any?(fn query ->
action = Ash.Resource.Info.action(resource, query.action)
query.relay? && action.pagination && action.pagination.countable
end)
if relay? do
[
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
@ -2271,7 +2294,8 @@ defmodule AshGraphql.Resource do
},
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "#{inspect(type)} connection",
fields: [
fields:
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "Page information",
identifier: :page_info,
@ -2292,14 +2316,46 @@ defmodule AshGraphql.Resource do
of_type: String.to_atom("#{type}_edge")
}
}
],
]
|> add_count_to_relay(schema, countable?),
identifier: String.to_atom("#{type}_connection"),
module: schema,
name: Macro.camelize("#{type}_connection"),
__reference__: ref(__ENV__)
}
]
else
end
end
end
defp add_count_to_relay(fields, schema, true) do
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "Total count on all pages",
identifier: :count,
module: schema,
name: "count",
__reference__: ref(__ENV__),
type: :integer
}
| fields
]
end
defp add_count_to_relay(fields, _, _), do: fields
# sobelow_skip ["DOS.StringToAtom"]
defp page_of(resource, schema) do
type = AshGraphql.Resource.Info.type(resource)
paginatable? =
resource
|> Ash.Resource.Info.actions()
|> Enum.any?(fn action ->
action.type == :read && action.pagination
end)
if paginatable? do
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "A page of #{inspect(type)}",
fields: [
@ -2330,8 +2386,65 @@ defmodule AshGraphql.Resource do
__reference__: ref(__ENV__)
}
end
else
nil
end
# sobelow_skip ["DOS.StringToAtom"]
defp keyset_page_of(resource, schema) do
type = AshGraphql.Resource.Info.type(resource)
paginatable? =
resource
|> Ash.Resource.Info.actions()
|> Enum.any?(fn action ->
action.type == :read && action.pagination
end)
if paginatable? do
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "A keyset page of #{inspect(type)}",
fields: [
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The records contained in the page",
identifier: :results,
module: schema,
name: "results",
__reference__: ref(__ENV__),
type: %Absinthe.Blueprint.TypeReference.List{
of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: type
}
}
},
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The count of records",
identifier: :count,
module: schema,
name: "count",
type: :integer,
__reference__: ref(__ENV__)
},
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The first keyset in the results",
identifier: :start_keyset,
module: schema,
name: "start_keyset",
type: :string,
__reference__: ref(__ENV__)
},
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The last keyset in the results",
identifier: :end_keyset,
module: schema,
name: "end_keyset",
type: :string,
__reference__: ref(__ENV__)
}
],
identifier: String.to_atom("keyset_page_of_#{type}"),
module: schema,
name: Macro.camelize("keyset_page_of_#{type}"),
__reference__: ref(__ENV__)
}
end
end
@ -2379,7 +2492,30 @@ defmodule AshGraphql.Resource do
attributes(resource, schema) ++
relationships(resource, api, schema) ++
aggregates(resource, schema) ++
calculations(resource, api, schema)
calculations(resource, api, schema) ++
keyset(resource, schema)
end
defp keyset(resource, schema) do
case AshGraphql.Resource.Info.keyset_field(resource) do
nil ->
[]
field ->
[
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "The pagination #{field}.",
identifier: field,
module: schema,
middleware: [
{{AshGraphql.Graphql.Resolver, :resolve_keyset}, field}
],
name: to_string(field),
type: :string,
__reference__: ref(__ENV__)
}
]
end
end
defp attributes(resource, schema) do

View file

@ -35,8 +35,11 @@ defmodule AshGraphql.PaginateTest do
query KeysetPaginatedPosts {
keysetPaginatedPosts(sort: [{field: TEXT}]) {
count
startKeyset
endKeyset
results{
text
keyset
}
}
}
@ -46,9 +49,11 @@ defmodule AshGraphql.PaginateTest do
%{
data: %{
"keysetPaginatedPosts" => %{
"startKeyset" => start_keyset,
"endKeyset" => end_keyset,
"count" => 5,
"results" => [
%{"text" => "a"},
%{"text" => "a", "keyset" => keyset},
%{"text" => "b"},
%{"text" => "c"},
%{"text" => "d"},
@ -57,6 +62,10 @@ defmodule AshGraphql.PaginateTest do
}
}
}} = Absinthe.run(doc, AshGraphql.Test.Schema)
assert is_binary(keyset)
assert is_binary(start_keyset)
assert is_binary(end_keyset)
end
end

View file

@ -48,6 +48,7 @@ defmodule AshGraphql.RelayTest do
startCursor
endCursor
}
count
edges{
cursor
node {
@ -62,6 +63,7 @@ defmodule AshGraphql.RelayTest do
%{
data: %{
"getRelayTags" => %{
"count" => 5,
"pageInfo" => %{
"hasNextPage" => false,
"hasPreviousPage" => false,

View file

@ -70,7 +70,8 @@ defmodule AshGraphql.Test.Post do
attribute_types integer_as_string_in_api: :string
attribute_input_types integer_as_string_in_api: :string
field_names(text_1_and_2: :text1_and2)
field_names text_1_and_2: :text1_and2
keyset_field :keyset
queries do
get :get_post, :read
@ -162,7 +163,12 @@ defmodule AshGraphql.Test.Post do
end
read :keyset_paginated do
pagination(required?: true, keyset?: true, countable: true, default_limit: 20)
pagination(
required?: true,
keyset?: true,
countable: true,
default_limit: 20
)
end
read :paginated_without_limit do