improvement: validate that relay? queries use keyset?: true actions

improvement: only add `count` to pages when one relevant query is countable
This commit is contained in:
Zach Daniel 2022-10-21 07:38:33 -04:00
parent 3be18011ea
commit c5662e90ba
5 changed files with 95 additions and 66 deletions

View file

@ -1,9 +1,6 @@
defmodule AshGraphql do defmodule AshGraphql do
@moduledoc """ @moduledoc """
AshGraphql is a graphql front extension for the Ash framework. AshGraphql is a GraphQL extension for the Ash framework.
See the [getting started guide](/getting_started.md) for information on setting it up, and
see the `AshGraphql.Resource` documentation for docs on its DSL
""" """
defmacro __using__(opts) do defmacro __using__(opts) do

View file

@ -288,7 +288,8 @@ defmodule AshGraphql.Resource do
} }
@transformers [ @transformers [
AshGraphql.Resource.Transformers.RequireIdPkey, AshGraphql.Resource.Transformers.RequirePkeyDelimiter,
AshGraphql.Resource.Transformers.RequireKeysetForRelayQueries,
AshGraphql.Resource.Transformers.ValidateActions, AshGraphql.Resource.Transformers.ValidateActions,
AshGraphql.Resource.Transformers.ValidateCompatibleNames AshGraphql.Resource.Transformers.ValidateCompatibleNames
] ]
@ -2318,7 +2319,7 @@ defmodule AshGraphql.Resource do
} }
} }
] ]
|> add_count_to_relay(schema, countable?), |> add_count_to_page(schema, countable?),
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"),
@ -2329,7 +2330,7 @@ defmodule AshGraphql.Resource do
end end
end end
defp add_count_to_relay(fields, schema, true) do defp add_count_to_page(fields, schema, true) do
[ [
%Absinthe.Blueprint.Schema.FieldDefinition{ %Absinthe.Blueprint.Schema.FieldDefinition{
description: "Total count on all pages", description: "Total count on all pages",
@ -2343,7 +2344,7 @@ defmodule AshGraphql.Resource do
] ]
end end
defp add_count_to_relay(fields, _, _), do: fields defp add_count_to_page(fields, _, _), do: fields
# sobelow_skip ["DOS.StringToAtom"] # sobelow_skip ["DOS.StringToAtom"]
defp page_of(resource, schema) do defp page_of(resource, schema) do
@ -2356,31 +2357,33 @@ defmodule AshGraphql.Resource do
action.type == :read && action.pagination action.type == :read && action.pagination
end) end)
countable? =
resource
|> queries()
|> Enum.any?(fn query ->
action = Ash.Resource.Info.action(resource, query.action)
action.pagination && action.pagination.offset? && action.pagination.countable
end)
if paginatable? do if paginatable? do
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{ %Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "A page of #{inspect(type)}", description: "A page of #{inspect(type)}",
fields: [ fields:
%Absinthe.Blueprint.Schema.FieldDefinition{ [
description: "The records contained in the page", %Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :results, description: "The records contained in the page",
module: schema, identifier: :results,
name: "results", module: schema,
__reference__: ref(__ENV__), name: "results",
type: %Absinthe.Blueprint.TypeReference.List{ __reference__: ref(__ENV__),
of_type: %Absinthe.Blueprint.TypeReference.NonNull{ type: %Absinthe.Blueprint.TypeReference.List{
of_type: type of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: type
}
} }
} }
}, ]
%Absinthe.Blueprint.Schema.FieldDefinition{ |> add_count_to_page(schema, countable?),
description: "The count of records",
identifier: :count,
module: schema,
name: "count",
type: :integer,
__reference__: ref(__ENV__)
}
],
identifier: String.to_atom("page_of_#{type}"), identifier: String.to_atom("page_of_#{type}"),
module: schema, module: schema,
name: Macro.camelize("page_of_#{type}"), name: Macro.camelize("page_of_#{type}"),
@ -2400,47 +2403,49 @@ defmodule AshGraphql.Resource do
action.type == :read && action.pagination action.type == :read && action.pagination
end) end)
countable? =
resource
|> queries()
|> Enum.any?(fn query ->
action = Ash.Resource.Info.action(resource, query.action)
action.pagination && action.pagination.keyset? && action.pagination.countable
end)
if paginatable? do if paginatable? do
%Absinthe.Blueprint.Schema.ObjectTypeDefinition{ %Absinthe.Blueprint.Schema.ObjectTypeDefinition{
description: "A keyset page of #{inspect(type)}", description: "A keyset page of #{inspect(type)}",
fields: [ fields:
%Absinthe.Blueprint.Schema.FieldDefinition{ [
description: "The records contained in the page", %Absinthe.Blueprint.Schema.FieldDefinition{
identifier: :results, description: "The records contained in the page",
module: schema, identifier: :results,
name: "results", module: schema,
__reference__: ref(__ENV__), name: "results",
type: %Absinthe.Blueprint.TypeReference.List{ __reference__: ref(__ENV__),
of_type: %Absinthe.Blueprint.TypeReference.NonNull{ type: %Absinthe.Blueprint.TypeReference.List{
of_type: type of_type: %Absinthe.Blueprint.TypeReference.NonNull{
of_type: type
}
} }
},
%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__)
} }
}, ]
%Absinthe.Blueprint.Schema.FieldDefinition{ |> add_count_to_page(schema, countable?),
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}"), identifier: String.to_atom("keyset_page_of_#{type}"),
module: schema, module: schema,
name: Macro.camelize("keyset_page_of_#{type}"), name: Macro.camelize("keyset_page_of_#{type}"),

View file

@ -0,0 +1,27 @@
defmodule AshGraphql.Resource.Transformers.RequireKeysetForRelayQueries do
@moduledoc "Ensures that all relay queries configure keyset pagination"
use Spark.Dsl.Transformer
alias Spark.Dsl.Transformer
def after_compile?, do: true
def transform(dsl) do
dsl
|> AshGraphql.Resource.Info.queries()
|> Enum.each(fn query ->
if query.relay? do
action = Ash.Resource.Info.action(dsl, query.action)
unless action.pagination && action.pagination.keyset? do
raise Spark.Error.DslError,
module: Transformer.get_persisted(dsl, :module),
message: "Relay queries must support keyset pagination",
path: [:graphql, :queries, query.name]
end
end
end)
{:ok, dsl}
end
end

View file

@ -1,4 +1,4 @@
defmodule AshGraphql.Resource.Transformers.RequireIdPkey do defmodule AshGraphql.Resource.Transformers.RequirePkeyDelimiter do
@moduledoc "Ensures that the resource has a primary key called `id`" @moduledoc "Ensures that the resource has a primary key called `id`"
use Spark.Dsl.Transformer use Spark.Dsl.Transformer

View file

@ -23,7 +23,7 @@ defmodule AshGraphql.Test.RelayTag do
defaults([:create, :update, :destroy, :read]) defaults([:create, :update, :destroy, :read])
read :read_paginated do read :read_paginated do
pagination(required?: true, keyset?: true, countable: true) pagination(required?: true, offset?: true, keyset?: true, countable: true)
end end
end end