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: 2,
get: 3, get: 3,
identity: 1, identity: 1,
keyset_field: 1,
list: 2, list: 2,
list: 3, list: 3,
lookup_identities: 1, lookup_identities: 1,

View file

@ -202,8 +202,7 @@ defmodule AshGraphql.Graphql.Resolver do
{:ok, opts} {:ok, opts}
{:ok, page_opts} -> {:ok, page_opts} ->
page_fields = get_page_fields(pagination) field_names = resolution |> fields([]) |> names_only()
field_names = resolution |> fields(page_fields) |> names_only()
page = page =
if Enum.any?(field_names, &(&1 == :count)) do if Enum.any?(field_names, &(&1 == :count)) do
@ -310,9 +309,10 @@ defmodule AshGraphql.Graphql.Resolver do
results: results, results: results,
more?: more, more?: more,
after: after_cursor, after: after_cursor,
before: before_cursor before: before_cursor,
count: count
}, },
true relay?
) do ) do
{start_cursor, end_cursor} = {start_cursor, end_cursor} =
case results do case results do
@ -340,36 +340,29 @@ defmodule AshGraphql.Graphql.Resolver do
{more, not Enum.empty?(results)} {more, not Enum.empty?(results)}
end end
{ if relay? do
:ok, {
%{ :ok,
page_info: %{ %{
start_cursor: start_cursor, page_info: %{
end_cursor: end_cursor, start_cursor: start_cursor,
has_next_page: has_next_page, end_cursor: end_cursor,
has_previous_page: has_previous_page has_next_page: has_next_page,
}, has_previous_page: has_previous_page
edges: },
Enum.map(results, fn result -> count: count,
%{ edges:
cursor: result.__metadata__.keyset, Enum.map(results, fn result ->
node: result %{
} cursor: result.__metadata__.keyset,
end) node: result
}
end)
}
} }
} else
end {: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 end
defp paginate(_resource, _action, %Ash.Page.Offset{results: results, count: count}, _) do 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)}) Absinthe.Resolution.put_result(resolution, {:ok, Map.get(parent, field)})
end 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( def resolve_composite_id(
%{source: parent} = resolution, %{source: parent} = resolution,
{_resource, _fields} {_resource, _fields}

View file

@ -33,6 +33,11 @@ defmodule AshGraphql.Resource.Info do
Extension.get_opt(resource, [:graphql], :attribute_types, []) Extension.get_opt(resource, [:graphql], :attribute_types, [])
end 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" @doc "Graphql field name (attribute/relationship/calculation/arguments) overrides for the resource"
def field_names(resource) do def field_names(resource) do
Extension.get_opt(resource, [:graphql], :field_names, []) Extension.get_opt(resource, [:graphql], :field_names, [])

View file

@ -243,6 +243,15 @@ defmodule AshGraphql.Resource do
doc: doc:
"A nested keyword list of action names, to argument name remappings. i.e `create: [arg_name: :new_name]`" "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: [ attribute_types: [
type: :keyword_list, type: :keyword_list,
doc: doc:
@ -858,10 +867,15 @@ defmodule AshGraphql.Resource do
# sobelow_skip ["DOS.StringToAtom"] # sobelow_skip ["DOS.StringToAtom"]
defp query_type(%{type: :list, relay?: relay?}, _resource, action, type) do defp query_type(%{type: :list, relay?: relay?}, _resource, action, type) do
if action.pagination do if action.pagination do
if relay? do cond do
String.to_atom("#{type}_connection") relay? ->
else String.to_atom("#{type}_connection")
String.to_atom("page_of_#{type}")
action.pagination.keyset? ->
String.to_atom("keyset_page_of_#{type}")
true ->
String.to_atom("page_of_#{type}")
end end
else else
%Absinthe.Blueprint.TypeReference.NonNull{ %Absinthe.Blueprint.TypeReference.NonNull{
@ -1182,6 +1196,8 @@ defmodule AshGraphql.Resource do
List.wrap(filter_input(resource, schema)) ++ List.wrap(filter_input(resource, schema)) ++
filter_field_types(resource, schema) ++ filter_field_types(resource, schema) ++
List.wrap(page_of(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__) ++ enum_definitions(resource, schema, __ENV__) ++
managed_relationship_definitions(resource, schema) managed_relationship_definitions(resource, schema)
end end
@ -2223,8 +2239,7 @@ defmodule AshGraphql.Resource do
end) end)
end end
# sobelow_skip ["DOS.StringToAtom"] defp relay_page(resource, schema) do
defp page_of(resource, schema) do
type = AshGraphql.Resource.Info.type(resource) type = AshGraphql.Resource.Info.type(resource)
paginatable? = paginatable? =
@ -2240,6 +2255,14 @@ defmodule AshGraphql.Resource do
|> queries() |> queries()
|> Enum.any?(& &1.relay?) |> 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 if relay? do
[ [
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{ %Absinthe.Blueprint.Schema.ObjectTypeDefinition{
@ -2271,67 +2294,157 @@ defmodule AshGraphql.Resource do
}, },
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{ %Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "#{inspect(type)} connection", description: "#{inspect(type)} connection",
fields: [ fields:
%Absinthe.Blueprint.Schema.FieldDefinition{ [
description: "Page information", %Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :page_info, description: "Page information",
module: schema, identifier: :page_info,
name: "page_info", module: schema,
__reference__: ref(__ENV__), name: "page_info",
type: %Absinthe.Blueprint.TypeReference.NonNull{ __reference__: ref(__ENV__),
of_type: :page_info type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: :page_info
}
},
%Absinthe.Blueprint.Schema.FieldDefinition{
description: "#{inspect(type)} edges",
identifier: :edges,
module: schema,
name: "edges",
__reference__: ref(__ENV__),
type: %Absinthe.Blueprint.TypeReference.List{
of_type: String.to_atom("#{type}_edge")
}
} }
}, ]
%Absinthe.Blueprint.Schema.FieldDefinition{ |> add_count_to_relay(schema, countable?),
description: "#{inspect(type)} edges",
identifier: :edges,
module: schema,
name: "edges",
__reference__: ref(__ENV__),
type: %Absinthe.Blueprint.TypeReference.List{
of_type: String.to_atom("#{type}_edge")
}
}
],
identifier: String.to_atom("#{type}_connection"), identifier: String.to_atom("#{type}_connection"),
module: schema, module: schema,
name: Macro.camelize("#{type}_connection"), name: Macro.camelize("#{type}_connection"),
__reference__: ref(__ENV__) __reference__: ref(__ENV__)
} }
] ]
else
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "A 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__)
}
],
identifier: String.to_atom("page_of_#{type}"),
module: schema,
name: Macro.camelize("page_of_#{type}"),
__reference__: ref(__ENV__)
}
end end
else end
nil 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: [
%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__)
}
],
identifier: String.to_atom("page_of_#{type}"),
module: schema,
name: Macro.camelize("page_of_#{type}"),
__reference__: ref(__ENV__)
}
end
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
end end
@ -2379,7 +2492,30 @@ defmodule AshGraphql.Resource do
attributes(resource, schema) ++ attributes(resource, schema) ++
relationships(resource, api, schema) ++ relationships(resource, api, schema) ++
aggregates(resource, 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 end
defp attributes(resource, schema) do defp attributes(resource, schema) do

View file

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

View file

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

View file

@ -70,7 +70,8 @@ defmodule AshGraphql.Test.Post do
attribute_types integer_as_string_in_api: :string attribute_types integer_as_string_in_api: :string
attribute_input_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 queries do
get :get_post, :read get :get_post, :read
@ -162,7 +163,12 @@ defmodule AshGraphql.Test.Post do
end end
read :keyset_paginated do 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 end
read :paginated_without_limit do read :paginated_without_limit do