mirror of
https://github.com/ash-project/ash_graphql.git
synced 2024-09-19 21:03:09 +12:00
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:
parent
b1bdc49ec5
commit
07e0c6170c
7 changed files with 256 additions and 96 deletions
|
@ -18,6 +18,7 @@ spark_locals_without_parens = [
|
|||
get: 2,
|
||||
get: 3,
|
||||
identity: 1,
|
||||
keyset_field: 1,
|
||||
list: 2,
|
||||
list: 3,
|
||||
lookup_identities: 1,
|
||||
|
|
|
@ -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,36 +340,29 @@ defmodule AshGraphql.Graphql.Resolver do
|
|||
{more, not Enum.empty?(results)}
|
||||
end
|
||||
|
||||
{
|
||||
:ok,
|
||||
%{
|
||||
page_info: %{
|
||||
start_cursor: start_cursor,
|
||||
end_cursor: end_cursor,
|
||||
has_next_page: has_next_page,
|
||||
has_previous_page: has_previous_page
|
||||
},
|
||||
edges:
|
||||
Enum.map(results, fn result ->
|
||||
%{
|
||||
cursor: result.__metadata__.keyset,
|
||||
node: result
|
||||
}
|
||||
end)
|
||||
if relay? do
|
||||
{
|
||||
:ok,
|
||||
%{
|
||||
page_info: %{
|
||||
start_cursor: start_cursor,
|
||||
end_cursor: end_cursor,
|
||||
has_next_page: has_next_page,
|
||||
has_previous_page: has_previous_page
|
||||
},
|
||||
count: count,
|
||||
edges:
|
||||
Enum.map(results, fn result ->
|
||||
%{
|
||||
cursor: result.__metadata__.keyset,
|
||||
node: result
|
||||
}
|
||||
end)
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp paginate(
|
||||
_resource,
|
||||
_action,
|
||||
%Ash.Page.Keyset{
|
||||
results: results,
|
||||
count: count
|
||||
},
|
||||
false
|
||||
) do
|
||||
{:ok, %{results: results, count: count}}
|
||||
else
|
||||
{:ok, %{results: results, count: count, start_keyset: start_cursor, end_keyset: end_cursor}}
|
||||
end
|
||||
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}
|
||||
|
|
|
@ -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, [])
|
||||
|
|
|
@ -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,10 +867,15 @@ 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
|
||||
String.to_atom("#{type}_connection")
|
||||
else
|
||||
String.to_atom("page_of_#{type}")
|
||||
cond do
|
||||
relay? ->
|
||||
String.to_atom("#{type}_connection")
|
||||
|
||||
action.pagination.keyset? ->
|
||||
String.to_atom("keyset_page_of_#{type}")
|
||||
|
||||
true ->
|
||||
String.to_atom("page_of_#{type}")
|
||||
end
|
||||
else
|
||||
%Absinthe.Blueprint.TypeReference.NonNull{
|
||||
|
@ -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,67 +2294,157 @@ defmodule AshGraphql.Resource do
|
|||
},
|
||||
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{
|
||||
description: "#{inspect(type)} connection",
|
||||
fields: [
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: "Page information",
|
||||
identifier: :page_info,
|
||||
module: schema,
|
||||
name: "page_info",
|
||||
__reference__: ref(__ENV__),
|
||||
type: %Absinthe.Blueprint.TypeReference.NonNull{
|
||||
of_type: :page_info
|
||||
fields:
|
||||
[
|
||||
%Absinthe.Blueprint.Schema.FieldDefinition{
|
||||
description: "Page information",
|
||||
identifier: :page_info,
|
||||
module: schema,
|
||||
name: "page_info",
|
||||
__reference__: ref(__ENV__),
|
||||
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{
|
||||
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")
|
||||
}
|
||||
}
|
||||
],
|
||||
]
|
||||
|> add_count_to_relay(schema, countable?),
|
||||
identifier: String.to_atom("#{type}_connection"),
|
||||
module: schema,
|
||||
name: Macro.camelize("#{type}_connection"),
|
||||
__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
|
||||
else
|
||||
nil
|
||||
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: [
|
||||
%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
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue