defmodule Ash.Resource.Actions.Read do
@moduledoc "Represents a read action on a resource."
defstruct arguments: [],
description: nil,
filter: nil,
filters: [],
get_by: nil,
get?: nil,
manual: nil,
metadata: [],
skip_unknown_inputs: [],
modify_query: nil,
multitenancy: nil,
name: nil,
pagination: nil,
preparations: [],
primary?: nil,
touches_resources: [],
timeout: nil,
transaction?: false,
type: :read
@type t :: %__MODULE__{
arguments: [Ash.Resource.Actions.Argument.t()],
description: String.t() | nil,
filter: any,
get_by: nil | atom | [atom],
get?: nil | boolean,
filters: [any],
manual: atom | {atom, Keyword.t()} | nil,
metadata: [Ash.Resource.Actions.Metadata.t()],
skip_unknown_inputs: list(atom | String.t()),
modify_query: nil | mfa,
multitenancy: atom,
name: atom,
pagination: any,
primary?: boolean,
touches_resources: [atom],
timeout: pos_integer() | nil,
transaction?: boolean,
type: :read
}
import Ash.Resource.Actions.SharedOptions
@global_opts shared_options()
@opt_schema Spark.Options.merge(
[
manual: [
type:
{:spark_function_behaviour, Ash.Resource.ManualRead,
{Ash.Resource.ManualRead.Function, 3}},
doc: """
Delegates running of the query to the provided module. Accepts a module or module and opts, or a function that takes the ash query, the data layer query, and context. See the [manual actions guide](/documentation/topics/manual-actions.md) for more.
"""
],
get?: [
type: :boolean,
default: false,
doc: """
Expresses that this action innately only returns a single result. Used by extensions to validate and/or modify behavior. Causes code interfaces to return a single value instead of a list. See the [code interface guide](/documentation/topics/resources/code-interfaces.md) for more.
"""
],
modify_query: [
type: {:or, [:mfa, {:fun, 2}]},
doc: """
Allows direct manipulation of the data layer query via an MFA. The ash query and the data layer query will be provided as additional arguments. The result must be `{:ok, new_data_layer_query} | {:error, error}`.
"""
],
get_by: [
type: {:wrap_list, :atom},
default: nil,
doc: """
A helper to automatically generate a "get by X" action. Sets `get?` to true, add args for each of the specified fields, and adds a filter for each of the arguments.
"""
],
timeout: [
type: :pos_integer,
doc: """
The maximum amount of time, in milliseconds, that the action is allowed to run for. Ignored if the data layer doesn't support transactions *and* async is disabled.
"""
],
multitenancy: [
type: {:in, [:enforce, :allow_global, :bypass, :bypass_all]},
default: :enforce,
doc: """
This setting defines how this action handles multitenancy. `:enforce` requires a tenant to be set (the default behavior), `:allow_global` allows using this action both with and without a tenant, `:bypass` completely ignores the tenant even if it's set, `:bypass_all` like `:bypass` but also bypasses the tenancy requirement for the nested resources. This is useful to change the behaviour of selected read action without the need of marking the whole resource with `global? true`.
"""
]
],
@global_opts,
"Action Options"
)
@pagination_schema [
keyset?: [
type: :boolean,
doc: "Whether or not keyset based pagination is supported",
default: false
],
offset?: [
type: :boolean,
doc: "Whether or not offset based pagination is supported",
default: false
],
default_limit: [
type: :pos_integer,
doc: "The default page size to apply, if one is not supplied"
],
# Change this default in 4.0
countable: [
type: {:in, [true, false, :by_default]},
doc:
"Whether not a returned page will have a full count of all records. Use `:by_default` to do it automatically.",
default: true
],
max_page_size: [
type: :pos_integer,
doc: "The maximum amount of records that can be requested in a single page",
default: 250
],
stable_sort: [
type: :any,
doc:
"A stable sort statement to add to a query (after any existing sorts). Only added if the sort does not already contain a stable sort (sorting on fields that uniquely identify a record). Defaults to the primary key."
],
required?: [
type: :boolean,
doc:
"Whether or not pagination can be disabled (by passing `page: false` to `Ash.Api.read!/2`, or by having `required?: false, default_limit: nil` set). Only relevant if some pagination configuration is supplied.",
default: true
]
]
defmodule Pagination do
@moduledoc "Represents the pagination configuration of a read action"
defstruct [
:default_limit,
:max_page_size,
countable: false,
stable_sort: nil,
required?: false,
keyset?: false,
offset?: false
]
@type t :: %__MODULE__{}
def transform(pagination) do
if pagination.keyset? or pagination.offset? do
{:ok, pagination}
else
{:error, "Must enable `keyset?` or `offset?`"}
end
end
end
def transform(read) do
{:ok,
read
|> transform_pagination()
|> concat_filters()}
end
def concat_filters(%{filters: [filter]} = read) do
%{read | filter: filter.filter}
end
def concat_filters(%{filters: [first | rest]} = read) do
filter =
Enum.reduce(rest, first.filter, fn filter, acc ->
Ash.Query.BooleanExpression.new(:and, filter.filter, acc)
end)
%{read | filter: filter}
end
@doc false
def concat_filters(read) do
read
end
defp transform_pagination(read) do
if read.pagination do
if is_list(read.pagination) do
%{read | pagination: List.last(read.pagination) || false}
else
%{read | pagination: read.pagination}
end
else
%{read | pagination: false}
end
end
@doc false
def opt_schema, do: @opt_schema
def pagination_schema, do: @pagination_schema
end